Compare commits
17 commits
restructur
...
master
Author | SHA1 | Date | |
---|---|---|---|
b513964735 | |||
78e9e60ed5 | |||
007236bd0f | |||
875775e1e4 | |||
ebd20b53ed | |||
6127260b91 | |||
3250810364 | |||
1a9c9e6b8a | |||
6b34c2d29b | |||
7eaeb36578 | |||
be95c23d5f | |||
2c854d4f8b | |||
e5f6d3ffaf | |||
e161d936aa | |||
2331ca03b2 | |||
588cf064c1 | |||
e72e79d1f7 |
87 changed files with 3335 additions and 5937 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1 +1,3 @@
|
|||
.log
|
||||
data
|
||||
|
||||
|
|
|
@ -1,89 +0,0 @@
|
|||
package list
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/list"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
func newItemDelegate(keys *delegateKeyMap) list.DefaultDelegate {
|
||||
d := list.NewDefaultDelegate()
|
||||
|
||||
d.UpdateFunc = func(msg tea.Msg, m *list.Model) tea.Cmd {
|
||||
var title string
|
||||
|
||||
if i, ok := m.SelectedItem().(Item); ok {
|
||||
title = i.Title()
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, keys.choose):
|
||||
return m.NewStatusMessage(statusMessageStyle("You chose " + title))
|
||||
|
||||
case key.Matches(msg, keys.remove):
|
||||
index := m.Index()
|
||||
m.RemoveItem(index)
|
||||
if len(m.Items()) == 0 {
|
||||
keys.remove.SetEnabled(false)
|
||||
}
|
||||
return m.NewStatusMessage(statusMessageStyle("Deleted " + title))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
help := []key.Binding{keys.choose, keys.remove}
|
||||
|
||||
d.ShortHelpFunc = func() []key.Binding {
|
||||
return help
|
||||
}
|
||||
|
||||
d.FullHelpFunc = func() [][]key.Binding {
|
||||
return [][]key.Binding{help}
|
||||
}
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
type delegateKeyMap struct {
|
||||
choose key.Binding
|
||||
remove key.Binding
|
||||
}
|
||||
|
||||
// Additional short help entries. This satisfies the help.KeyMap interface and
|
||||
// is entirely optional.
|
||||
func (d delegateKeyMap) ShortHelp() []key.Binding {
|
||||
return []key.Binding{
|
||||
d.choose,
|
||||
d.remove,
|
||||
}
|
||||
}
|
||||
|
||||
// Additional full help entries. This satisfies the help.KeyMap interface and
|
||||
// is entirely optional.
|
||||
func (d delegateKeyMap) FullHelp() [][]key.Binding {
|
||||
return [][]key.Binding{
|
||||
{
|
||||
d.choose,
|
||||
d.remove,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newDelegateKeyMap() *delegateKeyMap {
|
||||
return &delegateKeyMap{
|
||||
choose: key.NewBinding(
|
||||
key.WithKeys("enter"),
|
||||
key.WithHelp("enter", "choose"),
|
||||
),
|
||||
remove: key.NewBinding(
|
||||
key.WithKeys("x", "backspace"),
|
||||
key.WithHelp("x", "delete"),
|
||||
),
|
||||
}
|
||||
}
|
|
@ -1,158 +0,0 @@
|
|||
package list
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/list"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
var (
|
||||
appStyle = lipgloss.NewStyle().Padding(1, 2)
|
||||
|
||||
titleStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFDF5")).
|
||||
Background(lipgloss.Color("#25A065")).
|
||||
Padding(0, 1)
|
||||
|
||||
statusMessageStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.AdaptiveColor{Light: "#04B575", Dark: "#04B575"}).
|
||||
Render
|
||||
)
|
||||
|
||||
type Item struct {
|
||||
title string
|
||||
description string
|
||||
}
|
||||
|
||||
func NewItem(title string, description string) Item {
|
||||
return Item{title, description}
|
||||
}
|
||||
|
||||
func (i Item) Title() string { return i.title }
|
||||
func (i Item) Description() string { return i.description }
|
||||
func (i Item) FilterValue() string { return i.title }
|
||||
|
||||
type listKeyMap struct {
|
||||
toggleSpinner key.Binding
|
||||
toggleTitleBar key.Binding
|
||||
toggleStatusBar key.Binding
|
||||
togglePagination key.Binding
|
||||
toggleHelpMenu key.Binding
|
||||
}
|
||||
|
||||
func newListKeyMap() *listKeyMap {
|
||||
return &listKeyMap{
|
||||
toggleSpinner: key.NewBinding(
|
||||
key.WithKeys("s"),
|
||||
key.WithHelp("s", "toggle spinner"),
|
||||
),
|
||||
toggleTitleBar: key.NewBinding(
|
||||
key.WithKeys("T"),
|
||||
key.WithHelp("T", "toggle title"),
|
||||
),
|
||||
toggleStatusBar: key.NewBinding(
|
||||
key.WithKeys("S"),
|
||||
key.WithHelp("S", "toggle status"),
|
||||
),
|
||||
togglePagination: key.NewBinding(
|
||||
key.WithKeys("P"),
|
||||
key.WithHelp("P", "toggle pagination"),
|
||||
),
|
||||
toggleHelpMenu: key.NewBinding(
|
||||
key.WithKeys("H"),
|
||||
key.WithHelp("H", "toggle help"),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
type model struct {
|
||||
list list.Model
|
||||
keys *listKeyMap
|
||||
delegateKeys *delegateKeyMap
|
||||
}
|
||||
|
||||
func NewList(title string, items []list.Item) model {
|
||||
var (
|
||||
delegateKeys = newDelegateKeyMap()
|
||||
listKeys = newListKeyMap()
|
||||
)
|
||||
|
||||
// Setup list
|
||||
delegate := newItemDelegate(delegateKeys)
|
||||
groceryList := list.New(items, delegate, 0, 0)
|
||||
groceryList.Title = title
|
||||
groceryList.Styles.Title = titleStyle
|
||||
groceryList.AdditionalFullHelpKeys = func() []key.Binding {
|
||||
return []key.Binding{
|
||||
listKeys.toggleSpinner,
|
||||
listKeys.toggleTitleBar,
|
||||
listKeys.toggleStatusBar,
|
||||
listKeys.togglePagination,
|
||||
listKeys.toggleHelpMenu,
|
||||
}
|
||||
}
|
||||
|
||||
return model{
|
||||
list: groceryList,
|
||||
keys: listKeys,
|
||||
delegateKeys: delegateKeys,
|
||||
}
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
return tea.EnterAltScreen
|
||||
}
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmds []tea.Cmd
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
h, v := appStyle.GetFrameSize()
|
||||
m.list.SetSize(msg.Width-h, msg.Height-v)
|
||||
|
||||
case tea.KeyMsg:
|
||||
// Don't match any of the keys below if we're actively filtering.
|
||||
if m.list.FilterState() == list.Filtering {
|
||||
break
|
||||
}
|
||||
|
||||
switch {
|
||||
case key.Matches(msg, m.keys.toggleSpinner):
|
||||
cmd := m.list.ToggleSpinner()
|
||||
return m, cmd
|
||||
|
||||
case key.Matches(msg, m.keys.toggleTitleBar):
|
||||
v := !m.list.ShowTitle()
|
||||
m.list.SetShowTitle(v)
|
||||
m.list.SetShowFilter(v)
|
||||
m.list.SetFilteringEnabled(v)
|
||||
return m, nil
|
||||
|
||||
case key.Matches(msg, m.keys.toggleStatusBar):
|
||||
m.list.SetShowStatusBar(!m.list.ShowStatusBar())
|
||||
return m, nil
|
||||
|
||||
case key.Matches(msg, m.keys.togglePagination):
|
||||
m.list.SetShowPagination(!m.list.ShowPagination())
|
||||
return m, nil
|
||||
|
||||
case key.Matches(msg, m.keys.toggleHelpMenu):
|
||||
m.list.SetShowHelp(!m.list.ShowHelp())
|
||||
return m, nil
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// This will also call our delegate's update function.
|
||||
newListModel, cmd := m.list.Update(msg)
|
||||
m.list = newListModel
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m model) View() string {
|
||||
return appStyle.Render(m.list.View())
|
||||
}
|
|
@ -1,81 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"log/slog"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"git.andreafazzi.eu/andrea/probo/pkg/store/file"
|
||||
"github.com/lmittmann/tint"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func main() {
|
||||
slog.SetDefault(slog.New(
|
||||
tint.NewHandler(os.Stdout, &tint.Options{
|
||||
Level: slog.LevelInfo,
|
||||
TimeFormat: time.Kitchen,
|
||||
}),
|
||||
))
|
||||
|
||||
_, err := os.Stat(file.DefaultBaseDir)
|
||||
if err != nil {
|
||||
slog.Info("Base directory not found. Creating the folder structure...")
|
||||
for _, dir := range file.Dirs {
|
||||
err := os.MkdirAll(dir, os.ModePerm)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
slog.Info("Create", "directory", dir)
|
||||
}
|
||||
|
||||
slog.Info("Folder structure created without issues.")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
app := &cli.App{
|
||||
Name: "probo-cli",
|
||||
Usage: "Quiz Management System for Hackers!",
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "session",
|
||||
Aliases: []string{"s"},
|
||||
Usage: "options for command 'session'",
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "push",
|
||||
Usage: "Create a new exam session",
|
||||
Action: push,
|
||||
},
|
||||
{
|
||||
Name: "pull",
|
||||
Usage: "Download responses from a session",
|
||||
Action: pull,
|
||||
},
|
||||
{
|
||||
Name: "scores",
|
||||
Usage: "Show the scores for the given session",
|
||||
Action: scores,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "participant",
|
||||
Aliases: []string{"p"},
|
||||
Usage: "options for command 'participant'",
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "import",
|
||||
Usage: "Import participants from a CSV file",
|
||||
Action: importCSV,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"git.andreafazzi.eu/andrea/probo/pkg/store/file"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func importCSV(cCtx *cli.Context) error {
|
||||
path := cCtx.Args().First()
|
||||
if path == "" {
|
||||
return cli.Exit("Path for the CSV file not given.", 1)
|
||||
}
|
||||
|
||||
pStore, err := file.NewDefaultParticipantFileStore()
|
||||
if err != nil {
|
||||
return cli.Exit(fmt.Sprintf("An error occurred: %v", err), 1)
|
||||
}
|
||||
|
||||
participants, err := pStore.ImportCSV(path)
|
||||
if err != nil {
|
||||
return cli.Exit(fmt.Sprintf("An error occurred: %v", err), 1)
|
||||
}
|
||||
|
||||
slog.Info("Imported participants from csv file", "nParticipants", len(participants))
|
||||
|
||||
return nil
|
||||
|
||||
}
|
|
@ -1,144 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"git.andreafazzi.eu/andrea/probo/cmd/textinput"
|
||||
"git.andreafazzi.eu/andrea/probo/pkg/sessionmanager"
|
||||
"git.andreafazzi.eu/andrea/probo/pkg/store/file"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func push(cCtx *cli.Context) error {
|
||||
pStore, err := file.NewDefaultParticipantFileStore()
|
||||
if err != nil {
|
||||
return cli.Exit(fmt.Sprintf("An error occurred: %v", err), 1)
|
||||
}
|
||||
|
||||
participants := pStore.ReadAll()
|
||||
|
||||
if len(participants) == 0 {
|
||||
return cli.Exit("No participants found!", 1)
|
||||
}
|
||||
|
||||
sessionName := cCtx.Args().First()
|
||||
|
||||
if cCtx.Args().Len() < 1 {
|
||||
input := textinput.NewTextInput("My exam session")
|
||||
p := tea.NewProgram(input)
|
||||
if _, err := p.Run(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
sessionName = input.Value()
|
||||
}
|
||||
|
||||
sStore, err := file.NewDefaultSessionFileStore()
|
||||
if err != nil {
|
||||
log.Fatalf("An error occurred: %v", err)
|
||||
}
|
||||
|
||||
qStore, err := file.NewDefaultQuizFileStore()
|
||||
if err != nil {
|
||||
log.Fatalf("An error occurred: %v", err)
|
||||
}
|
||||
|
||||
sm, err := sessionmanager.NewSessionManager(
|
||||
"http://localhost:8080/",
|
||||
pStore.Storer,
|
||||
qStore.Storer,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("An error occurred: %v", err)
|
||||
}
|
||||
|
||||
session, err := sm.Push(sessionName)
|
||||
if err != nil {
|
||||
log.Fatalf("An error occurred: %v", err)
|
||||
}
|
||||
|
||||
_, err = sStore.Create(session)
|
||||
if err != nil {
|
||||
log.Fatalf("An error occurred: %v", err)
|
||||
}
|
||||
|
||||
log.Println("Session upload completed with success!")
|
||||
|
||||
for _, p := range pStore.ReadAll() {
|
||||
log.Printf("http://localhost:8080/%v/%v", session.ID, p.Token)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func pull(cCtx *cli.Context) error {
|
||||
if cCtx.Args().Len() < 1 {
|
||||
log.Fatalf("Please provide a session ID as first argument of pull.")
|
||||
}
|
||||
|
||||
rStore, err := file.NewDefaultResponseFileStore()
|
||||
if err != nil {
|
||||
log.Fatalf("An error occurred: %v", err)
|
||||
}
|
||||
|
||||
pStore, err := file.NewDefaultParticipantFileStore()
|
||||
if err != nil {
|
||||
log.Fatalf("An error occurred: %v", err)
|
||||
}
|
||||
qStore, err := file.NewDefaultQuizFileStore()
|
||||
if err != nil {
|
||||
log.Fatalf("An error occurred: %v", err)
|
||||
}
|
||||
|
||||
sm, err := sessionmanager.NewSessionManager(
|
||||
"http://localhost:8080/",
|
||||
pStore.Storer,
|
||||
qStore.Storer,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("An error occurred: %v", err)
|
||||
}
|
||||
|
||||
err = sm.Pull(rStore, cCtx.Args().First())
|
||||
if err != nil {
|
||||
log.Fatalf("An error occurred: %v", err)
|
||||
}
|
||||
|
||||
log.Println("Responses download completed with success!")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func scores(cCtx *cli.Context) error {
|
||||
if cCtx.Args().Len() < 1 {
|
||||
log.Fatalf("Please provide a session name as first argument of 'score'.")
|
||||
}
|
||||
|
||||
sStore, err := file.NewDefaultSessionFileStore()
|
||||
if err != nil {
|
||||
log.Fatalf("An error occurred: %v", err)
|
||||
}
|
||||
session, err := sStore.Read(cCtx.Args().First())
|
||||
if err != nil {
|
||||
log.Fatalf("An error occurred: %v", err)
|
||||
}
|
||||
rStore, err := file.NewDefaultResponseFileStore()
|
||||
if err != nil {
|
||||
log.Fatalf("An error occurred: %v", err)
|
||||
}
|
||||
|
||||
scores, err := sessionmanager.NewScores(rStore, session)
|
||||
if err != nil {
|
||||
log.Fatalf("An error occurred: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println(scores)
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
package textinput
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type (
|
||||
errMsg error
|
||||
)
|
||||
|
||||
type model struct {
|
||||
textInput textinput.Model
|
||||
err error
|
||||
}
|
||||
|
||||
func NewTextInput(placeholder string) *model {
|
||||
ti := textinput.New()
|
||||
ti.Placeholder = placeholder
|
||||
ti.Focus()
|
||||
ti.CharLimit = 156
|
||||
ti.Width = 20
|
||||
|
||||
return &model{
|
||||
textInput: ti,
|
||||
err: nil,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) Value() string {
|
||||
return m.textInput.Value()
|
||||
}
|
||||
|
||||
func (m *model) Init() tea.Cmd {
|
||||
return textinput.Blink
|
||||
}
|
||||
|
||||
func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmd tea.Cmd
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.Type {
|
||||
case tea.KeyEnter, tea.KeyCtrlC, tea.KeyEsc:
|
||||
return m, tea.Quit
|
||||
}
|
||||
|
||||
// We handle errors just like any other message
|
||||
case errMsg:
|
||||
m.err = msg
|
||||
return m, nil
|
||||
}
|
||||
|
||||
m.textInput, cmd = m.textInput.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m *model) View() string {
|
||||
return fmt.Sprintf(
|
||||
"\nPlease insert the name of the session\n\n%s\n\n%s",
|
||||
m.textInput.View(),
|
||||
"(esc to quit)",
|
||||
) + "\n"
|
||||
}
|
8
cmd/common.go
Normal file
8
cmd/common.go
Normal file
|
@ -0,0 +1,8 @@
|
|||
package cmd
|
||||
|
||||
var logo = ` ____ _
|
||||
| _ \ _ __ ___ | |__ ___
|
||||
| |_) | '__/ _ \| '_ \ / _ \
|
||||
| __/| | | (_) | |_) | (_) |
|
||||
|_| |_| \___/|_.__/ \___/
|
||||
`
|
61
cmd/filter.go
Normal file
61
cmd/filter.go
Normal file
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
Copyright © 2024 NAME HERE <EMAIL ADDRESS>
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.andreafazzi.eu/andrea/probo/cmd/filter"
|
||||
"git.andreafazzi.eu/andrea/probo/cmd/util"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/muesli/termenv"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var filterCmd = &cobra.Command{
|
||||
Use: "filter {participants,quizzes,responses}",
|
||||
Short: "Filter the given store",
|
||||
Long: util.RenderMarkdownTemplates("cli/*.tmpl", "cli/filter/*.tmpl"),
|
||||
Run: runFilter,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(filterCmd)
|
||||
filterCmd.PersistentFlags().StringP("input", "i", "", "Specify an input file")
|
||||
}
|
||||
|
||||
func runFilter(cmd *cobra.Command, args []string) {
|
||||
var storeType string
|
||||
|
||||
path, err := cmd.Flags().GetString("input")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
f := util.LogToFile()
|
||||
if f != nil {
|
||||
defer f.Close()
|
||||
}
|
||||
|
||||
lipgloss.SetColorProfile(termenv.TrueColor)
|
||||
|
||||
storeType = args[0]
|
||||
|
||||
model, err := tea.NewProgram(
|
||||
filter.New(path, storeType, util.ReadStdin()),
|
||||
tea.WithOutput(os.Stderr),
|
||||
).Run()
|
||||
if err != nil {
|
||||
fmt.Println("Error running program:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
result := model.(*filter.FilterModel)
|
||||
|
||||
if result.Result != "" {
|
||||
fmt.Fprintf(os.Stdout, result.Result)
|
||||
}
|
||||
}
|
418
cmd/filter/filter.go
Normal file
418
cmd/filter/filter.go
Normal file
|
@ -0,0 +1,418 @@
|
|||
package filter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.andreafazzi.eu/andrea/probo/pkg/store/file"
|
||||
"github.com/alecthomas/chroma/quick"
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/spinner"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/itchyny/gojq"
|
||||
foam "github.com/remogatto/sugarfoam"
|
||||
"github.com/remogatto/sugarfoam/components/group"
|
||||
"github.com/remogatto/sugarfoam/components/header"
|
||||
"github.com/remogatto/sugarfoam/components/help"
|
||||
"github.com/remogatto/sugarfoam/components/statusbar"
|
||||
"github.com/remogatto/sugarfoam/components/textinput"
|
||||
"github.com/remogatto/sugarfoam/components/viewport"
|
||||
"github.com/remogatto/sugarfoam/layout"
|
||||
)
|
||||
|
||||
type FilterModel struct {
|
||||
// UI
|
||||
textInput *textinput.Model
|
||||
viewport *viewport.Model
|
||||
group *group.Model
|
||||
help *help.Model
|
||||
statusBar *statusbar.Model
|
||||
spinner spinner.Model
|
||||
|
||||
// Layout
|
||||
document *layout.Layout
|
||||
|
||||
// Key bindings
|
||||
bindings *keyBindings
|
||||
|
||||
// file store
|
||||
store []any
|
||||
result []any
|
||||
|
||||
// json
|
||||
lastQuery string
|
||||
FilteredJson string
|
||||
InputJson string
|
||||
Result string
|
||||
|
||||
// filter file
|
||||
filterFilePath string
|
||||
|
||||
state int
|
||||
|
||||
filterType string
|
||||
}
|
||||
|
||||
func New(path string, filterType string, stdin string) *FilterModel {
|
||||
textInput := textinput.New(
|
||||
textinput.WithPlaceholder("Write your jq filter here..."),
|
||||
)
|
||||
|
||||
viewport := viewport.New()
|
||||
|
||||
group := group.New(
|
||||
group.WithItems(textInput, viewport),
|
||||
group.WithLayout(
|
||||
layout.New(
|
||||
layout.WithStyles(&layout.Styles{Container: lipgloss.NewStyle().Padding(1, 0, 1, 0)}),
|
||||
layout.WithItem(textInput),
|
||||
layout.WithItem(viewport),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
bindings := newBindings(group)
|
||||
statusBar := statusbar.New(bindings)
|
||||
|
||||
s := spinner.New(
|
||||
spinner.WithStyle(
|
||||
lipgloss.NewStyle().Foreground(lipgloss.Color("265"))),
|
||||
)
|
||||
s.Spinner = spinner.Dot
|
||||
|
||||
header := header.New(
|
||||
header.WithContent(
|
||||
lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Border(lipgloss.NormalBorder(), false, false, true, false).
|
||||
Render(filterTypeFormats[filterType]),
|
||||
),
|
||||
)
|
||||
|
||||
help := help.New(
|
||||
bindings,
|
||||
help.WithStyles(&foam.Styles{NoBorder: lipgloss.NewStyle().Padding(1, 1)}))
|
||||
|
||||
document := layout.New(
|
||||
layout.WithStyles(&layout.Styles{Container: lipgloss.NewStyle().Margin(1)}),
|
||||
layout.WithItem(header),
|
||||
layout.WithItem(group),
|
||||
layout.WithItem(help),
|
||||
layout.WithItem(statusBar),
|
||||
)
|
||||
|
||||
return &FilterModel{
|
||||
textInput: textInput,
|
||||
viewport: viewport,
|
||||
group: group,
|
||||
statusBar: statusBar,
|
||||
spinner: s,
|
||||
document: document,
|
||||
bindings: bindings,
|
||||
help: help,
|
||||
filterType: filterType,
|
||||
filterFilePath: path,
|
||||
InputJson: stdin,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *FilterModel) Init() tea.Cmd {
|
||||
var cmds []tea.Cmd
|
||||
|
||||
cmds = append(cmds, m.group.Init(), m.loadStore(), m.spinner.Tick)
|
||||
|
||||
m.group.Focus()
|
||||
|
||||
return tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m *FilterModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmds []tea.Cmd
|
||||
|
||||
switch msg := msg.(type) {
|
||||
|
||||
case tea.WindowSizeMsg:
|
||||
m.document.SetSize(msg.Width, msg.Height)
|
||||
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, m.bindings.quit):
|
||||
m.FilteredJson = ""
|
||||
return m, tea.Quit
|
||||
|
||||
case key.Matches(msg, m.bindings.enter):
|
||||
m.marshalJSON()
|
||||
return m, tea.Quit
|
||||
}
|
||||
|
||||
case storeLoadedMsg:
|
||||
cmds = append(cmds, m.handleStoreLoaded(msg))
|
||||
|
||||
case resultMsg:
|
||||
cmds = append(cmds, m.handleFiltered(msg))
|
||||
m.state = FilterState
|
||||
|
||||
case errorMsg:
|
||||
m.handleError(msg)
|
||||
m.state = ErrorState
|
||||
}
|
||||
|
||||
cmds = m.handleState(msg, cmds)
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m *FilterModel) View() string {
|
||||
return m.document.View()
|
||||
}
|
||||
|
||||
func (m *FilterModel) marshalJSON() {
|
||||
if m.FilteredJson == "" {
|
||||
return
|
||||
}
|
||||
if m.InputJson != "" {
|
||||
m.Result = fmt.Sprintf("{%s, \"%s\": %s}", strings.Trim(m.InputJson, "{}"), m.filterType, m.FilteredJson)
|
||||
} else {
|
||||
var result interface{}
|
||||
|
||||
filtered := fmt.Sprintf("{\"%s\": %s}", m.filterType, m.FilteredJson)
|
||||
err := json.Unmarshal([]byte(filtered), &result)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
resultJson, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
m.Result = string(resultJson)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *FilterModel) showErrorOnStatusBar(err error) {
|
||||
m.statusBar.SetContent(
|
||||
stateFormats[ErrorState][0],
|
||||
fmt.Sprintf(stateFormats[ErrorState][1], err),
|
||||
stateFormats[ErrorState][2],
|
||||
)
|
||||
}
|
||||
|
||||
func (m *FilterModel) handleError(msg tea.Msg) {
|
||||
err := msg.(errorMsg)
|
||||
|
||||
m.statusBar.SetContent(
|
||||
stateFormats[ErrorState][0],
|
||||
fmt.Sprintf(stateFormats[ErrorState][1], err.error),
|
||||
stateFormats[ErrorState][2],
|
||||
)
|
||||
}
|
||||
|
||||
func (m *FilterModel) handleStoreLoaded(msg tea.Msg) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
storeMsg := msg.(storeLoadedMsg)
|
||||
m.store = storeMsg.store
|
||||
m.state = FilterState
|
||||
|
||||
if m.filterFilePath != "" {
|
||||
jq, err := os.ReadFile(m.filterFilePath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
m.textInput.SetValue(strings.TrimSpace(string(jq)))
|
||||
return m.query(strings.TrimSpace(string(jq)))
|
||||
}
|
||||
|
||||
coloredJson, err := toColoredJson(m.store)
|
||||
if err != nil {
|
||||
return errorMsg{err}
|
||||
}
|
||||
|
||||
m.viewport.SetContent(coloredJson)
|
||||
|
||||
return m.query(".")
|
||||
}
|
||||
}
|
||||
|
||||
func (m *FilterModel) handleFiltered(msg tea.Msg) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
m.result = msg.(resultMsg).result
|
||||
|
||||
coloredJson, err := toColoredJson(m.result)
|
||||
if err != nil {
|
||||
return errorMsg{err}
|
||||
}
|
||||
|
||||
json, err := toJson(m.result)
|
||||
if err != nil {
|
||||
return errorMsg{err}
|
||||
}
|
||||
|
||||
m.FilteredJson = json
|
||||
|
||||
m.viewport.SetContent(coloredJson)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m *FilterModel) handleState(msg tea.Msg, cmds []tea.Cmd) []tea.Cmd {
|
||||
_, cmd := m.group.Update(msg)
|
||||
|
||||
if m.state == LoadingStoreState {
|
||||
return m.updateSpinner(msg, cmd, cmds)
|
||||
}
|
||||
|
||||
cmds = append(cmds, cmd, m.query(m.textInput.Value()))
|
||||
|
||||
if m.state != ErrorState {
|
||||
m.statusBar.SetContent(stateFormats[FilterState]...)
|
||||
}
|
||||
|
||||
return cmds
|
||||
}
|
||||
|
||||
func (m *FilterModel) updateSpinner(msg tea.Msg, cmd tea.Cmd, cmds []tea.Cmd) []tea.Cmd {
|
||||
m.spinner, cmd = m.spinner.Update(msg)
|
||||
|
||||
m.statusBar.SetContent(fmt.Sprintf(stateFormats[m.state][0], m.spinner.View()), stateFormats[m.state][1], stateFormats[m.state][2])
|
||||
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
return cmds
|
||||
}
|
||||
|
||||
func (m *FilterModel) loadStore() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
var jsonStore []byte
|
||||
|
||||
switch m.filterType {
|
||||
case "participants":
|
||||
pStore, err := file.NewDefaultParticipantFileStore()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
jsonStore, err = pStore.Storer.Json()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
case "quizzes":
|
||||
qStore, err := file.NewDefaultQuizFileStore()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
jsonStore, err = qStore.Storer.Json()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
case "responses":
|
||||
qStore, err := file.NewDefaultResponseFileStore()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
jsonStore, err = qStore.Storer.Json()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
default:
|
||||
panic("Unknown filter type!")
|
||||
}
|
||||
|
||||
v := make([]any, 0)
|
||||
|
||||
err := json.Unmarshal(jsonStore, &v)
|
||||
if err != nil {
|
||||
return errorMsg{err}
|
||||
}
|
||||
|
||||
return storeLoadedMsg{v}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *FilterModel) query(input string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
if input == m.lastQuery {
|
||||
return nil
|
||||
}
|
||||
|
||||
if m.state == LoadingStoreState {
|
||||
return nil
|
||||
}
|
||||
|
||||
m.lastQuery = input
|
||||
|
||||
query, err := gojq.Parse(input)
|
||||
if err != nil {
|
||||
return errorMsg{fmt.Errorf("jq query parse error: %v", err)}
|
||||
}
|
||||
|
||||
var result []string
|
||||
|
||||
iter := query.Run(m.store)
|
||||
for {
|
||||
v, ok := iter.Next()
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
|
||||
if err, ok := v.(error); ok {
|
||||
return errorMsg{fmt.Errorf("jq query run error: %v", err)}
|
||||
}
|
||||
|
||||
b, err := json.MarshalIndent(v, "", " ")
|
||||
if err != nil {
|
||||
return errorMsg{err}
|
||||
}
|
||||
|
||||
result = append(result, string(b))
|
||||
}
|
||||
|
||||
v := make([]any, 0)
|
||||
|
||||
err = json.Unmarshal([]byte(strings.Join(result, "\n")), &v)
|
||||
if err != nil {
|
||||
return errorMsg{err}
|
||||
}
|
||||
|
||||
return resultMsg{v}
|
||||
}
|
||||
}
|
||||
|
||||
func toColoredJson(data []any) (string, error) {
|
||||
result, err := json.MarshalIndent(data, "", " ")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
coloredBytes := make([]byte, 0)
|
||||
buffer := bytes.NewBuffer(coloredBytes)
|
||||
|
||||
err = quick.Highlight(buffer, string(result), "json", "terminal16m", "dracula")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return sanitize(buffer.String()), nil
|
||||
}
|
||||
|
||||
func toJson(data []any) (string, error) {
|
||||
result, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(result), nil
|
||||
}
|
||||
func sanitize(text string) string {
|
||||
// FIXME: The use of a standard '-' character causes rendering
|
||||
// issues within the viewport. Further investigation is
|
||||
// required to resolve this problem.
|
||||
return strings.Replace(text, "-", "–", -1)
|
||||
}
|
14
cmd/filter/format.go
Normal file
14
cmd/filter/format.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
package filter
|
||||
|
||||
var (
|
||||
stateFormats = map[int][]string{
|
||||
FilterState: []string{"FILTER 📖", "Write your jq command in the input box to start filtering. Press enter to return the result.", "STORE 🟢"},
|
||||
LoadingStoreState: []string{"LOAD %s", "Loading the store...", "STORE 🔴"},
|
||||
ErrorState: []string{"ERROR 📖", "%v", "STORE 🟢"},
|
||||
}
|
||||
filterTypeFormats = map[string]string{
|
||||
"participants": "👫 Participants filter 👫",
|
||||
"quizzes": "❓ Quizzes filter ❓",
|
||||
"responses": "📝 Responses filter 📝",
|
||||
}
|
||||
)
|
55
cmd/filter/keymap.go
Normal file
55
cmd/filter/keymap.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
package filter
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/remogatto/sugarfoam/components/group"
|
||||
"github.com/remogatto/sugarfoam/components/viewport"
|
||||
)
|
||||
|
||||
type keyBindings struct {
|
||||
group *group.Model
|
||||
|
||||
quit, enter key.Binding
|
||||
}
|
||||
|
||||
func (k *keyBindings) ShortHelp() []key.Binding {
|
||||
keys := make([]key.Binding, 0)
|
||||
|
||||
current := k.group.Current()
|
||||
|
||||
switch item := current.(type) {
|
||||
case *viewport.Model:
|
||||
keys = append(
|
||||
keys,
|
||||
item.KeyMap.Up,
|
||||
item.KeyMap.Down,
|
||||
)
|
||||
}
|
||||
|
||||
keys = append(
|
||||
keys,
|
||||
k.quit,
|
||||
)
|
||||
|
||||
return keys
|
||||
}
|
||||
|
||||
func (k keyBindings) FullHelp() [][]key.Binding {
|
||||
return [][]key.Binding{
|
||||
{
|
||||
k.quit,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newBindings(g *group.Model) *keyBindings {
|
||||
return &keyBindings{
|
||||
group: g,
|
||||
quit: key.NewBinding(
|
||||
key.WithKeys("esc"), key.WithHelp("esc", "Quit app"),
|
||||
),
|
||||
enter: key.NewBinding(
|
||||
key.WithKeys("enter"), key.WithHelp("enter", "Quit app and return the results"),
|
||||
),
|
||||
}
|
||||
}
|
13
cmd/filter/message.go
Normal file
13
cmd/filter/message.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package filter
|
||||
|
||||
type storeLoadedMsg struct {
|
||||
store []any
|
||||
}
|
||||
|
||||
type resultMsg struct {
|
||||
result []any
|
||||
}
|
||||
|
||||
type errorMsg struct {
|
||||
error error
|
||||
}
|
7
cmd/filter/state.go
Normal file
7
cmd/filter/state.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package filter
|
||||
|
||||
const (
|
||||
LoadingStoreState = iota
|
||||
FilterState
|
||||
ErrorState
|
||||
)
|
39
cmd/import.go
Normal file
39
cmd/import.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
Copyright © 2024 NAME HERE <EMAIL ADDRESS>
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// importCmd represents the import command
|
||||
var importCmd = &cobra.Command{
|
||||
Use: "import",
|
||||
Short: "Import entities",
|
||||
Long: `A longer description that spans multiple lines and likely contains examples
|
||||
and usage of using your command. For example:
|
||||
|
||||
Cobra is a CLI library for Go that empowers applications.
|
||||
This application is a tool to generate the needed files
|
||||
to quickly create a Cobra application.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Println("import called")
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(importCmd)
|
||||
|
||||
// Here you will define your flags and configuration settings.
|
||||
|
||||
// Cobra supports Persistent Flags which will work for this command
|
||||
// and all subcommands, e.g.:
|
||||
// importCmd.PersistentFlags().String("foo", "", "A help for foo")
|
||||
|
||||
// Cobra supports local flags which will only run when this command
|
||||
// is called directly, e.g.:
|
||||
// importCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||
}
|
38
cmd/init.go
Normal file
38
cmd/init.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
Copyright © 2024 NAME HERE <EMAIL ADDRESS>
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"git.andreafazzi.eu/andrea/probo/cmd/util"
|
||||
"git.andreafazzi.eu/andrea/probo/embed"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// initCmd represents the init command
|
||||
var initCmd = &cobra.Command{
|
||||
Use: "init",
|
||||
Short: "Initialize a working directory",
|
||||
Long: util.RenderMarkdownTemplates("cli/*.tmpl", "cli/init/*.tmpl"),
|
||||
Run: runInit,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(initCmd)
|
||||
}
|
||||
|
||||
func runInit(cmd *cobra.Command, args []string) {
|
||||
err := embed.CopyToWorkingDirectory(embed.Data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = embed.CopyToWorkingDirectory(embed.Templates)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = embed.CopyToWorkingDirectory(embed.Public)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
}
|
59
cmd/rank.go
Normal file
59
cmd/rank.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
Copyright © 2024 NAME HERE <EMAIL ADDRESS>
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.andreafazzi.eu/andrea/probo/cmd/rank"
|
||||
"git.andreafazzi.eu/andrea/probo/cmd/util"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/muesli/termenv"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// rankCmd represents the rank command
|
||||
var rankCmd = &cobra.Command{
|
||||
Use: "rank",
|
||||
Short: "Show a ranking from the given responses.",
|
||||
Long: util.RenderMarkdownTemplates("cli/*.tmpl", "cli/rank/*.tmpl"),
|
||||
Run: runRank,
|
||||
}
|
||||
|
||||
func runRank(cmd *cobra.Command, args []string) {
|
||||
path, err := cmd.Flags().GetString("input")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
f := util.LogToFile()
|
||||
if f != nil {
|
||||
defer f.Close()
|
||||
}
|
||||
|
||||
lipgloss.SetColorProfile(termenv.TrueColor)
|
||||
|
||||
model, err := tea.NewProgram(
|
||||
rank.New(path, util.ReadStdin()),
|
||||
tea.WithOutput(os.Stderr),
|
||||
).Run()
|
||||
if err != nil {
|
||||
fmt.Println("Error running program:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
result := model.(*rank.RankModel)
|
||||
|
||||
if result.Result != "" {
|
||||
fmt.Fprintf(os.Stdout, result.Result)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(rankCmd)
|
||||
rootCmd.PersistentFlags().StringP("input", "i", "", "Specify an input file")
|
||||
}
|
9
cmd/rank/format.go
Normal file
9
cmd/rank/format.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package rank
|
||||
|
||||
var (
|
||||
stateFormats = map[int][]string{
|
||||
BrowseState: []string{"BROWSE 📖", "Total scores: %d", "🟢"},
|
||||
ExecutingScriptState: []string{"EXE %s", "Executing script...", "🔴"},
|
||||
ErrorState: []string{"ERROR 📖", "%v", "🔴"},
|
||||
}
|
||||
)
|
62
cmd/rank/keymap.go
Normal file
62
cmd/rank/keymap.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
package rank
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/remogatto/sugarfoam/components/group"
|
||||
"github.com/remogatto/sugarfoam/components/table"
|
||||
"github.com/remogatto/sugarfoam/components/viewport"
|
||||
)
|
||||
|
||||
type keyBindings struct {
|
||||
group *group.Model
|
||||
|
||||
quit, enter key.Binding
|
||||
}
|
||||
|
||||
func (k *keyBindings) ShortHelp() []key.Binding {
|
||||
keys := make([]key.Binding, 0)
|
||||
|
||||
current := k.group.Current()
|
||||
|
||||
switch item := current.(type) {
|
||||
case *table.Model:
|
||||
keys = append(
|
||||
keys,
|
||||
item.KeyMap.LineUp,
|
||||
item.KeyMap.LineDown,
|
||||
)
|
||||
|
||||
case *viewport.Model:
|
||||
keys = append(
|
||||
keys,
|
||||
item.KeyMap.Up,
|
||||
item.KeyMap.Down,
|
||||
)
|
||||
}
|
||||
|
||||
keys = append(
|
||||
keys,
|
||||
k.group.KeyMap.FocusNext,
|
||||
k.group.KeyMap.FocusPrev,
|
||||
k.quit,
|
||||
)
|
||||
|
||||
return keys
|
||||
}
|
||||
|
||||
func (k keyBindings) FullHelp() [][]key.Binding {
|
||||
return [][]key.Binding{
|
||||
{
|
||||
k.quit,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newBindings(g *group.Model) *keyBindings {
|
||||
return &keyBindings{
|
||||
group: g,
|
||||
quit: key.NewBinding(
|
||||
key.WithKeys("esc"), key.WithHelp("esc", "quit app"),
|
||||
),
|
||||
}
|
||||
}
|
19
cmd/rank/message.go
Normal file
19
cmd/rank/message.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package rank
|
||||
|
||||
import "git.andreafazzi.eu/andrea/probo/pkg/store/file"
|
||||
|
||||
type storeLoadedMsg struct {
|
||||
store *file.SessionFileStore
|
||||
}
|
||||
|
||||
type resultMsg struct {
|
||||
result []any
|
||||
}
|
||||
|
||||
type errorMsg struct {
|
||||
error error
|
||||
}
|
||||
|
||||
type scriptExecutedMsg struct {
|
||||
result string
|
||||
}
|
448
cmd/rank/rank.go
Normal file
448
cmd/rank/rank.go
Normal file
|
@ -0,0 +1,448 @@
|
|||
package rank
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"text/template"
|
||||
|
||||
"git.andreafazzi.eu/andrea/probo/pkg/models"
|
||||
"git.andreafazzi.eu/andrea/probo/pkg/store/file"
|
||||
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/spinner"
|
||||
btTable "github.com/charmbracelet/bubbles/table"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/glamour"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/d5/tengo/v2"
|
||||
"github.com/d5/tengo/v2/stdlib"
|
||||
foam "github.com/remogatto/sugarfoam"
|
||||
"github.com/remogatto/sugarfoam/components/group"
|
||||
"github.com/remogatto/sugarfoam/components/header"
|
||||
"github.com/remogatto/sugarfoam/components/help"
|
||||
"github.com/remogatto/sugarfoam/components/statusbar"
|
||||
"github.com/remogatto/sugarfoam/components/table"
|
||||
"github.com/remogatto/sugarfoam/components/viewport"
|
||||
"github.com/remogatto/sugarfoam/layout"
|
||||
"github.com/remogatto/sugarfoam/layout/tiled"
|
||||
)
|
||||
|
||||
var responseTmpl = `{{range $answer := .Answers}}
|
||||
{{$answer.Quiz|toMarkdown $.Width}}
|
||||
R: {{$answer|toLipgloss}}
|
||||
{{end}}
|
||||
`
|
||||
|
||||
type ParticipantScore struct {
|
||||
Participant *models.Participant `json:"participant"`
|
||||
Response *models.Response `json:"response"`
|
||||
Score int `json:"score"`
|
||||
}
|
||||
|
||||
type Rank struct {
|
||||
Scores []*ParticipantScore `json:"scores"`
|
||||
}
|
||||
|
||||
type RankModel struct {
|
||||
// UI
|
||||
viewport *viewport.Model
|
||||
table *table.Model
|
||||
group *group.Model
|
||||
help *help.Model
|
||||
statusBar *statusbar.Model
|
||||
spinner spinner.Model
|
||||
|
||||
// Layout
|
||||
document *layout.Layout
|
||||
|
||||
// Key bindings
|
||||
bindings *keyBindings
|
||||
|
||||
// json
|
||||
InputJson string
|
||||
Result string
|
||||
|
||||
// session
|
||||
rank *Rank
|
||||
|
||||
// response
|
||||
responseTmpl *template.Template
|
||||
|
||||
// filter file
|
||||
scriptFilePath string
|
||||
|
||||
// markdown
|
||||
mdRenderer *glamour.TermRenderer
|
||||
|
||||
state int
|
||||
}
|
||||
|
||||
func New(path string, stdin string) *RankModel {
|
||||
viewport := viewport.New()
|
||||
|
||||
table := table.New(table.WithRelWidths(10, 20, 30, 30, 10))
|
||||
table.Model.SetColumns([]btTable.Column{
|
||||
{Title: "Pos", Width: 5},
|
||||
{Title: "Token", Width: 10},
|
||||
{Title: "Lastname", Width: 40},
|
||||
{Title: "Firstname", Width: 40},
|
||||
{Title: "Score", Width: 5},
|
||||
})
|
||||
|
||||
group := group.New(
|
||||
group.WithItems(table, viewport),
|
||||
group.WithLayout(
|
||||
layout.New(
|
||||
layout.WithStyles(&layout.Styles{Container: lipgloss.NewStyle().Padding(1, 1)}),
|
||||
layout.WithItem(tiled.New(table, viewport)),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
bindings := newBindings(group)
|
||||
statusBar := statusbar.New(bindings)
|
||||
|
||||
s := spinner.New(
|
||||
spinner.WithStyle(
|
||||
lipgloss.NewStyle().Foreground(lipgloss.Color("265"))),
|
||||
)
|
||||
s.Spinner = spinner.Dot
|
||||
|
||||
header := header.New(
|
||||
header.WithContent(
|
||||
lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Border(lipgloss.NormalBorder(), false, false, true, false).
|
||||
Render("😎 Rank 😎"),
|
||||
),
|
||||
)
|
||||
|
||||
help := help.New(
|
||||
bindings,
|
||||
help.WithStyles(&foam.Styles{NoBorder: lipgloss.NewStyle().Padding(1, 1)}))
|
||||
|
||||
document := layout.New(
|
||||
layout.WithStyles(&layout.Styles{Container: lipgloss.NewStyle().Margin(1)}),
|
||||
layout.WithItem(header),
|
||||
layout.WithItem(group),
|
||||
layout.WithItem(help),
|
||||
layout.WithItem(statusBar),
|
||||
)
|
||||
|
||||
renderer, err := glamour.NewTermRenderer(
|
||||
glamour.WithStandardStyle("dracula"),
|
||||
glamour.WithWordWrap(80),
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
tmpl, err := template.New("response").
|
||||
Funcs(template.FuncMap{
|
||||
"toMarkdown": func(width int, quiz *models.Quiz) string {
|
||||
md, err := models.QuizToMarkdown(quiz)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
result, err := renderer.Render(md)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return result
|
||||
},
|
||||
"toLipgloss": func(answer *models.ParticipantAnswer) string {
|
||||
color := "#ff0000"
|
||||
if answer.Correct {
|
||||
color = "#00ff00"
|
||||
}
|
||||
return lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color(color)).Render(answer.Answer.Text)
|
||||
},
|
||||
}).
|
||||
Parse(responseTmpl)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &RankModel{
|
||||
table: table,
|
||||
viewport: viewport,
|
||||
group: group,
|
||||
statusBar: statusBar,
|
||||
spinner: s,
|
||||
document: document,
|
||||
responseTmpl: tmpl,
|
||||
bindings: bindings,
|
||||
help: help,
|
||||
scriptFilePath: path,
|
||||
InputJson: stdin,
|
||||
mdRenderer: renderer,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *RankModel) Init() tea.Cmd {
|
||||
var cmds []tea.Cmd
|
||||
|
||||
cmds = append(cmds, m.group.Init(), m.executeScript(), m.spinner.Tick)
|
||||
|
||||
m.group.Focus()
|
||||
|
||||
return tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m *RankModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmds []tea.Cmd
|
||||
|
||||
switch msg := msg.(type) {
|
||||
|
||||
case tea.WindowSizeMsg:
|
||||
m.handleWindowSize(msg)
|
||||
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, m.bindings.quit):
|
||||
cmds = append(cmds, tea.Quit)
|
||||
}
|
||||
|
||||
case scriptExecutedMsg:
|
||||
m.handleScriptExecuted(msg)
|
||||
|
||||
case errorMsg:
|
||||
m.handleError(msg)
|
||||
m.state = ErrorState
|
||||
}
|
||||
|
||||
cmds = m.handleState(msg, cmds)
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m *RankModel) View() string {
|
||||
return m.document.View()
|
||||
}
|
||||
|
||||
func (m *RankModel) executeScript() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
if m.scriptFilePath == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
rankJson, err := json.Marshal(Rank{Scores: make([]*ParticipantScore, 0)})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
script, err := os.ReadFile(m.scriptFilePath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
s := tengo.NewScript(script)
|
||||
|
||||
s.SetImports(stdlib.GetModuleMap("fmt", "json", "rand", "times"))
|
||||
_ = s.Add("input", m.InputJson)
|
||||
_ = s.Add("output", string(rankJson))
|
||||
|
||||
c, err := s.Compile()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := c.Run(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return scriptExecutedMsg{fmt.Sprintf("%s", c.Get("output"))}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (m *RankModel) showErrorOnStatusBar(err error) {
|
||||
m.statusBar.SetContent(
|
||||
stateFormats[ErrorState][0],
|
||||
fmt.Sprintf(stateFormats[ErrorState][1], err),
|
||||
stateFormats[ErrorState][2],
|
||||
)
|
||||
}
|
||||
|
||||
func (m *RankModel) updateTableContent() {
|
||||
rows := make([]btTable.Row, 0)
|
||||
|
||||
for i, score := range m.rank.Scores {
|
||||
rows = append(rows, btTable.Row{
|
||||
strconv.Itoa(i + 1),
|
||||
score.Participant.Token,
|
||||
score.Participant.Lastname,
|
||||
score.Participant.Firstname,
|
||||
strconv.Itoa(score.Score),
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
m.table.SetRows(rows)
|
||||
}
|
||||
|
||||
func (m *RankModel) updateViewportContent() {
|
||||
if len(m.table.Rows()) == 0 {
|
||||
panic(errors.New("No scores available!"))
|
||||
}
|
||||
|
||||
currentPos := m.table.SelectedRow()[0]
|
||||
|
||||
pos, err := strconv.Atoi(currentPos)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
currentResponse := m.rank.Scores[pos-1]
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
||||
var response struct {
|
||||
*models.Response
|
||||
Width int
|
||||
} = struct {
|
||||
*models.Response
|
||||
Width int
|
||||
}{currentResponse.Response, m.viewport.GetWidth()}
|
||||
|
||||
err = m.responseTmpl.Execute(&buf, response)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
m.viewport.SetContent(buf.String())
|
||||
}
|
||||
|
||||
func (m *RankModel) handleWindowSize(msg tea.WindowSizeMsg) {
|
||||
m.group.SetSize(msg.Width, msg.Height)
|
||||
m.document.SetSize(msg.Width, msg.Height)
|
||||
m.mdRenderer = m.createMDRenderer()
|
||||
}
|
||||
|
||||
func (m *RankModel) handleError(msg tea.Msg) {
|
||||
err := msg.(errorMsg)
|
||||
|
||||
m.statusBar.SetContent(
|
||||
stateFormats[ErrorState][0],
|
||||
fmt.Sprintf(stateFormats[ErrorState][1], err.error),
|
||||
stateFormats[ErrorState][2],
|
||||
)
|
||||
}
|
||||
|
||||
func (m *RankModel) handleScriptExecuted(msg tea.Msg) {
|
||||
rank := new(Rank)
|
||||
jsonData := []byte(msg.(scriptExecutedMsg).result)
|
||||
|
||||
err := json.Unmarshal(jsonData, &rank)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
slices.SortFunc(rank.Scores,
|
||||
func(a, b *ParticipantScore) int {
|
||||
return cmp.Compare(b.Score, a.Score)
|
||||
})
|
||||
|
||||
m.rank = rank
|
||||
|
||||
m.updateTableContent()
|
||||
m.updateViewportContent()
|
||||
|
||||
m.state = BrowseState
|
||||
}
|
||||
|
||||
func (m *RankModel) handleState(msg tea.Msg, cmds []tea.Cmd) []tea.Cmd {
|
||||
_, cmd := m.group.Update(msg)
|
||||
|
||||
if m.state == ExecutingScriptState {
|
||||
return m.updateSpinner(msg, cmd, cmds)
|
||||
}
|
||||
|
||||
if m.state == BrowseState {
|
||||
m.updateViewportContent()
|
||||
}
|
||||
|
||||
if m.state != ErrorState {
|
||||
m.statusBar.SetContent(
|
||||
stateFormats[BrowseState][0],
|
||||
fmt.Sprintf(stateFormats[BrowseState][1], len(m.rank.Scores)),
|
||||
stateFormats[BrowseState][2],
|
||||
)
|
||||
}
|
||||
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
return cmds
|
||||
}
|
||||
|
||||
func (m *RankModel) updateSpinner(msg tea.Msg, cmd tea.Cmd, cmds []tea.Cmd) []tea.Cmd {
|
||||
m.spinner, cmd = m.spinner.Update(msg)
|
||||
|
||||
m.statusBar.SetContent(fmt.Sprintf(stateFormats[m.state][0], m.spinner.View()), stateFormats[m.state][1], stateFormats[m.state][2])
|
||||
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
return cmds
|
||||
}
|
||||
|
||||
func (m *RankModel) loadStore() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
sStore, err := file.NewDefaultSessionFileStore()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return storeLoadedMsg{sStore}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *RankModel) createMDRenderer() *glamour.TermRenderer {
|
||||
renderer, err := glamour.NewTermRenderer(
|
||||
glamour.WithStandardStyle("dracula"),
|
||||
glamour.WithWordWrap(m.viewport.GetWidth()),
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return renderer
|
||||
}
|
||||
|
||||
// func toColoredJson(data []any) (string, error) {
|
||||
// result, err := json.MarshalIndent(data, "", " ")
|
||||
// if err != nil {
|
||||
// return "", err
|
||||
// }
|
||||
|
||||
// coloredBytes := make([]byte, 0)
|
||||
// buffer := bytes.NewBuffer(coloredBytes)
|
||||
|
||||
// err = quick.Highlight(buffer, string(result), "json", "terminal16m", "dracula")
|
||||
// if err != nil {
|
||||
// panic(err)
|
||||
// }
|
||||
|
||||
// return sanitize(buffer.String()), nil
|
||||
// }
|
||||
|
||||
// func sanitize(text string) string {
|
||||
// // FIXME: The use of a standard '-' character causes rendering
|
||||
// // issues within the viewport. Further investigation is
|
||||
// // required to resolve this problem.
|
||||
// return strings.Replace(text, "-", "–", -1)
|
||||
// }
|
||||
|
||||
// func desanitize(text string) string {
|
||||
// // FIXME: The use of a standard '-' character causes rendering
|
||||
// // issues within the viewport. Further investigation is
|
||||
// // required to resolve this problem.
|
||||
// return strings.Replace(text, "–", "-", -1)
|
||||
// }
|
7
cmd/rank/state.go
Normal file
7
cmd/rank/state.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package rank
|
||||
|
||||
const (
|
||||
ExecutingScriptState = iota
|
||||
BrowseState
|
||||
ErrorState
|
||||
)
|
17
cmd/root.go
17
cmd/root.go
|
@ -25,22 +25,25 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.andreafazzi.eu/andrea/probo/cmd/util"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var cfgFile string
|
||||
var (
|
||||
cfgFile string
|
||||
)
|
||||
|
||||
// rootCmd represents the base command when called without any subcommands
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "probo",
|
||||
Short: "A brief description of your application",
|
||||
Long: `A longer description that spans multiple lines and likely contains
|
||||
examples and usage of using your application. For example:
|
||||
Short: "A Quiz Management System",
|
||||
/*Long: `
|
||||
Probo is a CLI/TUI application that allows for the quick
|
||||
creation of quizzes from markdown files and their distribution
|
||||
as web pages.`,*/
|
||||
Long: util.RenderMarkdownTemplates("cli/*.tmpl", "cli/root/*.tmpl"),
|
||||
|
||||
Cobra is a CLI library for Go that empowers applications.
|
||||
This application is a tool to generate the needed files
|
||||
to quickly create a Cobra application.`,
|
||||
// Uncomment the following line if your bare application
|
||||
// has an action associated with it:
|
||||
// Run: func(cmd *cobra.Command, args []string) { },
|
||||
|
|
82
cmd/serve.go
Normal file
82
cmd/serve.go
Normal file
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
Copyright © 2024 NAME HERE <EMAIL ADDRESS>
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.andreafazzi.eu/andrea/probo/cmd/serve"
|
||||
"git.andreafazzi.eu/andrea/probo/pkg/store/file"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// serveCmd represents the serve command
|
||||
var serveCmd = &cobra.Command{
|
||||
Use: "serve",
|
||||
Short: "Launch a web server to adminster exam sessions",
|
||||
Long: `A longer description that spans multiple lines and likely contains examples
|
||||
and usage of using your command. For example:
|
||||
|
||||
Cobra is a CLI library for Go that empowers applications.
|
||||
This application is a tool to generate the needed files
|
||||
to quickly create a Cobra application.`,
|
||||
Run: runServer,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(serveCmd)
|
||||
}
|
||||
|
||||
func runServer(cmd *cobra.Command, args []string) {
|
||||
sStore, err := file.NewDefaultSessionFileStore()
|
||||
if err != nil {
|
||||
log.Fatal("Session store loading", "err", err)
|
||||
}
|
||||
|
||||
rStore, err := file.NewDefaultResponseFileStore()
|
||||
if err != nil {
|
||||
log.Fatal("Session store loading", "err", err)
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
loginController := serve.NewController(sStore, rStore).
|
||||
WithTemplates(
|
||||
"templates/login/layout-login.html.tmpl",
|
||||
"templates/login/login.html.tmpl",
|
||||
).
|
||||
WithHandlerFunc(serve.LoginHandler)
|
||||
|
||||
sessionsController := serve.NewController(sStore, rStore).
|
||||
WithTemplates(
|
||||
"templates/sessions/layout-sessions.html.tmpl",
|
||||
"templates/sessions/sessions.html.tmpl",
|
||||
).
|
||||
WithHandlerFunc(serve.SessionsHandler)
|
||||
|
||||
examController := serve.NewController(sStore, rStore).
|
||||
WithTemplates(
|
||||
"templates/exam/layout-exam.html.tmpl",
|
||||
"templates/exam/exam.html.tmpl",
|
||||
).
|
||||
WithHandlerFunc(serve.ExamHandler)
|
||||
|
||||
mux.Handle("GET /login", serve.Recover(loginController))
|
||||
mux.Handle("POST /login", serve.Recover(loginController))
|
||||
|
||||
mux.Handle("GET /sessions", serve.Recover(sessionsController))
|
||||
mux.Handle("GET /sessions/{uuid}/exams/{participantID}", serve.Recover(examController))
|
||||
mux.Handle("POST /sessions/{uuid}/exams/{participantID}", serve.Recover(examController))
|
||||
|
||||
mux.Handle("GET /public/", http.StripPrefix("/public", http.FileServer(http.Dir("public"))))
|
||||
|
||||
log.Info("Listening...")
|
||||
|
||||
err = http.ListenAndServe(":3000", mux)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
}
|
64
cmd/serve/controller.go
Normal file
64
cmd/serve/controller.go
Normal file
|
@ -0,0 +1,64 @@
|
|||
package serve
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
"git.andreafazzi.eu/andrea/probo/pkg/store/file"
|
||||
)
|
||||
|
||||
type Controller struct {
|
||||
handlerFunc http.HandlerFunc
|
||||
|
||||
sStore *file.SessionFileStore
|
||||
rStore *file.ResponseFileStore
|
||||
|
||||
template *template.Template
|
||||
}
|
||||
|
||||
func NewController(sStore *file.SessionFileStore, rStore *file.ResponseFileStore) *Controller {
|
||||
return &Controller{
|
||||
sStore: sStore,
|
||||
rStore: rStore,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) WithHandlerFunc(f func(c *Controller, w http.ResponseWriter, r *http.Request)) *Controller {
|
||||
hf := func(w http.ResponseWriter, r *http.Request) {
|
||||
f(c, w, r)
|
||||
}
|
||||
|
||||
c.handlerFunc = hf
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Controller) WithTemplates(paths ...string) *Controller {
|
||||
tmpl, err := template.ParseFiles(paths...)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
c.template = tmpl
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Controller) ExecuteTemplate(w http.ResponseWriter, data any) error {
|
||||
var buf bytes.Buffer
|
||||
|
||||
err := c.template.Execute(&buf, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=UTF-8")
|
||||
|
||||
buf.WriteTo(w)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
c.handlerFunc.ServeHTTP(w, r)
|
||||
}
|
81
cmd/serve/exam.go
Normal file
81
cmd/serve/exam.go
Normal file
|
@ -0,0 +1,81 @@
|
|||
package serve
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"git.andreafazzi.eu/andrea/probo/pkg/models"
|
||||
"github.com/charmbracelet/log"
|
||||
)
|
||||
|
||||
var ExamHandler = func(c *Controller, w http.ResponseWriter, r *http.Request) {
|
||||
_, err := ValidateJwtCookie(r)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
participantID := r.PathValue("participantID")
|
||||
|
||||
session, err := c.sStore.Read(r.PathValue("uuid"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
exam, ok := session.Exams[participantID]
|
||||
if !ok {
|
||||
panic(errors.New("Exam not found in the store!"))
|
||||
}
|
||||
|
||||
examWithSession := struct {
|
||||
*models.Exam
|
||||
Session *models.Session
|
||||
}{exam, session}
|
||||
|
||||
switch r.Method {
|
||||
|
||||
case http.MethodGet:
|
||||
log.Info("Sending exam to", "participant", exam.Participant, "exam", exam)
|
||||
|
||||
err = c.ExecuteTemplate(w, examWithSession)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
case http.MethodPost:
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
answers := make([]*models.ParticipantAnswer, 0)
|
||||
|
||||
participant := session.Participants[participantID]
|
||||
|
||||
for quizID, values := range r.Form {
|
||||
correct := false
|
||||
|
||||
quiz := session.Quizzes[quizID]
|
||||
|
||||
for _, answerID := range values {
|
||||
if quiz.Correct.ID == answerID {
|
||||
correct = true
|
||||
}
|
||||
answers = append(answers, &models.ParticipantAnswer{Quiz: quiz, Answer: session.Answers[answerID], Correct: correct})
|
||||
}
|
||||
}
|
||||
|
||||
response, err := c.rStore.Create(
|
||||
&models.Response{
|
||||
SessionTitle: session.Title,
|
||||
Participant: participant,
|
||||
Answers: answers,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
log.Info("Saving response", "response", response)
|
||||
|
||||
}
|
||||
|
||||
}
|
36
cmd/serve/jwt.go
Normal file
36
cmd/serve/jwt.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package serve
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt"
|
||||
)
|
||||
|
||||
const jwtExpiresAt = time.Hour
|
||||
|
||||
type Claims struct {
|
||||
Token string `json:"token"`
|
||||
jwt.StandardClaims
|
||||
}
|
||||
|
||||
var (
|
||||
jwtKey = []byte("my-secret")
|
||||
)
|
||||
|
||||
func ValidateJwtCookie(r *http.Request) (*jwt.Token, error) {
|
||||
cookie, err := r.Cookie("Authorize")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token, err := jwt.Parse(cookie.Value, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return jwtKey, nil
|
||||
})
|
||||
|
||||
return token, err
|
||||
}
|
76
cmd/serve/login.go
Normal file
76
cmd/serve/login.go
Normal file
|
@ -0,0 +1,76 @@
|
|||
package serve
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.andreafazzi.eu/andrea/probo/pkg/models"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/golang-jwt/jwt"
|
||||
)
|
||||
|
||||
var LoginHandler = func(c *Controller, w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodPost {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
pToken := r.FormValue("participantToken")
|
||||
if pToken == "" {
|
||||
panic(errors.New("Token not found parsing the request!"))
|
||||
}
|
||||
|
||||
log.Info("Received", "participantToken", pToken)
|
||||
|
||||
var loggedParticipant *models.Participant
|
||||
|
||||
done := false
|
||||
for _, session := range c.sStore.ReadAll() {
|
||||
if done {
|
||||
break
|
||||
}
|
||||
for _, exam := range session.Exams {
|
||||
if pToken == exam.Participant.Token {
|
||||
loggedParticipant = exam.Participant
|
||||
done = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("Participant logged in as", "participant", loggedParticipant)
|
||||
|
||||
if loggedParticipant == nil {
|
||||
panic(errors.New("Participant not found!"))
|
||||
}
|
||||
|
||||
claims := &Claims{
|
||||
Token: pToken,
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
ExpiresAt: time.Now().Add(jwtExpiresAt).Unix(),
|
||||
},
|
||||
}
|
||||
|
||||
tokenString, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(jwtKey))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "Authorize",
|
||||
Value: tokenString,
|
||||
Expires: time.Now().Add(jwtExpiresAt),
|
||||
})
|
||||
|
||||
log.Info("Released", "jwt", tokenString)
|
||||
log.Info("Redirect to", "url", "/sessions")
|
||||
|
||||
http.Redirect(w, r, "/sessions", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
err := c.ExecuteTemplate(w, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
22
cmd/serve/recover.go
Normal file
22
cmd/serve/recover.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
package serve
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
)
|
||||
|
||||
func Recover(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
err := recover()
|
||||
if err != nil {
|
||||
log.Error("Recovering from", "err", err)
|
||||
http.Error(w, err.(error).Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
}()
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
39
cmd/serve/sessions.go
Normal file
39
cmd/serve/sessions.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package serve
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.andreafazzi.eu/andrea/probo/pkg/models"
|
||||
"github.com/golang-jwt/jwt"
|
||||
)
|
||||
|
||||
var SessionsHandler = func(c *Controller, w http.ResponseWriter, r *http.Request) {
|
||||
token, err := ValidateJwtCookie(r)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
claims := token.Claims.(jwt.MapClaims)
|
||||
|
||||
var participantSessions []struct {
|
||||
ParticipantID string
|
||||
*models.Session
|
||||
}
|
||||
|
||||
for _, session := range c.sStore.ReadAll() {
|
||||
for _, exam := range session.Exams {
|
||||
if exam.Participant.Token == claims["token"] {
|
||||
s := struct {
|
||||
ParticipantID string
|
||||
*models.Session
|
||||
}{exam.Participant.ID, session}
|
||||
participantSessions = append(participantSessions, s)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = c.ExecuteTemplate(w, participantSessions)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
60
cmd/session.go
Normal file
60
cmd/session.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
Copyright © 2024 NAME HERE <EMAIL ADDRESS>
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.andreafazzi.eu/andrea/probo/cmd/session"
|
||||
"git.andreafazzi.eu/andrea/probo/cmd/util"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/muesli/termenv"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// sessionCmd represents the session command
|
||||
var sessionCmd = &cobra.Command{
|
||||
Use: "session",
|
||||
Short: "Create a new session or update a given one",
|
||||
Long: "Create a new session or update a given one.",
|
||||
Run: updateSession,
|
||||
}
|
||||
|
||||
func init() {
|
||||
updateCmd.AddCommand(sessionCmd)
|
||||
|
||||
sessionCmd.Flags().StringP("script", "s", "", "Execute a tengo script to initiate a session")
|
||||
}
|
||||
|
||||
func updateSession(cmd *cobra.Command, args []string) {
|
||||
f := util.LogToFile()
|
||||
if f != nil {
|
||||
defer f.Close()
|
||||
}
|
||||
|
||||
path, err := cmd.Flags().GetString("script")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
lipgloss.SetColorProfile(termenv.TrueColor)
|
||||
|
||||
model, err := tea.NewProgram(
|
||||
session.New(path, util.ReadStdin()),
|
||||
tea.WithOutput(os.Stderr),
|
||||
).Run()
|
||||
if err != nil {
|
||||
fmt.Println("Error running program:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
result := model.(*session.SessionModel)
|
||||
|
||||
if result.Result != "" {
|
||||
fmt.Fprintf(os.Stdout, result.Result)
|
||||
}
|
||||
|
||||
}
|
9
cmd/session/format.go
Normal file
9
cmd/session/format.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package session
|
||||
|
||||
var (
|
||||
stateFormats = map[int][]string{
|
||||
BrowseState: []string{"BROWSE 📖", "Total sessions in the store: %d, Exams in the current session: %d", "STORE 🟢"},
|
||||
LoadingStoreState: []string{"LOAD %s", "Loading the store...", "STORE 🔴"},
|
||||
ErrorState: []string{"ERROR 📖", "%v", "STORE 🟢"},
|
||||
}
|
||||
)
|
69
cmd/session/keymap.go
Normal file
69
cmd/session/keymap.go
Normal file
|
@ -0,0 +1,69 @@
|
|||
package session
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/remogatto/sugarfoam/components/form"
|
||||
"github.com/remogatto/sugarfoam/components/group"
|
||||
"github.com/remogatto/sugarfoam/components/table"
|
||||
"github.com/remogatto/sugarfoam/components/viewport"
|
||||
)
|
||||
|
||||
type keyBindings struct {
|
||||
group *group.Model
|
||||
|
||||
quit, enter key.Binding
|
||||
}
|
||||
|
||||
func (k *keyBindings) ShortHelp() []key.Binding {
|
||||
keys := make([]key.Binding, 0)
|
||||
|
||||
current := k.group.Current()
|
||||
|
||||
switch item := current.(type) {
|
||||
case *table.Model:
|
||||
keys = append(
|
||||
keys,
|
||||
item.KeyMap.LineUp,
|
||||
item.KeyMap.LineDown,
|
||||
)
|
||||
|
||||
case *viewport.Model:
|
||||
keys = append(
|
||||
keys,
|
||||
item.KeyMap.Up,
|
||||
item.KeyMap.Down,
|
||||
)
|
||||
case *form.Model:
|
||||
keys = append(
|
||||
keys,
|
||||
item.KeyBinds()...,
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
keys = append(
|
||||
keys,
|
||||
k.group.KeyMap.FocusNext,
|
||||
k.group.KeyMap.FocusPrev,
|
||||
k.quit,
|
||||
)
|
||||
|
||||
return keys
|
||||
}
|
||||
|
||||
func (k keyBindings) FullHelp() [][]key.Binding {
|
||||
return [][]key.Binding{
|
||||
{
|
||||
k.quit,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newBindings(g *group.Model) *keyBindings {
|
||||
return &keyBindings{
|
||||
group: g,
|
||||
quit: key.NewBinding(
|
||||
key.WithKeys("esc"), key.WithHelp("esc", "quit app"),
|
||||
),
|
||||
}
|
||||
}
|
19
cmd/session/message.go
Normal file
19
cmd/session/message.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package session
|
||||
|
||||
import "git.andreafazzi.eu/andrea/probo/pkg/store/file"
|
||||
|
||||
type storeLoadedMsg struct {
|
||||
store *file.SessionFileStore
|
||||
}
|
||||
|
||||
type resultMsg struct {
|
||||
result []any
|
||||
}
|
||||
|
||||
type errorMsg struct {
|
||||
error error
|
||||
}
|
||||
|
||||
type scriptExecutedMsg struct {
|
||||
result string
|
||||
}
|
452
cmd/session/session.go
Normal file
452
cmd/session/session.go
Normal file
|
@ -0,0 +1,452 @@
|
|||
package session
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.andreafazzi.eu/andrea/probo/pkg/models"
|
||||
"git.andreafazzi.eu/andrea/probo/pkg/store/file"
|
||||
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/spinner"
|
||||
btTable "github.com/charmbracelet/bubbles/table"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/glamour"
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/d5/tengo/v2"
|
||||
"github.com/d5/tengo/v2/stdlib"
|
||||
foam "github.com/remogatto/sugarfoam"
|
||||
"github.com/remogatto/sugarfoam/components/form"
|
||||
"github.com/remogatto/sugarfoam/components/group"
|
||||
"github.com/remogatto/sugarfoam/components/header"
|
||||
"github.com/remogatto/sugarfoam/components/help"
|
||||
"github.com/remogatto/sugarfoam/components/statusbar"
|
||||
"github.com/remogatto/sugarfoam/components/table"
|
||||
"github.com/remogatto/sugarfoam/components/viewport"
|
||||
"github.com/remogatto/sugarfoam/layout"
|
||||
"github.com/remogatto/sugarfoam/layout/tiled"
|
||||
)
|
||||
|
||||
var mockSession = `
|
||||
{"participants":[{"Attributes":{"class":"Math 101A"},"created_at":"2024-04-03T09:22:11.201142494+02:00","firstname":"THOMAS","id":"1024758d-0dba-4bec-aaab-339e87a0649b","lastname":"MANN","token":"427628","updated_at":"2024-05-27T13:48:50.096492274+02:00"},{"Attributes":{"class":"Math 101A"},"created_at":"2024-04-04T10:23:12.203143495+02:00","firstname":"JAMES","id":"f3c5a7bd-6fda-4bce-bbbb-333e88a0650c","lastname":"BROWN","token":"428629","updated_at":"2024-05-27T13:48:50.075089555+02:00"},{"Attributes":{"class":"Math 101A"},"created_at":"2024-04-05T11:24:13.204144496+02:00","firstname":"LUCY","id":"g4d6c8cd-7eda-4cce-cccc-444e89a0661d","lastname":"DOE","token":"429630","updated_at":"2024-05-27T13:48:50.079134348+02:00"}], "quizzes": [{"CorrectPos":0,"answers":[{"created_at":"2024-05-27T13:55:26.448679335+02:00","id":"6771592e-4e2c-45f1-b14f-91f943a5616b","text":"3 + 1 = 1 + 3","updated_at":"2024-05-27T13:55:26.448679428+02:00"},{"created_at":"2024-05-27T13:55:26.448681412+02:00","id":"bad7db2a-f5be-4f22-879a-fd783bb1c0a9","text":"3 - 1 = 1 - 3","updated_at":"2024-05-27T13:55:26.448681456+02:00"},{"created_at":"2024-05-27T13:55:26.448683251+02:00","id":"8ae29e51-65a7-4618-8074-58ee90c4fb83","text":"3 / 2 = 2 / 3","updated_at":"2024-05-27T13:55:26.448683289+02:00"},{"created_at":"2024-05-27T13:55:26.448685048+02:00","id":"7d2a9491-338d-4274-9e62-3d2641788777","text":"-3 + 1 = -1 + 3","updated_at":"2024-05-27T13:55:26.448685085+02:00"}],"correct":{"created_at":"2024-05-27T13:55:26.448679335+02:00","id":"6771592e-4e2c-45f1-b14f-91f943a5616b","text":"3 + 1 = 1 + 3","updated_at":"2024-05-27T13:55:26.448679428+02:00"},"created_at":"2024-05-27T13:49:24.170080837+02:00","hash":"","id":"6a21d3fa-d63c-4acf-9238-551692ed74c4","question":{"created_at":"2024-05-27T13:55:26.448673273+02:00","id":"bf19c1f1-6896-4ca2-b7dc-1657adcce4cd","text":"In #mathematics, for the commutative property of addition, we have\nthat:","updated_at":"2024-05-27T13:55:26.448673633+02:00"},"tags":["#mathematics"],"type":0,"updated_at":"2024-05-27T13:55:26.448701931+02:00"},{"CorrectPos":0,"answers":[{"created_at":"2024-05-27T13:55:26.449222989+02:00","id":"37f741c2-12de-4463-b6c0-42b8b5249e7a","text":"3 * (4 + 5) = 3 * 4 + 3 * 5","updated_at":"2024-05-27T13:55:26.449223079+02:00"},{"created_at":"2024-05-27T13:55:26.44922686+02:00","id":"c60b1b81-5e16-4401-831c-d9fffba1d1eb","text":"6 * (2 - 3) = 6 * 2 + 6 * 3","updated_at":"2024-05-27T13:55:26.449226924+02:00"},{"created_at":"2024-05-27T13:55:26.449230299+02:00","id":"fb0ba30c-1e4e-41a1-9630-7f18aa24a6db","text":"7 * (8 + 2) = 7 * 8 - 7 * 2","updated_at":"2024-05-27T13:55:26.449230351+02:00"},{"created_at":"2024-05-27T13:55:26.449233993+02:00","id":"0cfd6fbb-7aac-4d35-aa17-3e62f5297c68","text":"2 * (1 + 2) = (- 2) * 1 - 4","updated_at":"2024-05-27T13:55:26.449234055+02:00"}],"correct":{"created_at":"2024-05-27T13:55:26.449222989+02:00","id":"37f741c2-12de-4463-b6c0-42b8b5249e7a","text":"3 * (4 + 5) = 3 * 4 + 3 * 5","updated_at":"2024-05-27T13:55:26.449223079+02:00"},"created_at":"2024-05-27T13:49:24.173550821+02:00","hash":"","id":"40aa188a-46c6-4f26-9a1e-4d5a028ba367","question":{"created_at":"2024-05-27T13:55:26.449216807+02:00","id":"723acb0a-5c60-4062-8615-fd3494220b4d","text":"In #mathematics, the distributive property allows us to multiply a sum\nor difference by a single number. Which statement correctly applies\nthis property?","updated_at":"2024-05-27T13:55:26.449216918+02:00"},"tags":["#mathematics"],"type":0,"updated_at":"2024-05-27T13:55:26.449281612+02:00"},{"CorrectPos":0,"answers":[{"created_at":"2024-05-27T13:55:26.449440615+02:00","id":"11be2252-45f0-44c1-a9f0-19dcdcd45b68","text":"1","updated_at":"2024-05-27T13:55:26.449440713+02:00"},{"created_at":"2024-05-27T13:55:26.449452222+02:00","id":"971fbf3b-27b1-4b67-9636-ac681056ce62","text":"0","updated_at":"2024-05-27T13:55:26.449452291+02:00"},{"created_at":"2024-05-27T13:55:26.449455664+02:00","id":"d35104b1-37f9-4312-aa21-a470650cb4dc","text":"2","updated_at":"2024-05-27T13:55:26.44945573+02:00"},{"created_at":"2024-05-27T13:55:26.449459322+02:00","id":"220077d2-1994-40ed-8d28-f39d2ebb8dcf","text":"3","updated_at":"2024-05-27T13:55:26.449459397+02:00"}],"correct":{"created_at":"2024-05-27T13:55:26.449440615+02:00","id":"11be2252-45f0-44c1-a9f0-19dcdcd45b68","text":"1","updated_at":"2024-05-27T13:55:26.449440713+02:00"},"created_at":"2024-05-27T13:49:24.175865713+02:00","hash":"","id":"f9fab318-3373-4729-a80f-b681460824e3","question":{"created_at":"2024-05-27T13:55:26.449432971+02:00","id":"af7d973a-a83c-4a18-ac36-92ab6442ef07","text":"In #mathematics, certain numbers have special properties. Identify the\nidentity element for multiplication among the options given:","updated_at":"2024-05-27T13:55:26.449433136+02:00"},"tags":["#mathematics"],"type":0,"updated_at":"2024-05-27T13:55:26.449476666+02:00"}]}`
|
||||
|
||||
type SessionModel struct {
|
||||
// UI
|
||||
form *form.Model
|
||||
viewport *viewport.Model
|
||||
table *table.Model
|
||||
group *group.Model
|
||||
help *help.Model
|
||||
statusBar *statusbar.Model
|
||||
spinner spinner.Model
|
||||
|
||||
// Layout
|
||||
document *layout.Layout
|
||||
|
||||
// Key bindings
|
||||
bindings *keyBindings
|
||||
|
||||
// store
|
||||
store *file.SessionFileStore
|
||||
lenStore int
|
||||
result []any
|
||||
|
||||
// json
|
||||
InputJson string
|
||||
Result string
|
||||
|
||||
// session
|
||||
session *models.Session
|
||||
|
||||
// markdown
|
||||
mdRenderer *glamour.TermRenderer
|
||||
|
||||
// filter file
|
||||
scriptFilePath string
|
||||
|
||||
state int
|
||||
}
|
||||
|
||||
func New(path string, stdin string) *SessionModel {
|
||||
form := form.New(
|
||||
form.WithGroups(huh.NewGroup(
|
||||
huh.NewInput().
|
||||
Key("sessionTitle").
|
||||
Title("Session title").
|
||||
Description("Enter the title of the session").
|
||||
Validate(func(str string) error {
|
||||
if str == "" {
|
||||
return errors.New("You must set a session name!")
|
||||
}
|
||||
return nil
|
||||
|
||||
}),
|
||||
)))
|
||||
|
||||
formBinding := huh.NewDefaultKeyMap()
|
||||
formBinding.Input.Next = key.NewBinding(key.WithKeys("down"), key.WithHelp("down", "next"))
|
||||
formBinding.Input.Prev = key.NewBinding(key.WithKeys("up"), key.WithHelp("up", "prev"))
|
||||
formBinding.Confirm.Next = key.NewBinding(key.WithKeys("down"), key.WithHelp("down", "next"))
|
||||
formBinding.Confirm.Prev = key.NewBinding(key.WithKeys("up"), key.WithHelp("up", "prev"))
|
||||
|
||||
form.WithShowHelp(false).WithTheme(huh.ThemeDracula()).WithKeyMap(formBinding)
|
||||
|
||||
viewport := viewport.New()
|
||||
|
||||
table := table.New(table.WithRelWidths(20, 10, 25, 25, 20))
|
||||
table.Model.SetColumns([]btTable.Column{
|
||||
{Title: "UUID", Width: 20},
|
||||
{Title: "Token", Width: 20},
|
||||
{Title: "Lastname", Width: 20},
|
||||
{Title: "Firstname", Width: 20},
|
||||
{Title: "Class", Width: 20},
|
||||
})
|
||||
|
||||
group := group.New(
|
||||
group.WithItems(form, table, viewport),
|
||||
group.WithLayout(
|
||||
layout.New(
|
||||
layout.WithStyles(&layout.Styles{Container: lipgloss.NewStyle().Padding(1, 1)}),
|
||||
layout.WithItem(form),
|
||||
layout.WithItem(tiled.New(table, viewport)),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
bindings := newBindings(group)
|
||||
statusBar := statusbar.New(bindings)
|
||||
|
||||
s := spinner.New(
|
||||
spinner.WithStyle(
|
||||
lipgloss.NewStyle().Foreground(lipgloss.Color("265"))),
|
||||
)
|
||||
s.Spinner = spinner.Dot
|
||||
|
||||
header := header.New(
|
||||
header.WithContent(
|
||||
lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Border(lipgloss.NormalBorder(), false, false, true, false).
|
||||
Render("✨ Create session ✨"),
|
||||
),
|
||||
)
|
||||
|
||||
help := help.New(
|
||||
bindings,
|
||||
help.WithStyles(&foam.Styles{NoBorder: lipgloss.NewStyle().Padding(1, 1)}))
|
||||
|
||||
document := layout.New(
|
||||
layout.WithStyles(&layout.Styles{Container: lipgloss.NewStyle().Margin(1)}),
|
||||
layout.WithItem(header),
|
||||
layout.WithItem(group),
|
||||
layout.WithItem(help),
|
||||
layout.WithItem(statusBar),
|
||||
)
|
||||
|
||||
renderer, err := glamour.NewTermRenderer(
|
||||
glamour.WithStandardStyle("dracula"),
|
||||
glamour.WithWordWrap(80),
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &SessionModel{
|
||||
form: form,
|
||||
table: table,
|
||||
viewport: viewport,
|
||||
group: group,
|
||||
statusBar: statusBar,
|
||||
spinner: s,
|
||||
document: document,
|
||||
mdRenderer: renderer,
|
||||
bindings: bindings,
|
||||
help: help,
|
||||
scriptFilePath: path,
|
||||
InputJson: stdin,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *SessionModel) Init() tea.Cmd {
|
||||
var cmds []tea.Cmd
|
||||
|
||||
cmds = append(cmds, m.group.Init(), m.loadStore(), m.spinner.Tick)
|
||||
|
||||
m.group.Focus()
|
||||
|
||||
return tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m *SessionModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmds []tea.Cmd
|
||||
|
||||
switch msg := msg.(type) {
|
||||
|
||||
case tea.WindowSizeMsg:
|
||||
m.handleWindowSize(msg)
|
||||
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, m.bindings.quit):
|
||||
cmds = append(cmds, tea.Quit)
|
||||
}
|
||||
|
||||
case storeLoadedMsg:
|
||||
cmds = append(cmds, m.handleStoreLoaded(msg))
|
||||
|
||||
case scriptExecutedMsg:
|
||||
m.handleScriptExecuted(msg)
|
||||
|
||||
case errorMsg:
|
||||
m.handleError(msg)
|
||||
m.state = ErrorState
|
||||
}
|
||||
|
||||
cmds = m.handleState(msg, cmds)
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m *SessionModel) View() string {
|
||||
return m.document.View()
|
||||
}
|
||||
|
||||
func (m *SessionModel) marshalJSON() (string, error) {
|
||||
session, err := m.createSession()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
resultJson, err := json.Marshal(session)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(resultJson), nil
|
||||
}
|
||||
|
||||
func (m *SessionModel) executeScript() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
if m.scriptFilePath == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
sessionJson, err := json.Marshal(models.Session{Exams: map[string]*models.Exam{}})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
script, err := os.ReadFile(m.scriptFilePath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
s := tengo.NewScript(script)
|
||||
|
||||
s.SetImports(stdlib.GetModuleMap("fmt", "json", "rand", "times"))
|
||||
_ = s.Add("input", m.InputJson)
|
||||
_ = s.Add("output", string(sessionJson))
|
||||
|
||||
c, err := s.Compile()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := c.Run(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return scriptExecutedMsg{fmt.Sprintf("%s", c.Get("output"))}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (m *SessionModel) createSession() (*models.Session, error) {
|
||||
m.session.Title = m.form.GetString("sessionTitle")
|
||||
|
||||
session, err := m.store.Storer.Create(m.session)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return session, err
|
||||
}
|
||||
|
||||
func (m *SessionModel) showErrorOnStatusBar(err error) {
|
||||
m.statusBar.SetContent(
|
||||
stateFormats[ErrorState][0],
|
||||
fmt.Sprintf(stateFormats[ErrorState][1], err),
|
||||
stateFormats[ErrorState][2],
|
||||
)
|
||||
}
|
||||
|
||||
func (m *SessionModel) updateTableContent(session *models.Session) {
|
||||
rows := make([]btTable.Row, 0)
|
||||
|
||||
for _, exam := range session.Exams {
|
||||
rows = append(rows, btTable.Row{
|
||||
exam.Participant.ID,
|
||||
exam.Participant.Token,
|
||||
exam.Participant.Lastname,
|
||||
exam.Participant.Firstname,
|
||||
exam.Participant.Attributes.Get("class"),
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
m.table.SetRows(rows)
|
||||
}
|
||||
|
||||
func (m *SessionModel) updateViewportContent(session *models.Session) {
|
||||
if len(m.table.Rows()) == 0 {
|
||||
panic(errors.New("Session have not exams"))
|
||||
}
|
||||
|
||||
currentUUID := m.table.SelectedRow()[0]
|
||||
currentExam := session.Exams[currentUUID]
|
||||
|
||||
if currentExam == nil {
|
||||
panic("Current token is not associate to any exam!")
|
||||
}
|
||||
|
||||
// md, err := currentExam.ToMarkdown()
|
||||
// if err != nil {
|
||||
// m.showErrorOnStatusBar(err)
|
||||
// }
|
||||
|
||||
// data, err := currentExam.Marshal()
|
||||
// if err != nil {
|
||||
// panic(err)
|
||||
// }
|
||||
|
||||
// result, err := m.mdRenderer.Render(md)
|
||||
// if err != nil {
|
||||
// m.showErrorOnStatusBar(err)
|
||||
// }
|
||||
|
||||
// m.viewport.SetContent(result)
|
||||
// m.viewport.SetContent(string(data))
|
||||
m.viewport.SetContent(mockSession)
|
||||
}
|
||||
|
||||
func (m *SessionModel) createMDRenderer() *glamour.TermRenderer {
|
||||
renderer, err := glamour.NewTermRenderer(
|
||||
glamour.WithStandardStyle("dracula"),
|
||||
glamour.WithWordWrap(m.viewport.GetWidth()),
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return renderer
|
||||
}
|
||||
|
||||
func (m *SessionModel) handleWindowSize(msg tea.WindowSizeMsg) {
|
||||
m.group.SetSize(msg.Width, msg.Height)
|
||||
m.document.SetSize(msg.Width, msg.Height)
|
||||
m.mdRenderer = m.createMDRenderer()
|
||||
}
|
||||
|
||||
func (m *SessionModel) handleError(msg tea.Msg) {
|
||||
err := msg.(errorMsg)
|
||||
|
||||
m.statusBar.SetContent(
|
||||
stateFormats[ErrorState][0],
|
||||
fmt.Sprintf(stateFormats[ErrorState][1], err.error),
|
||||
stateFormats[ErrorState][2],
|
||||
)
|
||||
}
|
||||
|
||||
func (m *SessionModel) handleScriptExecuted(msg tea.Msg) {
|
||||
session := new(models.Session)
|
||||
jsonData := []byte(msg.(scriptExecutedMsg).result)
|
||||
|
||||
err := json.Unmarshal(jsonData, &session)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
m.session = session
|
||||
|
||||
m.updateTableContent(session)
|
||||
m.updateViewportContent(session)
|
||||
|
||||
m.state = BrowseState
|
||||
}
|
||||
|
||||
func (m *SessionModel) handleStoreLoaded(msg tea.Msg) tea.Cmd {
|
||||
storeMsg := msg.(storeLoadedMsg)
|
||||
|
||||
m.store = storeMsg.store
|
||||
m.lenStore = len(m.store.ReadAll())
|
||||
|
||||
return m.executeScript()
|
||||
|
||||
}
|
||||
|
||||
func (m *SessionModel) handleState(msg tea.Msg, cmds []tea.Cmd) []tea.Cmd {
|
||||
_, cmd := m.group.Update(msg)
|
||||
|
||||
if m.state == LoadingStoreState {
|
||||
return m.updateSpinner(msg, cmd, cmds)
|
||||
}
|
||||
|
||||
if m.form.State == huh.StateCompleted {
|
||||
var err error
|
||||
|
||||
m.Result, err = m.marshalJSON()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
cmds = append(cmds, tea.Quit)
|
||||
}
|
||||
|
||||
if len(m.form.Errors()) > 0 {
|
||||
m.state = ErrorState
|
||||
for _, err := range m.form.Errors() {
|
||||
m.showErrorOnStatusBar(err)
|
||||
}
|
||||
} else {
|
||||
m.state = BrowseState
|
||||
}
|
||||
|
||||
if m.state == BrowseState {
|
||||
m.updateViewportContent(m.session)
|
||||
}
|
||||
|
||||
if m.state != ErrorState {
|
||||
m.statusBar.SetContent(
|
||||
stateFormats[BrowseState][0],
|
||||
fmt.Sprintf(stateFormats[BrowseState][1], m.lenStore, len(m.session.Exams)),
|
||||
stateFormats[BrowseState][2],
|
||||
)
|
||||
}
|
||||
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
return cmds
|
||||
}
|
||||
|
||||
func (m *SessionModel) updateSpinner(msg tea.Msg, cmd tea.Cmd, cmds []tea.Cmd) []tea.Cmd {
|
||||
m.spinner, cmd = m.spinner.Update(msg)
|
||||
|
||||
m.statusBar.SetContent(fmt.Sprintf(stateFormats[m.state][0], m.spinner.View()), stateFormats[m.state][1], stateFormats[m.state][2])
|
||||
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
return cmds
|
||||
}
|
||||
|
||||
func (m *SessionModel) loadStore() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
sStore, err := file.NewDefaultSessionFileStore()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return storeLoadedMsg{sStore}
|
||||
}
|
||||
}
|
7
cmd/session/state.go
Normal file
7
cmd/session/state.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package session
|
||||
|
||||
const (
|
||||
LoadingStoreState = iota
|
||||
BrowseState
|
||||
ErrorState
|
||||
)
|
29
cmd/update.go
Normal file
29
cmd/update.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
Copyright © 2024 NAME HERE <EMAIL ADDRESS>
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// updateCmd represents the update command
|
||||
var updateCmd = &cobra.Command{
|
||||
Use: "update",
|
||||
Short: "Create or update an entity",
|
||||
Long: `A longer description that spans multiple lines and likely contains examples
|
||||
and usage of using your command. For example:
|
||||
|
||||
Cobra is a CLI library for Go that empowers applications.
|
||||
This application is a tool to generate the needed files
|
||||
to quickly create a Cobra application.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Println("update called")
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(updateCmd)
|
||||
}
|
74
cmd/util/util.go
Normal file
74
cmd/util/util.go
Normal file
|
@ -0,0 +1,74 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.andreafazzi.eu/andrea/probo/embed"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/glamour"
|
||||
)
|
||||
|
||||
func RenderMarkdownTemplates(paths ...string) string {
|
||||
tmpl, err := template.ParseFS(embed.CLI, paths...)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
||||
err = tmpl.Execute(&buf, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
result, err := glamour.Render(buf.String(), "dark")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func LogToFile() *os.File {
|
||||
if len(os.Getenv("DEBUG")) > 0 {
|
||||
f, err := tea.LogToFile("probo-debug.log", "[DEBUG]")
|
||||
if err != nil {
|
||||
fmt.Println("fatal:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return f
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ReadStdin() string {
|
||||
var b strings.Builder
|
||||
|
||||
stat, err := os.Stdin.Stat()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if stat.Mode()&os.ModeNamedPipe != 0 || stat.Size() != 0 {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
for {
|
||||
r, _, err := reader.ReadRune()
|
||||
if err != nil && err == io.EOF {
|
||||
break
|
||||
}
|
||||
_, err = b.WriteRune(r)
|
||||
if err != nil {
|
||||
fmt.Println("Error getting input:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return strings.TrimSpace(b.String())
|
||||
}
|
33
embed/cli/filter/description.tmpl
Normal file
33
embed/cli/filter/description.tmpl
Normal file
|
@ -0,0 +1,33 @@
|
|||
{{define "description"}}
|
||||
# Filters
|
||||
|
||||
**Filters can be used to make selections among stores.**
|
||||
|
||||
Filters allow you to narrow down selections across various stores. By
|
||||
using filters, you can select participants, quizzes, and
|
||||
responses. The command triggers a Text User Interface (TUI) that runs
|
||||
a `jq` filter, displaying the outcome in real-time. After you're content
|
||||
with the filtered JSON, pressing ⏎ will present the result on
|
||||
stdout, enabling you to further process it by piping it forward.
|
||||
|
||||
## Examples
|
||||
|
||||
1. Apply a filter to participants:
|
||||
|
||||
```
|
||||
probo filter participants
|
||||
```
|
||||
|
||||
2. Apply a filter to quizzes using the `jq` filter in `tags.jq` file:
|
||||
|
||||
```
|
||||
probo filter quizzes -i data/filters/tags.jq
|
||||
```
|
||||
|
||||
3. Apply a filter to participants, then pass the output through
|
||||
another filter. The final result is saved in a JSON file:
|
||||
|
||||
```
|
||||
probo filter participants | probo filter quizzes > data/json/selection.json
|
||||
```
|
||||
{{end}}
|
27
embed/cli/init/description.tmpl
Normal file
27
embed/cli/init/description.tmpl
Normal file
|
@ -0,0 +1,27 @@
|
|||
{{define "description"}}
|
||||
# Init
|
||||
|
||||
**Init initializes the current working directory.**
|
||||
|
||||
The `init` command creates, within the current directory, a file and
|
||||
folder structure prepared for immediate use of `probo`. The filesystem
|
||||
structure is as follows:
|
||||
```
|
||||
|
||||
├── filters
|
||||
├── json
|
||||
├── participants
|
||||
├── quizzes
|
||||
├── responses
|
||||
├── sessions
|
||||
└── templates
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
1. Initialize the current directory:
|
||||
|
||||
```
|
||||
probo init
|
||||
```
|
||||
{{end}}
|
2
embed/cli/layout.tmpl
Normal file
2
embed/cli/layout.tmpl
Normal file
|
@ -0,0 +1,2 @@
|
|||
{{template "logo" .}}
|
||||
{{template "description" .}}
|
10
embed/cli/logo.tmpl
Normal file
10
embed/cli/logo.tmpl
Normal file
|
@ -0,0 +1,10 @@
|
|||
{{define "logo"}}
|
||||
```
|
||||
____ _
|
||||
| _ \ _ __ ___ | |__ ___
|
||||
| |_) | '__/ _ \| '_ \ / _ \
|
||||
| __/| | | (_) | |_) | (_) |
|
||||
|_| |_| \___/|_.__/ \___/____
|
||||
|_____|
|
||||
```
|
||||
{{end}}
|
21
embed/cli/rank/description.tmpl
Normal file
21
embed/cli/rank/description.tmpl
Normal file
|
@ -0,0 +1,21 @@
|
|||
{{define "description"}}
|
||||
# Rank
|
||||
|
||||
**The `rank` command generates a ranking of results based on participant answers.**
|
||||
|
||||
With the `rank` command, it is possible to generate a ranking of
|
||||
results based on the answers provided by participants. The command
|
||||
accepts on standard input either a JSON ready for display or the
|
||||
result of a filter applied to the answers that needs to be processed
|
||||
through a `tengo` script to conform to the JSON schema.
|
||||
|
||||
## Example
|
||||
|
||||
Filtra le risposte date dai partecipanti al test "Math Test" e produce
|
||||
una classifica dei punteggi assegnando +1 punto per ogni risposta
|
||||
esatta fornita.
|
||||
|
||||
```
|
||||
probo filter responses -i data/filters/math_test.jq | probo rank -s data/scripts/score.tengo
|
||||
```
|
||||
{{end}}
|
50
embed/cli/root/description.tmpl
Normal file
50
embed/cli/root/description.tmpl
Normal file
|
@ -0,0 +1,50 @@
|
|||
{{define "description"}}
|
||||
# Probo
|
||||
|
||||
`probo` is a quiz management system designed for command line
|
||||
enthusiasts. `probo` aims to highlight themes such as privacy,
|
||||
interoperability, accessibility, decentralization, and quick usage
|
||||
speed.
|
||||
|
||||
`probo` organizes information in plain text files and folders so that
|
||||
data can be easily revised through version control systems.
|
||||
|
||||
`probo` contains a built-in web server that allows quizzes to be
|
||||
administered to participants and their answers received.
|
||||
|
||||
`probo` is a self-contained application that can be distributed through
|
||||
a simple executable containing all necessary assets for its operation.
|
||||
|
||||
# Quickstart
|
||||
|
||||
1. Initialize the working directory. The command will build a scaffold
|
||||
containing example participants, quizzes, and filters
|
||||
|
||||
```
|
||||
probo init
|
||||
```
|
||||
|
||||
2. Filter participants and quizzes and create an exam
|
||||
session.
|
||||
|
||||
```
|
||||
probo filter participant -i data/filters/9th_grade.jq \
|
||||
| probo filter quizzes -i data/filters/difficult_easy.jq \
|
||||
| probo create session --name="My First Session" > data/sessions/my_session.json
|
||||
```
|
||||
|
||||
3. Run the web server to allow participants to respond.
|
||||
|
||||
```
|
||||
probo serve
|
||||
```
|
||||
|
||||
4. Share the *qrcode* generated from the previous step with the
|
||||
participants and wait for them to finish responding.
|
||||
|
||||
5. Explore the results.
|
||||
|
||||
```
|
||||
probo filter responses -f '.' | probo rank
|
||||
```
|
||||
{{end}}
|
71
embed/embed.go
Normal file
71
embed/embed.go
Normal file
|
@ -0,0 +1,71 @@
|
|||
package embed
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed cli/*
|
||||
CLI embed.FS
|
||||
|
||||
//go:embed templates/*
|
||||
Templates embed.FS
|
||||
|
||||
//go:embed public/*
|
||||
Public embed.FS
|
||||
|
||||
//go:embed data/*
|
||||
Data embed.FS
|
||||
)
|
||||
|
||||
func CopyToWorkingDirectory(data embed.FS) error {
|
||||
currentDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := fs.WalkDir(data, ".", func(path string, info fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
log.Info(err)
|
||||
return err
|
||||
}
|
||||
fullDstPath := filepath.Join(currentDir, path)
|
||||
|
||||
if info.IsDir() {
|
||||
log.Info("Creating folder", "path", path)
|
||||
if err := os.MkdirAll(fullDstPath, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
srcFile, err := data.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
dstFile, err := os.Create(fullDstPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dstFile.Close()
|
||||
|
||||
log.Info("Copying file", "path", path)
|
||||
_, err = io.Copy(dstFile, srcFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
40
embed/public/css/login.css
Normal file
40
embed/public/css/login.css
Normal file
|
@ -0,0 +1,40 @@
|
|||
.container {
|
||||
display: grid;
|
||||
width: 300px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.container label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.container input[type="text"],
|
||||
.container input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.container button {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.container button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
23
embed/templates/exam/exam.html.tmpl
Normal file
23
embed/templates/exam/exam.html.tmpl
Normal file
|
@ -0,0 +1,23 @@
|
|||
{{ define "content" }}
|
||||
<div class="container">
|
||||
{{range $quiz := .Quizzes }}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="card my-2">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{$quiz.Question}}</h5>
|
||||
<h6 class="card-subtitle mb-2 text-body-secondary">Una sola scelta possibile</h6>
|
||||
{{range $answer := $quiz.Answers}}
|
||||
<input type="radio"
|
||||
id="{{$quiz.ID}}_{{$answer.ID}}" name="{{$quiz.ID}}"
|
||||
value="{{$answer.ID}}">
|
||||
<label
|
||||
for="{{$quiz.ID}}_{{$answer.ID}}">{{$answer.Text}}</label><br>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
37
embed/templates/exam/layout-exam.html.tmpl
Normal file
37
embed/templates/exam/layout-exam.html.tmpl
Normal file
|
@ -0,0 +1,37 @@
|
|||
<!doctype html>
|
||||
<html lang="en" data-bs-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Test di {{.Participant}}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="navbar bg-body-tertiary">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="#">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="24" fill="currentColor" class="bi bi-book d-inline-block align-text-top" viewBox="0 0 16 16">
|
||||
<path d="M1 2.828c.885-.37 2.154-.769 3.388-.893 1.33-.134 2.458.063 3.112.752v9.746c-.935-.53-2.12-.603-3.213-.493-1.18.12-2.37.461-3.287.811zm7.5-.141c.654-.689 1.782-.886 3.112-.752 1.234.124 2.503.523 3.388.893v9.923c-.918-.35-2.107-.692-3.287-.81-1.094-.111-2.278-.039-3.213.492zM8 1.783C7.015.936 5.587.81 4.287.94c-1.514.153-3.042.672-3.994 1.105A.5.5 0 0 0 0 2.5v11a.5.5 0 0 0 .707.455c.882-.4 2.303-.881 3.68-1.02 1.409-.142 2.59.087 3.223.877a.5.5 0 0 0 .78 0c.633-.79 1.814-1.019 3.222-.877 1.378.139 2.8.62 3.681 1.02A.5.5 0 0 0 16 13.5v-11a.5.5 0 0 0-.293-.455c-.952-.433-2.48-.952-3.994-1.105C10.413.809 8.985.936 8 1.783"/>
|
||||
</svg>
|
||||
Probo_
|
||||
</a>
|
||||
|
||||
<span class="navbar-text">
|
||||
{{.Session.Title}}
|
||||
</span>
|
||||
|
||||
<div class="text-end">
|
||||
<input type="submit" value="Salva" class="btn btn-primary me-2" form="submit-exam-form"/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<form action="/sessions/{{.Session.ID}}/exams/{{.Participant.ID}}" method="POST" id="submit-exam-form"/>
|
||||
|
||||
{{template "content" .}}
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
29
embed/templates/login/layout-login.html.tmpl
Normal file
29
embed/templates/login/layout-login.html.tmpl
Normal file
|
@ -0,0 +1,29 @@
|
|||
<!doctype html>
|
||||
<html lang="en" data-bs-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Probo login</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||
</head>
|
||||
<body>
|
||||
{{template "content" .}}
|
||||
|
||||
<footer class="d-flex flex-wrap justify-content-between align-items-center py-3 my-4 border-top fixed-bottom">
|
||||
<div class="col-md-4 d-flex align-items-center">
|
||||
<a href="/" class="mb-3 me-2 mb-md-0 text-body-secondary text-decoration-none lh-1">
|
||||
<svg class="bi" width="30" height="24"><use xlink:href="#bootstrap"></use></svg>
|
||||
</a>
|
||||
<span class="mb-3 mb-md-0 text-body-secondary">© 2024 Andrea Fazzi</span>
|
||||
</div>
|
||||
|
||||
<ul class="nav col-md-4 justify-content-end list-unstyled d-flex">
|
||||
<li class="ms-3"><a class="text-body-secondary" href="#"><svg class="bi" width="24" height="24"><use xlink:href="#twitter"></use></svg></a></li>
|
||||
<li class="ms-3"><a class="text-body-secondary" href="#"><svg class="bi" width="24" height="24"><use xlink:href="#instagram"></use></svg></a></li>
|
||||
<li class="ms-3"><a class="text-body-secondary" href="#"><svg class="bi" width="24" height="24"><use xlink:href="#facebook"></use></svg></a></li>
|
||||
</ul>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
25
embed/templates/login/login.html.tmpl
Normal file
25
embed/templates/login/login.html.tmpl
Normal file
|
@ -0,0 +1,25 @@
|
|||
{{ define "content" }}
|
||||
<div class="container col-xl-10 col-xxl-8 px-4 py-5">
|
||||
<div class="row align-items-center g-lg-5 py-5">
|
||||
<div class="col-lg-7 text-center text-lg-start">
|
||||
<h1 class="display-4 fw-bold lh-1 text-body-emphasis mb-3">🎓 Probo_</h1>
|
||||
<p class="col-lg-10 fs-4">
|
||||
Una piattaforma per la creazione, la gestione e la
|
||||
somministrazione di test <em>the hacker way</em>. Inserisci il
|
||||
tuo <strong>token</strong> personale per iniziare.
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-10 mx-auto col-lg-5">
|
||||
<form method="post" action="/login" class="p-4 p-md-5 border rounded-3 bg-body-tertiary">
|
||||
<div class="form-floating mb-3">
|
||||
<input type="password" class="form-control" name="participantToken" id="participantToken" placeholder="Token">
|
||||
<label for="participantToken">Inserisci il token...</label>
|
||||
</div>
|
||||
<button class="w-100 btn btn-lg btn-primary" type="submit">Inizia</button>
|
||||
<hr class="my-4">
|
||||
<small class="text-body-secondary">Fai un bel respiro e non aver paura: non è un test su di te ma su quello che sai!</small>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
55
embed/templates/sessions/layout-sessions.html.tmpl
Normal file
55
embed/templates/sessions/layout-sessions.html.tmpl
Normal file
|
@ -0,0 +1,55 @@
|
|||
<!doctype html>
|
||||
<html lang="en" data-bs-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Probo login</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="mb-4 p-3 text-bg-dark border-bottom">
|
||||
<div class="container">
|
||||
|
||||
<div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start">
|
||||
<a href="/" class="d-flex align-items-center mb-2 mb-lg-0 text-white text-decoration-none">
|
||||
<span class="fs-4 mx-2">Probo_</span>
|
||||
</a>
|
||||
|
||||
<ul class="nav col-12 col-lg-auto me-lg-auto mb-2 justify-content-center mb-md-0">
|
||||
<li><a href="#" class="nav-link px-2 text-secondary">I tuoi test</a></li>
|
||||
</ul>
|
||||
|
||||
<form class="col-12 col-lg-auto mb-3 mb-lg-0 me-lg-3" role="search">
|
||||
<input type="search" class="form-control form-control-dark text-bg-dark" placeholder="Cerca..." aria-label="Search">
|
||||
</form>
|
||||
|
||||
<div class="text-end">
|
||||
<button type="button" class="btn btn-outline-light me-2">Cerca...</button>
|
||||
<button type="button" class="btn btn-warning">Esci</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{{template "content" .}}
|
||||
|
||||
<footer class="d-flex flex-wrap justify-content-between align-items-center py-3 my-4 border-top">
|
||||
<div class="col-md-4 d-flex align-items-center">
|
||||
<a href="/" class="mb-3 me-2 mb-md-0 text-body-secondary text-decoration-none lh-1">
|
||||
<svg class="bi" width="30" height="24"><use xlink:href="#bootstrap"></use></svg>
|
||||
</a>
|
||||
<span class="mb-3 mb-md-0 text-body-secondary">© 2024 Andrea Fazzi</span>
|
||||
</div>
|
||||
|
||||
<ul class="nav col-md-4 justify-content-end list-unstyled d-flex">
|
||||
<li class="ms-3"><a class="text-body-secondary" href="#"><svg class="bi" width="24" height="24"><use xlink:href="#twitter"></use></svg></a></li>
|
||||
<li class="ms-3"><a class="text-body-secondary" href="#"><svg class="bi" width="24" height="24"><use xlink:href="#instagram"></use></svg></a></li>
|
||||
<li class="ms-3"><a class="text-body-secondary" href="#"><svg class="bi" width="24" height="24"><use xlink:href="#facebook"></use></svg></a></li>
|
||||
</ul>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
18
embed/templates/sessions/sessions.html.tmpl
Normal file
18
embed/templates/sessions/sessions.html.tmpl
Normal file
|
@ -0,0 +1,18 @@
|
|||
{{ define "content" }}
|
||||
<div class="container">
|
||||
{{range $session := . }}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="card my-2">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{$session.Title}}</h5>
|
||||
<h6 class="card-subtitle mb-2 text-body-secondary">{{$session.CreatedAt}}</h6>
|
||||
<p class="card-text">{{$session.Description}}</p>
|
||||
<a href="/sessions/{{$session.ID}}/exams/{{$session.ParticipantID}}" class="btn btn-primary">Inizia!</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
60
go.mod
60
go.mod
|
@ -1,61 +1,75 @@
|
|||
module git.andreafazzi.eu/andrea/probo
|
||||
|
||||
go 1.21.6
|
||||
go 1.22.2
|
||||
|
||||
require (
|
||||
github.com/charmbracelet/bubbles v0.18.0
|
||||
github.com/alecthomas/chroma v0.10.0
|
||||
github.com/charmbracelet/bubbles v0.18.1-0.20240309002305-b9e62cbfe181
|
||||
github.com/charmbracelet/bubbletea v0.25.0
|
||||
github.com/charmbracelet/lipgloss v0.9.1
|
||||
github.com/charmbracelet/glamour v0.6.0
|
||||
github.com/charmbracelet/huh v0.3.0
|
||||
github.com/charmbracelet/lipgloss v0.10.0
|
||||
github.com/charmbracelet/log v0.4.0
|
||||
github.com/charmbracelet/x/ansi v0.1.2
|
||||
github.com/d5/tengo/v2 v2.17.0
|
||||
github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/itchyny/gojq v0.12.14
|
||||
github.com/lmittmann/tint v1.0.4
|
||||
github.com/muesli/termenv v0.15.2
|
||||
github.com/remogatto/prettytest v0.0.0-20200211072524-6d385e11dcb8
|
||||
github.com/urfave/cli v1.22.14
|
||||
github.com/urfave/cli/v2 v2.27.1
|
||||
github.com/remogatto/sugarfoam v0.0.0-20240418083243-766dd70853af
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/spf13/viper v1.18.2
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gorm.io/gorm v1.25.6
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/catppuccin/go v0.2.0 // indirect
|
||||
github.com/containerd/console v1.0.4 // indirect
|
||||
github.com/dlclark/regexp2 v1.4.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/itchyny/timefmt-go v0.1.5 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-isatty v0.0.18 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.26 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/reflow v0.3.0 // indirect
|
||||
github.com/muesli/termenv v0.15.2 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.6 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rogpeppe/go-internal v1.9.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/cobra v1.8.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/viper v1.18.2 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||
github.com/yuin/goldmark v1.5.2 // indirect
|
||||
github.com/yuin/goldmark-emoji v1.0.1 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/sync v0.5.0 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
golang.org/x/term v0.6.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect
|
||||
golang.org/x/net v0.19.0 // indirect
|
||||
golang.org/x/sync v0.6.0 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
golang.org/x/term v0.18.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
|
|
145
go.sum
145
go.sum
|
@ -1,82 +1,126 @@
|
|||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
||||
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
|
||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
|
||||
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
|
||||
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0=
|
||||
github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA=
|
||||
github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
|
||||
github.com/charmbracelet/bubbles v0.18.1-0.20240309002305-b9e62cbfe181 h1:ntdtXC9+kcgQYvqa6nyLZLniCEUOhWQknLlz38JpDpM=
|
||||
github.com/charmbracelet/bubbles v0.18.1-0.20240309002305-b9e62cbfe181/go.mod h1:Zlzkn8WOd6QS7RC1BXAY1iw1VLq+xT70UZ1vkEtnrvo=
|
||||
github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM=
|
||||
github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg=
|
||||
github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg=
|
||||
github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I=
|
||||
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
|
||||
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
|
||||
github.com/charmbracelet/glamour v0.6.0 h1:wi8fse3Y7nfcabbbDuwolqTqMQPMnVPeZhDM273bISc=
|
||||
github.com/charmbracelet/glamour v0.6.0/go.mod h1:taqWV4swIMMbWALc0m7AfE9JkPSU8om2538k9ITBxOc=
|
||||
github.com/charmbracelet/huh v0.3.0 h1:CxPplWkgW2yUTDDG0Z4S5HH8SJOosWHd4LxCvi0XsKE=
|
||||
github.com/charmbracelet/huh v0.3.0/go.mod h1:fujUdKX8tC45CCSaRQdw789O6uaCRwx8l2NDyKfC4jA=
|
||||
github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s=
|
||||
github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE=
|
||||
github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
|
||||
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
|
||||
github.com/charmbracelet/x/ansi v0.1.2 h1:6+LR39uG8DE6zAmbu023YlqjJHkYXDF1z36ZwzO4xZY=
|
||||
github.com/charmbracelet/x/ansi v0.1.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
|
||||
github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=
|
||||
github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/d5/tengo/v2 v2.17.0 h1:BWUN9NoJzw48jZKiYDXDIF3QrIVZRm1uV1gTzeZ2lqM=
|
||||
github.com/d5/tengo/v2 v2.17.0/go.mod h1:XRGjEs5I9jYIKTxly6HCF8oiiilk5E/RYXOZ5b0DZC8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
|
||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
|
||||
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a h1:RYfmiM0zluBJOiPDJseKLEN4BapJ42uSi9SZBQ2YyiA=
|
||||
github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/itchyny/gojq v0.12.14 h1:6k8vVtsrhQSYgSGg827AD+PVVaB1NLXEdX+dda2oZCc=
|
||||
github.com/itchyny/gojq v0.12.14/go.mod h1:y1G7oO7XkcR1LPZO59KyoCRy08T3j9vDYRV0GgYSS+s=
|
||||
github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE=
|
||||
github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lmittmann/tint v1.0.4 h1:LeYihpJ9hyGvE0w+K2okPTGUdVLfng1+nDNVR4vWISc=
|
||||
github.com/lmittmann/tint v1.0.4/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
|
||||
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg=
|
||||
github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM=
|
||||
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
|
||||
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
|
||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||
github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc=
|
||||
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
|
||||
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/remogatto/prettytest v0.0.0-20200211072524-6d385e11dcb8 h1:nRDwTcxV9B3elxMt+1xINX0bwaPdpouqp5fbynexY8U=
|
||||
github.com/remogatto/prettytest v0.0.0-20200211072524-6d385e11dcb8/go.mod h1:jOEnp79oIHy5cvQSHeLcgVJk1GHOOHJHQWps/d1N5Yo=
|
||||
github.com/remogatto/sugarfoam v0.0.0-20240418083243-766dd70853af h1:rSEwVRdJxMq4RK2kI1LEVhM5J3yg3pcvlRYy1vjn7mQ=
|
||||
github.com/remogatto/sugarfoam v0.0.0-20240418083243-766dd70853af/go.mod h1:WeyW6WPrlPDwa48kDIytaLxXKyRjOwLp4BEd2tEGY1Y=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg=
|
||||
github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f h1:MvTmaQdww/z0Q4wrYjDSCcZ78NoftLQyHBSLW/Cx79Y=
|
||||
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
|
@ -93,41 +137,46 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
|||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk=
|
||||
github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA=
|
||||
github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho=
|
||||
github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.5.2 h1:ALmeCk/px5FSm1MAcFBAsVKZjDuMVj8Tm7FFIlMJnqU=
|
||||
github.com/yuin/goldmark v1.5.2/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os=
|
||||
github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o=
|
||||
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
|
||||
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
|
@ -136,5 +185,3 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
|||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/gorm v1.25.6 h1:V92+vVda1wEISSOMtodHVRcUIOPYa2tgQtyF+DfFx+A=
|
||||
gorm.io/gorm v1.25.6/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
|
|
9
go.work
9
go.work
|
@ -1,3 +1,8 @@
|
|||
go 1.21.6
|
||||
go 1.22.2
|
||||
|
||||
use .
|
||||
toolchain go1.22.3
|
||||
|
||||
use (
|
||||
.
|
||||
../sugarfoam
|
||||
)
|
||||
|
|
108
go.work.sum
108
go.work.sum
|
@ -1,34 +1,122 @@
|
|||
cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic=
|
||||
cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI=
|
||||
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
||||
cloud.google.com/go/firestore v1.14.0/go.mod h1:96MVaHLsEhbvkBEdZgfN+AS/GIkco1LRpH9Xp9YZfzQ=
|
||||
cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8=
|
||||
cloud.google.com/go/longrunning v0.5.4/go.mod h1:zqNVncI0BOP8ST6XQD1+VcvuShMmq7+xFSzOL++V0dI=
|
||||
cloud.google.com/go/storage v1.35.1/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8=
|
||||
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
||||
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
||||
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||
github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
|
||||
github.com/aymanbagabas/go-osc52 v1.0.3 h1:DTwqENW7X9arYimJrPeGZcV0ln14sGMt3pHZspWD+Mg=
|
||||
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/disintegration/gift v1.2.1/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI=
|
||||
github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec/go.mod h1:K0KBFIr1gWu/C1Gp10nFAcAE4hsB7JxE6OgLijrJ8Sk=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
|
||||
github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
|
||||
github.com/googleapis/google-cloud-go-testing v0.0.0-20210719221736-1c9a4c676720/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
||||
github.com/hashicorp/consul/api v1.25.1/go.mod h1:iiLVwR/htV7mas/sy0O+XSuEnrdBUUydemjxcUrAt4g=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/nats-io/nats.go v1.31.0/go.mod h1:di3Bm5MLsoB4Bx61CBTsxuarI36WbhAwOm8QrW39+i8=
|
||||
github.com/nats-io/nkeys v0.4.6/go.mod h1:4DxZNzenSVd1cYQoAa8948QY3QDjrHfcfVADymtkpts=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
|
||||
github.com/remogatto/imgcat v0.0.0-20240318115229-ee6a34ad38fe/go.mod h1:e6G+BhMs87z7k9UKiGmV8tLWguKaNic9zlb3N+yC5Vc=
|
||||
github.com/sagikazarmark/crypt v0.17.0/go.mod h1:SMtHTvdmsZMuY/bpZoqokSoChIrcJ/epOxZN58PbZDg=
|
||||
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f h1:MvTmaQdww/z0Q4wrYjDSCcZ78NoftLQyHBSLW/Cx79Y=
|
||||
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
|
||||
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||
github.com/srwiley/oksvg v0.0.0-20220128195007-1f435e4c2b44/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
|
||||
github.com/srwiley/rasterx v0.0.0-20220128185129-2efea2b9ea41/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.etcd.io/etcd/api/v3 v3.5.10/go.mod h1:TidfmT4Uycad3NM/o25fG3J07odo4GBB9hoxaodFCtI=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.10/go.mod h1:DYivfIviIuQ8+/lCq4vcxuseg2P2XbHygkKwFo9fc8U=
|
||||
go.etcd.io/etcd/client/v2 v2.305.10/go.mod h1:m3CKZi69HzilhVqtPDcjhSGp+kA1OmbNn0qamH80xjA=
|
||||
go.etcd.io/etcd/client/v3 v3.5.10/go.mod h1:RVeBnDz2PUEZqTpgqwAtUd8nAPf5kjyFyND7P1VkOKc=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM=
|
||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
|
||||
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
google.golang.org/api v0.153.0/go.mod h1:3qNJX5eOmhiWYc67jRA/3GsDw97UFb5ivv7Y2PrriAY=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:J7XzRzVy1+IPwWHZUzoD0IccYZIrXILAQpc+Qy9CMhY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc=
|
||||
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
|
|
4
main.go
4
main.go
|
@ -21,7 +21,9 @@ THE SOFTWARE.
|
|||
*/
|
||||
package main
|
||||
|
||||
import "git.andreafazzi.eu/andrea/probo/cmd"
|
||||
import (
|
||||
"git.andreafazzi.eu/andrea/probo/cmd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
|
|
121
misc/logseq/.gitignore
vendored
121
misc/logseq/.gitignore
vendored
|
@ -1,121 +0,0 @@
|
|||
.DS_Store
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
.env.production
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
*~
|
|
@ -1,21 +0,0 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2022 Andrea Fazzi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -1,21 +0,0 @@
|
|||
Copyright (c) 2022 Andrea Fazzi
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -1,4 +0,0 @@
|
|||
# What's that?
|
||||
|
||||
A very basic boilerplate useful to start devoloping a
|
||||
[Logseq](https://logseq.com/) plugin.
|
|
@ -1,10 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-hierarchy" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="gray" fill="gray" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<circle cx="12" cy="5" r="2" />
|
||||
<circle cx="5" cy="19" r="2" />
|
||||
<circle cx="19" cy="19" r="2" />
|
||||
<path d="M6.5 17.5l5.5 -4.5l5.5 4.5" />
|
||||
<line x1="12" y1="7" x2="12" y2="13" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 471 B |
4707
misc/logseq/package-lock.json
generated
4707
misc/logseq/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,29 +0,0 @@
|
|||
{
|
||||
"logseq": {
|
||||
"id": "logseq-probo-plugin",
|
||||
"title": "logseq-probo-plugin",
|
||||
"icon": "./icon.svg"
|
||||
},
|
||||
"name": "logseq-probo-plugin",
|
||||
"version": "1.2.0",
|
||||
"description": "",
|
||||
"main": "dist/index.html",
|
||||
"targets": {
|
||||
"main": false
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "parcel build --no-source-maps src/index.html --public-url ./"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Andrea Fazzi",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@logseq/libs": "^0.0.1-alpha.35",
|
||||
"js-base64": "^3.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"buffer": "^6.0.3",
|
||||
"parcel": "^2.2.0"
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Document</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="index.ts" type="module"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,121 +0,0 @@
|
|||
import "@logseq/libs";
|
||||
import { BlockEntity } from "@logseq/libs/dist/LSPlugin.user";
|
||||
|
||||
const endpoint = 'http://localhost:3000/quizzes';
|
||||
|
||||
const uniqueIdentifier = () =>
|
||||
Math.random()
|
||||
.toString(36)
|
||||
.replace(/[^a-z]+/g, "");
|
||||
|
||||
const sanitizeBlockContent = (text: string) => text.replace(/((?<=::).*|.*::)/g, "").replace(/{.*}/, "").trim()
|
||||
|
||||
async function fetchQuizzes() {
|
||||
const { status: status, content: quizzes } = await fetch(endpoint).then(res => res.json())
|
||||
const ret = quizzes || []
|
||||
|
||||
return ret.map((quiz, i) => {
|
||||
return `${i + 1}. ${quiz.Question.Text}`
|
||||
})
|
||||
}
|
||||
|
||||
const render = (id, slot, status: ("modified" | "saved" | "error")) => {
|
||||
logseq.provideUI({
|
||||
key: `${id}`,
|
||||
slot,
|
||||
reset: true,
|
||||
template: `
|
||||
${status === 'saved' ? '<button data-on-click="createOrUpdateQuiz" class="renderBtn">Save</button><span>saved</span>' : '<button data-on-click="createOrUpdateQuiz" class="renderBtn">Save</button>'}
|
||||
|
||||
`,
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
const main = () => {
|
||||
console.log("logseq-probo-plugin LOADED!");
|
||||
|
||||
logseq.Editor.registerSlashCommand("Get All Quizzes", async () => {
|
||||
const currBlock = await logseq.Editor.getCurrentBlock();
|
||||
|
||||
let blocks = await fetchQuizzes()
|
||||
|
||||
blocks = blocks.map((it: BlockEntity) => ({ content: it }))
|
||||
|
||||
await logseq.Editor.insertBatchBlock(currBlock.uuid, blocks, {
|
||||
sibling: false
|
||||
})
|
||||
});
|
||||
|
||||
logseq.Editor.registerSlashCommand("Create a new Probo quiz", async () => {
|
||||
await logseq.Editor.insertAtEditingCursor(
|
||||
`{{renderer :probo_${uniqueIdentifier()}}}`
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
logseq.App.onMacroRendererSlotted(async ({ slot, payload }) => {
|
||||
const [type] = payload.arguments;
|
||||
|
||||
if (!type.startsWith(":probo")) return
|
||||
|
||||
const id = type.split("_")[1]?.trim();
|
||||
const proboId = `probo_${id}`;
|
||||
|
||||
let status: ("modified" | "saved" | "error")
|
||||
|
||||
logseq.provideModel({
|
||||
async createOrUpdateQuiz() {
|
||||
const parentBlock = await logseq.Editor.getBlock(payload.uuid, { includeChildren: true });
|
||||
const answers = parentBlock.children.map((answer: BlockEntity, i: number) => {
|
||||
return { text: answer.content, correct: (i == 0) ? true : false }
|
||||
})
|
||||
|
||||
const quiz = {
|
||||
question: { text: sanitizeBlockContent(parentBlock.content) },
|
||||
answers: answers
|
||||
}
|
||||
|
||||
if (parentBlock.properties.proboQuizUuid) {
|
||||
const res = await fetch(endpoint + `/update/${parentBlock.properties.proboQuizUuid}`, { method: 'PUT', body: JSON.stringify(quiz) })
|
||||
const data = await res.json();
|
||||
await logseq.Editor.upsertBlockProperty(parentBlock.uuid, `probo-quiz-uuid`, data.content.ID)
|
||||
render(proboId, slot, "saved")
|
||||
} else {
|
||||
const res = await fetch(endpoint + '/create', { method: 'POST', body: JSON.stringify(quiz) })
|
||||
const data = await res.json();
|
||||
await logseq.Editor.upsertBlockProperty(parentBlock.uuid, `probo-quiz-uuid`, data.content.ID)
|
||||
render(proboId, slot, "saved")
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
logseq.provideStyle(`
|
||||
.renderBtn {
|
||||
border: 1px solid white;
|
||||
border-radius: 8px;
|
||||
padding: 5px;
|
||||
margin-right: 5px;
|
||||
font-size: 80%;
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.renderBtn:hover {
|
||||
background-color: white;
|
||||
color: black;
|
||||
}
|
||||
`);
|
||||
|
||||
logseq.provideUI({
|
||||
key: `${proboId}`,
|
||||
slot,
|
||||
reset: true,
|
||||
template: `<button data-on-click="createOrUpdateQuiz" class="renderBtn">Save</button>`,
|
||||
});
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
logseq.ready(main).catch(console.error);
|
|
@ -10,13 +10,12 @@ import (
|
|||
type Exam struct {
|
||||
Meta
|
||||
|
||||
SessionID string
|
||||
Participant *Participant
|
||||
Quizzes []*Quiz
|
||||
Participant *Participant `json:"participant"`
|
||||
Quizzes []*Quiz `json:"quizzes"`
|
||||
}
|
||||
|
||||
func (e *Exam) String() string {
|
||||
return fmt.Sprintf("Exam ID %v with %v quizzes.", e.ID, len(e.Quizzes))
|
||||
return fmt.Sprintf("%v's exam with %v quizzes.", e.Participant, len(e.Quizzes))
|
||||
}
|
||||
|
||||
func (e *Exam) GetHash() string {
|
||||
|
@ -35,3 +34,17 @@ func (e *Exam) Marshal() ([]byte, error) {
|
|||
func (e *Exam) Unmarshal(data []byte) error {
|
||||
return json.Unmarshal(data, e)
|
||||
}
|
||||
|
||||
func (e *Exam) ToMarkdown() (string, error) {
|
||||
result := ""
|
||||
for _, quiz := range e.Quizzes {
|
||||
quizMD, err := QuizToMarkdown(quiz)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
result += fmt.Sprintf(quizMD)
|
||||
result += "\n"
|
||||
}
|
||||
|
||||
return strings.TrimRight(fmt.Sprintf("# %s %s \n %s", e.Participant.Lastname, e.Participant.Firstname, result), "\n"), nil
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@ Question text with #tag1 #tag2 (3).
|
|||
{Text: "Answer 4"},
|
||||
},
|
||||
CorrectPos: 0,
|
||||
Tags: []string{"#tag1", "#tag2"},
|
||||
}
|
||||
|
||||
q := new(Quiz)
|
||||
|
|
|
@ -14,12 +14,12 @@ type AttributeList map[string]string
|
|||
type Participant struct {
|
||||
Meta
|
||||
|
||||
Firstname string
|
||||
Lastname string
|
||||
Firstname string `json:"firstname" csv:"firstname"`
|
||||
Lastname string `json:"lastname" csv:"lastname"`
|
||||
|
||||
Token string
|
||||
Token string `json:"token" csv:"token"`
|
||||
|
||||
Attributes AttributeList `csv:"Attributes"`
|
||||
Attributes AttributeList `csv:"attributes"`
|
||||
}
|
||||
|
||||
func (p *Participant) String() string {
|
||||
|
|
|
@ -15,11 +15,11 @@ type Quiz struct {
|
|||
Meta
|
||||
|
||||
Hash string `json:"hash"`
|
||||
Question *Question `json:"question" gorm:"foreignKey:ID"`
|
||||
Answers []*Answer `json:"answers" gorm:"many2many:quiz_answers"`
|
||||
Tags []*Tag `json:"tags" yaml:"-" gorm:"-"`
|
||||
Correct *Answer `json:"correct" gorm:"foreignKey:ID"`
|
||||
CorrectPos uint `gorm:"-"` // Position of the correct answer during quiz creation
|
||||
Question *Question `json:"question"`
|
||||
Answers []*Answer `json:"answers"`
|
||||
Tags []string `json:"tags" yaml:"-"`
|
||||
Correct *Answer `json:"correct"`
|
||||
CorrectPos uint // Position of the correct answer during quiz creation
|
||||
Type int `json:"type"`
|
||||
}
|
||||
|
||||
|
@ -33,6 +33,7 @@ func MarkdownToQuiz(quiz *Quiz, markdown string) error {
|
|||
|
||||
questionText := ""
|
||||
answers := []*Answer{}
|
||||
tags := make([]string, 0)
|
||||
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "*") {
|
||||
|
@ -45,6 +46,8 @@ func MarkdownToQuiz(quiz *Quiz, markdown string) error {
|
|||
}
|
||||
questionText += line
|
||||
}
|
||||
|
||||
parseTags(&tags, line)
|
||||
}
|
||||
|
||||
questionText = strings.TrimRight(questionText, "\n")
|
||||
|
@ -61,8 +64,7 @@ func MarkdownToQuiz(quiz *Quiz, markdown string) error {
|
|||
|
||||
quiz.Question = question
|
||||
quiz.Answers = answers
|
||||
|
||||
//quiz = &Quiz{Question: question, Answers: answers, CorrectPos: 0}
|
||||
quiz.Tags = tags
|
||||
|
||||
if meta != nil {
|
||||
quiz.Meta = *meta
|
||||
|
@ -201,3 +203,30 @@ func readLine(reader *strings.Reader) (string, error) {
|
|||
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
func parseTags(tags *[]string, text string) {
|
||||
// Trim the following chars
|
||||
trimChars := "*:.,/\\@()[]{}<>"
|
||||
|
||||
// Split the text into words
|
||||
words := strings.Fields(text)
|
||||
|
||||
for _, word := range words {
|
||||
// If the word starts with '#', it is considered as a tag
|
||||
if strings.HasPrefix(word, "#") {
|
||||
// Check if the tag already exists in the tags slice
|
||||
exists := false
|
||||
for _, tag := range *tags {
|
||||
if tag == word {
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If the tag does not exist in the tags slice, add it
|
||||
if !exists {
|
||||
*tags = append(*tags, strings.TrimRight(word, trimChars))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,14 +5,22 @@ import (
|
|||
"fmt"
|
||||
)
|
||||
|
||||
type ParticipantAnswer struct {
|
||||
Quiz *Quiz `json:"question"`
|
||||
Answer *Answer `json:"answer"`
|
||||
Correct bool `json:"correct"`
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Meta
|
||||
|
||||
Questions map[string]string
|
||||
SessionTitle string `json:"session_title"`
|
||||
Participant *Participant `json:"participant"`
|
||||
Answers []*ParticipantAnswer `json:"answers"`
|
||||
}
|
||||
|
||||
func (r *Response) String() string {
|
||||
return fmt.Sprintf("Questions/Answers: %v", r.Questions)
|
||||
return fmt.Sprintf("Questions/Answers: %v", r.Answers)
|
||||
}
|
||||
|
||||
func (r *Response) GetHash() string {
|
||||
|
|
|
@ -9,16 +9,21 @@ import (
|
|||
type Session struct {
|
||||
Meta
|
||||
|
||||
Name string
|
||||
Exams map[string]*Exam
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
|
||||
Participants map[string]*Participant `json:"participants"`
|
||||
Quizzes map[string]*Quiz `json:"quizzes"`
|
||||
Answers map[string]*Answer `json:"answers"`
|
||||
Exams map[string]*Exam `json:"exams"`
|
||||
}
|
||||
|
||||
func (s *Session) String() string {
|
||||
return s.Name
|
||||
return s.Title
|
||||
}
|
||||
|
||||
func (s *Session) GetHash() string {
|
||||
return fmt.Sprintf("%x", sha256.Sum256([]byte(s.Name)))
|
||||
return fmt.Sprintf("%x", sha256.Sum256([]byte(s.Title)))
|
||||
}
|
||||
|
||||
func (s *Session) Marshal() ([]byte, error) {
|
||||
|
|
|
@ -1,14 +1,5 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Tag struct {
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
Name string `json:"name" gorm:"primaryKey"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import (
|
|||
"os"
|
||||
|
||||
"git.andreafazzi.eu/andrea/probo/pkg/models"
|
||||
"git.andreafazzi.eu/andrea/probo/pkg/store"
|
||||
"github.com/remogatto/prettytest"
|
||||
)
|
||||
|
||||
|
@ -15,67 +14,67 @@ type examTestSuite struct {
|
|||
prettytest.Suite
|
||||
}
|
||||
|
||||
func (t *examTestSuite) TestCreate() {
|
||||
participantStore, err := NewParticipantFileStore(
|
||||
&FileStoreConfig[*models.Participant, *store.ParticipantStore]{
|
||||
FilePathConfig: FilePathConfig{"testdata/exams/participants", "participant", ".json"},
|
||||
IndexDirFunc: DefaultIndexDirFunc[*models.Participant, *store.ParticipantStore],
|
||||
CreateEntityFunc: func() *models.Participant {
|
||||
return &models.Participant{
|
||||
Attributes: make(map[string]string),
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
// func (t *examTestSuite) TestCreate() {
|
||||
// participantStore, err := NewParticipantFileStore(
|
||||
// &FileStoreConfig[*models.Participant, *store.ParticipantStore]{
|
||||
// FilePathConfig: FilePathConfig{"testdata/exams/participants", "participant", ".json"},
|
||||
// IndexDirFunc: DefaultIndexDirFunc[*models.Participant, *store.ParticipantStore],
|
||||
// CreateEntityFunc: func() *models.Participant {
|
||||
// return &models.Participant{
|
||||
// Attributes: make(map[string]string),
|
||||
// }
|
||||
// },
|
||||
// },
|
||||
// )
|
||||
|
||||
t.Nil(err)
|
||||
// t.Nil(err)
|
||||
|
||||
quizStore, err := NewQuizFileStore(
|
||||
&FileStoreConfig[*models.Quiz, *store.QuizStore]{
|
||||
FilePathConfig: FilePathConfig{"testdata/exams/quizzes", "quiz", ".md"},
|
||||
IndexDirFunc: DefaultQuizIndexDirFunc,
|
||||
},
|
||||
)
|
||||
// quizStore, err := NewQuizFileStore(
|
||||
// &FileStoreConfig[*models.Quiz, *store.QuizStore]{
|
||||
// FilePathConfig: FilePathConfig{"testdata/exams/quizzes", "quiz", ".md"},
|
||||
// IndexDirFunc: DefaultQuizIndexDirFunc,
|
||||
// },
|
||||
// )
|
||||
|
||||
t.Nil(err)
|
||||
// t.Nil(err)
|
||||
|
||||
if !t.Failed() {
|
||||
t.Equal(3, len(participantStore.ReadAll()))
|
||||
// if !t.Failed() {
|
||||
// t.Equal(3, len(participantStore.ReadAll()))
|
||||
|
||||
examStore, err := NewDefaultExamFileStore()
|
||||
t.Nil(err)
|
||||
// examStore, err := NewDefaultExamFileStore()
|
||||
// t.Nil(err)
|
||||
|
||||
if !t.Failed() {
|
||||
g := new(models.Group)
|
||||
c := new(models.Collection)
|
||||
// if !t.Failed() {
|
||||
// g := new(models.Group)
|
||||
// c := new(models.Collection)
|
||||
|
||||
participants := participantStore.Storer.FilterInGroup(g, map[string]string{"class": "1 D LIN"})
|
||||
quizzes := quizStore.Storer.FilterInCollection(c, map[string]string{"tags": "#tag1"})
|
||||
// participants := participantStore.Storer.FilterInGroup(g, map[string]string{"class": "1 D LIN"})
|
||||
// quizzes := quizStore.Storer.FilterInCollection(c, map[string]string{"tags": "#tag1"})
|
||||
|
||||
for _, p := range participants {
|
||||
e := new(models.Exam)
|
||||
e.Participant = p
|
||||
e.Quizzes = quizzes
|
||||
// for _, p := range participants {
|
||||
// e := new(models.Exam)
|
||||
// e.Participant = p
|
||||
// e.Quizzes = quizzes
|
||||
|
||||
_, err = examStore.Create(e)
|
||||
t.Nil(err)
|
||||
// _, err = examStore.Create(e)
|
||||
// t.Nil(err)
|
||||
|
||||
defer os.Remove(examStore.GetPath(e))
|
||||
// defer os.Remove(examStore.GetPath(e))
|
||||
|
||||
examFromDisk, err := readExamFromJSON(e.GetID())
|
||||
t.Nil(err)
|
||||
// examFromDisk, err := readExamFromJSON(e.GetID())
|
||||
// t.Nil(err)
|
||||
|
||||
if !t.Failed() {
|
||||
t.Not(t.Nil(examFromDisk.Participant))
|
||||
if !t.Failed() {
|
||||
t.Equal("Smith", examFromDisk.Participant.Lastname)
|
||||
t.Equal(2, len(examFromDisk.Quizzes))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// if !t.Failed() {
|
||||
// t.Not(t.Nil(examFromDisk.Participant))
|
||||
// if !t.Failed() {
|
||||
// t.Equal("Smith", examFromDisk.Participant.Lastname)
|
||||
// t.Equal(2, len(examFromDisk.Quizzes))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
func readExamFromJSON(examID string) (*models.Exam, error) {
|
||||
// Build the path to the JSON file
|
||||
|
|
|
@ -12,7 +12,7 @@ import (
|
|||
"git.andreafazzi.eu/andrea/probo/pkg/store"
|
||||
)
|
||||
|
||||
type IndexDirFunc[T FileStorable, K Storer[T]] func(s *FileStore[T, K]) error
|
||||
type IndexDirFunc[T FileStorable, K store.Storer[T]] func(s *FileStore[T, K]) error
|
||||
|
||||
var (
|
||||
ErrorMetaHeaderIsNotPresent = errors.New("Meta header was not found in file.")
|
||||
|
@ -25,8 +25,13 @@ type FileStorable interface {
|
|||
Unmarshal([]byte) error
|
||||
}
|
||||
|
||||
type Storer[T store.Storable] interface {
|
||||
store.Storer[T]
|
||||
type FileStorer[T FileStorable] interface {
|
||||
// store.Storer[T]
|
||||
Create(T, ...string) (T, error)
|
||||
ReadAll() []T
|
||||
Read(string) (T, error)
|
||||
Update(T, string) (T, error)
|
||||
Delete(string) (T, error)
|
||||
}
|
||||
|
||||
type FilePathConfig struct {
|
||||
|
@ -35,14 +40,14 @@ type FilePathConfig struct {
|
|||
FileSuffix string
|
||||
}
|
||||
|
||||
type FileStoreConfig[T FileStorable, K Storer[T]] struct {
|
||||
type FileStoreConfig[T FileStorable, K store.Storer[T]] struct {
|
||||
FilePathConfig
|
||||
IndexDirFunc func(*FileStore[T, K]) error
|
||||
CreateEntityFunc func() T
|
||||
NoIndexOnCreate bool
|
||||
}
|
||||
|
||||
type FileStore[T FileStorable, K Storer[T]] struct {
|
||||
type FileStore[T FileStorable, K store.Storer[T]] struct {
|
||||
*FileStoreConfig[T, K]
|
||||
|
||||
Storer K
|
||||
|
@ -51,7 +56,7 @@ type FileStore[T FileStorable, K Storer[T]] struct {
|
|||
paths map[string]string
|
||||
}
|
||||
|
||||
func DefaultIndexDirFunc[T FileStorable, K Storer[T]](s *FileStore[T, K]) error {
|
||||
func DefaultIndexDirFunc[T FileStorable, K store.Storer[T]](s *FileStore[T, K]) error {
|
||||
if s.CreateEntityFunc == nil {
|
||||
return errors.New("CreateEntityFunc cannot be nil!")
|
||||
}
|
||||
|
@ -74,31 +79,39 @@ func DefaultIndexDirFunc[T FileStorable, K Storer[T]](s *FileStore[T, K]) error
|
|||
filename := file.Name()
|
||||
fullPath := filepath.Join(s.Dir, filename)
|
||||
|
||||
content, err := os.ReadFile(fullPath)
|
||||
fileInfo, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
return err
|
||||
panic(err)
|
||||
}
|
||||
|
||||
entity := s.CreateEntityFunc()
|
||||
if fileInfo.Size() > 0 {
|
||||
content, err := os.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = entity.Unmarshal(content)
|
||||
if err != nil {
|
||||
return err
|
||||
entity := s.CreateEntityFunc()
|
||||
|
||||
err = entity.Unmarshal(content)
|
||||
if err != nil {
|
||||
return fmt.Errorf("An error occurred unmarshalling %v: %v", filename, err)
|
||||
}
|
||||
|
||||
// mEntity, err := s.Create(entity, fullPath)
|
||||
mEntity, err := s.Storer.Create(entity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.SetPath(mEntity, fullPath)
|
||||
}
|
||||
|
||||
mEntity, err := s.Create(entity, fullPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.SetPath(mEntity, fullPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func NewFileStore[T FileStorable, K Storer[T]](config *FileStoreConfig[T, K], storer K) (*FileStore[T, K], error) {
|
||||
func NewFileStore[T FileStorable, K store.Storer[T]](config *FileStoreConfig[T, K], storer K) (*FileStore[T, K], error) {
|
||||
store := &FileStore[T, K]{
|
||||
FileStoreConfig: config,
|
||||
Storer: storer,
|
||||
|
|
|
@ -14,7 +14,7 @@ func NewParticipantFileStore(config *FileStoreConfig[*models.Participant, *store
|
|||
|
||||
pStore := new(ParticipantFileStore)
|
||||
|
||||
pStore.FileStore, err = NewFileStore[*models.Participant, *store.ParticipantStore](config, store.NewParticipantStore())
|
||||
pStore.FileStore, err = NewFileStore(config, store.NewParticipantStore())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
|
@ -60,7 +61,7 @@ func DefaultQuizIndexDirFunc(s *QuizFileStore) error {
|
|||
|
||||
err = entity.Unmarshal(content)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("An error occurred unmarshalling %v: %v", filename, err)
|
||||
}
|
||||
|
||||
var errQuizAlreadyPresent *store.ErrQuizAlreadyPresent
|
||||
|
|
|
@ -87,7 +87,7 @@ func (t *quizTestSuite) TestCreate() {
|
|||
t.Equal(a.Text, quiz.Answers[i].Text)
|
||||
}
|
||||
for i, tag := range quizFromDisk.Tags {
|
||||
t.Equal(tag.Name, quiz.Tags[i].Name)
|
||||
t.Equal(tag, quiz.Tags[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
type ResponseFileStore = FileStore[*models.Response, *store.Store[*models.Response]]
|
||||
|
||||
func NewResponseFileStore(config *FileStoreConfig[*models.Response, *store.ResponseStore]) (*ResponseFileStore, error) {
|
||||
return NewFileStore[*models.Response](config, store.NewStore[*models.Response]())
|
||||
return NewFileStore(config, store.NewStore[*models.Response]())
|
||||
}
|
||||
|
||||
func NewDefaultResponseFileStore() (*ResponseFileStore, error) {
|
||||
|
|
|
@ -5,10 +5,10 @@ import (
|
|||
"git.andreafazzi.eu/andrea/probo/pkg/store"
|
||||
)
|
||||
|
||||
type SessionFileStore = FileStore[*models.Session, *store.Store[*models.Session]]
|
||||
type SessionFileStore = FileStore[*models.Session, *store.SessionStore]
|
||||
|
||||
func NewSessionFileStore(config *FileStoreConfig[*models.Session, *store.SessionStore]) (*SessionFileStore, error) {
|
||||
return NewFileStore[*models.Session](config, store.NewStore[*models.Session]())
|
||||
return NewFileStore[*models.Session](config, store.NewSessionStore())
|
||||
}
|
||||
|
||||
func NewDefaultSessionFileStore() (*SessionFileStore, error) {
|
||||
|
|
|
@ -60,27 +60,3 @@ func generateToken() string {
|
|||
|
||||
return token
|
||||
}
|
||||
|
||||
// func (s *ParticipantStore) FilterInGroup(group *models.Group, filter map[string]string) []*models.Participant {
|
||||
// participants := s.ReadAll()
|
||||
|
||||
// if filter == nil {
|
||||
// return participants
|
||||
// }
|
||||
|
||||
// filteredParticipants := s.Filter(participants, func(p *models.Participant) bool {
|
||||
// for pk, pv := range p.Attributes {
|
||||
// for fk, fv := range filter {
|
||||
// if pk == fk && pv == fv {
|
||||
// return true
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// return false
|
||||
// })
|
||||
|
||||
// group.Participants = filteredParticipants
|
||||
|
||||
// return group.Participants
|
||||
// }
|
||||
|
|
|
@ -58,7 +58,7 @@ func (s *QuizStore) Create(quiz *models.Quiz) (*models.Quiz, error) {
|
|||
answers = append(answers, storedAnswer)
|
||||
}
|
||||
|
||||
tags := make([]*models.Tag, 0)
|
||||
tags := make([]string, 0)
|
||||
|
||||
q, err := s.Store.Create(&models.Quiz{
|
||||
Meta: quiz.Meta,
|
||||
|
@ -96,7 +96,7 @@ func (s *QuizStore) Update(quiz *models.Quiz, id string) (*models.Quiz, error) {
|
|||
answers = append(answers, storedAnswer)
|
||||
}
|
||||
|
||||
tags := make([]*models.Tag, 0)
|
||||
tags := make([]string, 0)
|
||||
|
||||
q, err := s.Store.Update(&models.Quiz{
|
||||
Question: parseTags[*models.Question](&tags, question)[0],
|
||||
|
@ -112,49 +112,7 @@ func (s *QuizStore) Update(quiz *models.Quiz, id string) (*models.Quiz, error) {
|
|||
return q, nil
|
||||
}
|
||||
|
||||
func (s *QuizStore) FilterInCollection(collection *models.Collection, filter map[string]string) []*models.Quiz {
|
||||
quizzes := s.ReadAll()
|
||||
|
||||
if filter == nil {
|
||||
return quizzes
|
||||
}
|
||||
|
||||
tagsValue := filter["tags"]
|
||||
|
||||
if tagsValue == "" || len(tagsValue) == 0 {
|
||||
return quizzes
|
||||
}
|
||||
|
||||
fTags := strings.Split(tagsValue, ",")
|
||||
|
||||
filteredQuizzes := s.Filter(quizzes, func(q *models.Quiz) bool {
|
||||
count := 0
|
||||
for _, qTag := range q.Tags {
|
||||
if s.isTagInFilter(qTag, fTags) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
if count == len(fTags) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
collection.Quizzes = filteredQuizzes
|
||||
|
||||
return collection.Quizzes
|
||||
}
|
||||
|
||||
func (s *QuizStore) isTagInFilter(tag *models.Tag, fTags []string) bool {
|
||||
for _, t := range fTags {
|
||||
if tag.Name == strings.TrimSpace(t) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func parseTags[T fmt.Stringer](tags *[]*models.Tag, entities ...T) []T {
|
||||
func parseTags[T fmt.Stringer](tags *[]string, entities ...T) []T {
|
||||
for _, entity := range entities {
|
||||
// Trim the following chars
|
||||
trimChars := "*:.,/\\@()[]{}<>"
|
||||
|
@ -168,7 +126,7 @@ func parseTags[T fmt.Stringer](tags *[]*models.Tag, entities ...T) []T {
|
|||
// Check if the tag already exists in the tags slice
|
||||
exists := false
|
||||
for _, tag := range *tags {
|
||||
if tag.Name == word {
|
||||
if tag == word {
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
|
@ -176,7 +134,7 @@ func parseTags[T fmt.Stringer](tags *[]*models.Tag, entities ...T) []T {
|
|||
|
||||
// If the tag does not exist in the tags slice, add it
|
||||
if !exists {
|
||||
*tags = append(*tags, &models.Tag{Name: strings.TrimRight(word, trimChars)})
|
||||
*tags = append(*tags, strings.TrimRight(word, trimChars))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -183,45 +183,6 @@ func (t *quizTestSuite) TestDeleteQuiz() {
|
|||
}
|
||||
}
|
||||
|
||||
func (t *quizTestSuite) TestFilter() {
|
||||
store := NewQuizStore()
|
||||
|
||||
quiz_1, _ := store.Create(
|
||||
&models.Quiz{
|
||||
Question: &models.Question{Text: "Question text with #tag1."},
|
||||
Answers: []*models.Answer{
|
||||
{Text: "Answer 1"},
|
||||
{Text: "Answer 2 with #tag2"},
|
||||
{Text: "Answer 3"},
|
||||
{Text: "Answer 4"},
|
||||
},
|
||||
CorrectPos: 0,
|
||||
})
|
||||
|
||||
quiz_2, _ := store.Create(
|
||||
&models.Quiz{
|
||||
Question: &models.Question{Text: "Question text with #tag3."},
|
||||
Answers: []*models.Answer{
|
||||
{Text: "Answer 1"},
|
||||
{Text: "Answer 2 with #tag4"},
|
||||
{Text: "Answer 3"},
|
||||
{Text: "Answer 4"},
|
||||
},
|
||||
CorrectPos: 0,
|
||||
})
|
||||
|
||||
quizzes := store.Filter([]*models.Quiz{quiz_1, quiz_2}, func(q *models.Quiz) bool {
|
||||
for _, t := range q.Tags {
|
||||
if t.Name == "#tag1" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
t.Equal(1, len(quizzes))
|
||||
}
|
||||
|
||||
func (t *quizTestSuite) TestParseTextForTags() {
|
||||
store := NewQuizStore()
|
||||
|
||||
|
@ -244,8 +205,8 @@ func (t *quizTestSuite) TestParseTextForTags() {
|
|||
t.Nil(err, "Quiz should be found in the store.")
|
||||
t.Equal(2, len(storedQuiz.Tags))
|
||||
if !t.Failed() {
|
||||
t.Equal("#tag1", storedQuiz.Tags[0].Name)
|
||||
t.Equal("#tag2", storedQuiz.Tags[1].Name)
|
||||
t.Equal("#tag1", storedQuiz.Tags[0])
|
||||
t.Equal("#tag2", storedQuiz.Tags[1])
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,86 @@
|
|||
package store
|
||||
|
||||
import "git.andreafazzi.eu/andrea/probo/pkg/models"
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
type SessionStore = Store[*models.Session]
|
||||
"git.andreafazzi.eu/andrea/probo/pkg/models"
|
||||
)
|
||||
|
||||
type SessionStore struct {
|
||||
*Store[*models.Session]
|
||||
}
|
||||
|
||||
type ErrSessionAlreadyPresent struct {
|
||||
hash string
|
||||
}
|
||||
|
||||
func (e *ErrSessionAlreadyPresent) Error() string {
|
||||
return fmt.Sprintf("Session with hash %v is already present in the store.", e.hash)
|
||||
}
|
||||
|
||||
func NewSessionStore() *SessionStore {
|
||||
return &SessionStore{
|
||||
Store: NewStore[*models.Session](),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s *SessionStore) Create(session *models.Session) (*models.Session, error) {
|
||||
if hash := session.GetHash(); hash != "" {
|
||||
session, ok := s.hashes[hash]
|
||||
if ok {
|
||||
return session, &ErrSessionAlreadyPresent{hash}
|
||||
}
|
||||
}
|
||||
|
||||
session.Participants = make(map[string]*models.Participant, 0)
|
||||
session.Quizzes = make(map[string]*models.Quiz, 0)
|
||||
session.Answers = make(map[string]*models.Answer, 0)
|
||||
|
||||
for _, exam := range session.Exams {
|
||||
|
||||
session.Participants[exam.Participant.ID] = exam.Participant
|
||||
|
||||
for _, quiz := range exam.Quizzes {
|
||||
session.Quizzes[quiz.ID] = quiz
|
||||
|
||||
for _, answer := range quiz.Answers {
|
||||
session.Answers[answer.ID] = answer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sess, err := s.Store.Create(session)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return sess, nil
|
||||
}
|
||||
|
||||
func (s *SessionStore) Update(session *models.Session, id string) (*models.Session, error) {
|
||||
_, err := s.Read(id)
|
||||
if err != nil {
|
||||
return session, err
|
||||
}
|
||||
|
||||
session.Quizzes = make(map[string]*models.Quiz, 0)
|
||||
session.Answers = make(map[string]*models.Answer, 0)
|
||||
|
||||
for _, exam := range session.Exams {
|
||||
for _, quiz := range exam.Quizzes {
|
||||
session.Quizzes[quiz.ID] = quiz
|
||||
|
||||
for _, answer := range quiz.Answers {
|
||||
session.Answers[answer.ID] = answer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sess, err := s.Store.Update(session, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return sess, nil
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package store
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
@ -26,12 +27,7 @@ type Storer[T Storable] interface {
|
|||
Read(string) (T, error)
|
||||
Update(T, string) (T, error)
|
||||
Delete(string) (T, error)
|
||||
}
|
||||
|
||||
type FilterStorer[T Storable] interface {
|
||||
Storer[T]
|
||||
|
||||
Filter([]T, func(T) bool) []T
|
||||
Json() ([]byte, error)
|
||||
}
|
||||
|
||||
type Store[T Storable] struct {
|
||||
|
@ -169,3 +165,7 @@ func (s *Store[T]) Delete(id string) (T, error) {
|
|||
|
||||
return sEntity, nil
|
||||
}
|
||||
|
||||
func (s *Store[T]) Json() ([]byte, error) {
|
||||
return json.Marshal(s.ReadAll())
|
||||
}
|
||||
|
|
2
pkg/store/testdata/participants.csv
vendored
2
pkg/store/testdata/participants.csv
vendored
|
@ -1,4 +1,4 @@
|
|||
Lastname,Firstname,Token,Attributes
|
||||
lastname,firstname,token,attributes
|
||||
CABRERA,GAIA,982998,class:1 D LIN
|
||||
VERDI,ANNA,868424,class:1 D LIN
|
||||
BIANCHI,ELENA,795233,class:1 D LIN
|
||||
|
|
|
Loading…
Reference in a new issue