Add embed and documentation templates

This commit is contained in:
andrea 2024-05-22 10:00:10 +02:00
parent 1a9c9e6b8a
commit 3250810364
36 changed files with 1084 additions and 70 deletions

View file

@ -11,53 +11,20 @@ import (
"git.andreafazzi.eu/andrea/probo/cmd/filter"
"git.andreafazzi.eu/andrea/probo/cmd/util"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/glamour"
"github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss"
"github.com/muesli/termenv"
"github.com/spf13/cobra"
)
var longDescription string = `
# Filters
**Filters can made selection over 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
Filter over participants store.
**probo filter participants**
Filter over quizzes store using the jq filter in tags.jq file
**probo filter quizzes -i data/filters/tags.jq**
Filter over participants and pipe the result on the next filter. The result is then stored in a JSON file.
**probo filter participants | probo filter quizzes > data/json/selection.json**
`
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() {
desc, err := glamour.Render(fmt.Sprintf("```\n%s```\n%s", logo, longDescription), "dark")
if err != nil {
panic(err)
}
filterCmd := &cobra.Command{
Use: "filter {participants,quizzes,responses}",
Short: "Filter the given store",
Long: desc,
Run: runFilter,
}
rootCmd.AddCommand(filterCmd)
filterCmd.PersistentFlags().StringP("input", "i", "", "Specify an input file")
}

38
cmd/init.go Normal file
View 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)
}
}

View file

@ -25,20 +25,24 @@ 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 Quiz Management System for Hackers!",
Long: `
Probo is a CLI/TUI application that allows for the quick
creation of quizzes from markdown files and their distribution
as web pages.`,
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"),
// Uncomment the following line if your bare application
// has an action associated with it:

82
cmd/serve.go Normal file
View 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/{token}", serve.Recover(examController))
mux.Handle("POST /sessions/{uuid}/exams/{token}", 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
View 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)
}

78
cmd/serve/exam.go Normal file
View file

@ -0,0 +1,78 @@
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)
}
participantToken := r.PathValue("token")
session, err := c.sStore.Read(r.PathValue("uuid"))
if err != nil {
panic(err)
}
exam, ok := session.Exams[participantToken]
if !ok {
panic(errors.New("Exam not found in the store!"))
}
examWithSession := struct {
*models.Exam
SessionID string
}{exam, session.ID}
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)
for quizID, values := range r.Form {
correct := false
quiz := session.Quizzes[quizID]
for _, answerID := range values {
log.Info(answerID)
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{
SessionID: session.ID,
Answers: answers,
})
if err != nil {
panic(err)
}
log.Info("Saving response", "response", response)
}
}

36
cmd/serve/jwt.go Normal file
View 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
View 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
View 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
View 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 {
Token string
*models.Session
}
for _, session := range c.sStore.ReadAll() {
for _, exam := range session.Exams {
if exam.Participant.Token == claims["token"] {
s := struct {
Token string
*models.Session
}{exam.Participant.Token, session}
participantSessions = append(participantSessions, s)
break
}
}
}
err = c.ExecuteTemplate(w, participantSessions)
if err != nil {
panic(err)
}
}

View file

@ -302,7 +302,7 @@ func (m *SessionModel) updateViewportContent(session *models.Session) {
m.showErrorOnStatusBar(err)
}
m.viewport.SetContent(result)
m.viewport.SetContent(sanitize(result))
}

View file

@ -1,3 +0,0 @@
{{define "description"}}
{{.Description}}
{{end}}

View file

@ -1,6 +1,5 @@
/*
Copyright © 2024 NAME HERE <EMAIL ADDRESS>
*/
package cmd
@ -13,7 +12,7 @@ import (
// updateCmd represents the update command
var updateCmd = &cobra.Command{
Use: "update",
Short: "A brief description of your command",
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:
@ -27,14 +26,4 @@ to quickly create a Cobra application.`,
func init() {
rootCmd.AddCommand(updateCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// updateCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// updateCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

View file

@ -2,14 +2,39 @@ 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]")

View 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}}

View 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}}

View file

@ -1,2 +1,2 @@
{{.Logo}}
{{template "logo" .}}
{{template "description" .}}

10
embed/cli/logo.tmpl Normal file
View file

@ -0,0 +1,10 @@
{{define "logo"}}
```
____ _
| _ \ _ __ ___ | |__ ___
| |_) | '__/ _ \| '_ \ / _ \
| __/| | | (_) | |_) | (_) |
|_| |_| \___/|_.__/ \___/____
|_____|
```
{{end}}

View 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}}

View file

@ -0,0 +1,62 @@
{{define "description"}}
# Probo
`probo` is a quiz management and administration system designed for
command line and text interface 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. Quizzes
can be written in Markdown format, filters use the `jq` language, exam
sessions, participants, and their quiz answers are JSON files.
`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
```
probo init
```
2. Create our first quiz.
```
probo create quiz > data/quizzes/quiz_1.md
```
3. Create two participants.
```
probo create participant > data/participants/john.json
probo create participant > data/participants/mary.json
```
4. Filter participants and quizzes (in this case, all participants and
all quizzes present in the store will be selected) and create an exam
session.
```
probo filter participant -f '.' | probo filter quizzes -f '.' | probo create session --name="My First Session" > data/sessions/my_session.json
```
5. Run the web server to allow participants to respond.
```
probo serve
```
6. Share the *qrcode* generated from the previous step with the participants and wait for them to finish responding.
7. Explore the results.
```
probo filter responses -f '.' | probo rank
```
{{end}}

71
embed/embed.go Normal file
View 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
}

61
embed/embed.go~ Normal file
View file

@ -0,0 +1,61 @@
package embed
import (
"embed"
"io"
"io/fs"
"os"
"path/filepath"
)
var (
//go:embed templates/*
Templates embed.FS
Assets 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 {
return err
}
fullPath := path
if info.IsDir() {
dirPath := filepath.Join(currentDir, path)
if err := os.MkdirAll(dirPath, 0755); err != nil {
return err
}
} else {
srcFile, err := data.Open(fullPath)
if err != nil {
return err
}
dstFile, err := os.Create(filepath.Join(currentDir, path))
if err != nil {
return err
}
defer dstFile.Close()
_, err = io.Copy(dstFile, srcFile)
if err != nil {
return err
}
}
return nil
}); err != nil {
return err
}
return nil
}

View 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;
}

View 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 }}

View file

@ -0,0 +1,42 @@
<!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>
<header class="d-flex flex-wrap align-items-center justify-content-between py-3 border-bottom p-3 text-bg-dark border-bottom sticky-top">
<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>
<div class="text-end">
<input type="submit" value="Salva" class="btn btn-primary me-2" form="submit-exam-form"/>
</div>
</header>
<form action="/sessions/{{.SessionID}}/exams/{{.Participant.Token}}" method="POST" id="submit-exam-form"/>
{{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>

View 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>

View 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 }}

View 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>

View 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.Token}}" class="btn btn-primary">Inizia!</a>
</div>
</div>
</div>
</div>
{{ end }}
</div>
{{ end }}

13
go.mod
View file

@ -1,15 +1,18 @@
module git.andreafazzi.eu/andrea/probo
go 1.21.6
go 1.22.2
require (
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/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/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
@ -24,10 +27,13 @@ require (
require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // 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/itchyny/timefmt-go v0.1.5 // indirect
@ -38,10 +44,12 @@ require (
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.21 // indirect
github.com/mitchellh/mapstructure v1.5.0 // 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/olekukonko/tablewriter v0.0.5 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
@ -52,9 +60,12 @@ require (
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // 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-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

36
go.sum
View file

@ -6,18 +6,25 @@ github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbf
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/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/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/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=
@ -34,12 +41,18 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
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=
@ -58,13 +71,18 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69
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.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/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-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
@ -73,8 +91,11 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU
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/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
@ -117,24 +138,39 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
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/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-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.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=
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=

View file

@ -1,4 +1,6 @@
go 1.21.6
go 1.22.2
toolchain go1.22.3
use (
.

View file

@ -5,8 +5,6 @@ cloud.google.com/go/firestore v1.14.0/go.mod h1:96MVaHLsEhbvkBEdZgfN+AS/GIkco1LR
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/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
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=
@ -51,6 +49,8 @@ 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=
@ -65,7 +65,6 @@ 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.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM=
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=

View file

@ -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()

View file

@ -88,7 +88,7 @@ func DefaultIndexDirFunc[T FileStorable, K store.Storer[T]](s *FileStore[T, K])
err = entity.Unmarshal(content)
if err != nil {
return err
return fmt.Errorf("An error occurred unmarshalling %v: %v", filename, err)
}
mEntity, err := s.Create(entity, fullPath)

View file

@ -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