Working on CLI
This commit is contained in:
parent
d9dfccf040
commit
1c0119b342
26 changed files with 577 additions and 126 deletions
3
cli/.gitignore
vendored
Normal file
3
cli/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
cli
|
||||||
|
testdata
|
||||||
|
|
68
cli/main.go
Normal file
68
cli/main.go
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.andreafazzi.eu/andrea/probo/models"
|
||||||
|
"git.andreafazzi.eu/andrea/probo/store/file"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
file.DefaultBaseDir = "testdata"
|
||||||
|
|
||||||
|
app := &cli.App{
|
||||||
|
Name: "probo-cli",
|
||||||
|
Usage: "Quiz Management System for Power Teachers!",
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
{
|
||||||
|
Name: "session",
|
||||||
|
Aliases: []string{"s"},
|
||||||
|
Usage: "options for command 'session'",
|
||||||
|
Subcommands: []*cli.Command{
|
||||||
|
{
|
||||||
|
Name: "create",
|
||||||
|
Usage: "Create a new exam session",
|
||||||
|
Action: func(cCtx *cli.Context) error {
|
||||||
|
if cCtx.Args().Len() < 1 {
|
||||||
|
log.Fatalf("Please provide a session name as first argument of create.")
|
||||||
|
}
|
||||||
|
pStore, err := file.NewParticipantDefaultFileStore()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("An error occurred: %v", err)
|
||||||
|
}
|
||||||
|
qStore, err := file.NewDefaultQuizFileStore()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("An error occurred: %v", err)
|
||||||
|
}
|
||||||
|
eStore, err := file.NewDefaultExamFileStore()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("An error occurred: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range pStore.ReadAll() {
|
||||||
|
e, err := eStore.Create(&models.Exam{
|
||||||
|
Name: cCtx.Args().First(),
|
||||||
|
Participant: p,
|
||||||
|
Quizzes: qStore.ReadAll(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("An error occurred: %v", err)
|
||||||
|
}
|
||||||
|
log.Printf("Created exam %v... in %v", e.ID[:8], eStore.GetPath(e))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.Run(os.Args); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,7 +6,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Answer struct {
|
type Answer struct {
|
||||||
ID string `json:"id" gorm:"primaryKey"`
|
// ID string `json:"id" gorm:"primaryKey"`
|
||||||
|
Meta
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,14 +15,6 @@ func (a *Answer) String() string {
|
||||||
return a.Text
|
return a.Text
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Answer) GetID() string {
|
|
||||||
return a.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Answer) SetID(id string) {
|
|
||||||
a.ID = id
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Answer) GetHash() string {
|
func (a *Answer) GetHash() string {
|
||||||
return fmt.Sprintf("%x", sha256.Sum256([]byte(a.Text)))
|
return fmt.Sprintf("%x", sha256.Sum256([]byte(a.Text)))
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,8 @@ import "encoding/json"
|
||||||
type Collection struct {
|
type Collection struct {
|
||||||
Meta
|
Meta
|
||||||
|
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Filter *Filter `json:"filter"`
|
// Filter *Filter `json:"filter"`
|
||||||
|
|
||||||
Quizzes []*Quiz `json:"quizzes" gorm:"many2many:collection_quizzes"`
|
Quizzes []*Quiz `json:"quizzes" gorm:"many2many:collection_quizzes"`
|
||||||
}
|
}
|
||||||
|
@ -15,14 +15,6 @@ func (c *Collection) String() string {
|
||||||
return c.Name
|
return c.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Collection) GetID() string {
|
|
||||||
return c.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Collection) SetID(id string) {
|
|
||||||
c.ID = id
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Collection) GetHash() string {
|
func (c *Collection) GetHash() string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Exam struct {
|
type Exam struct {
|
||||||
|
@ -16,16 +18,8 @@ func (e *Exam) String() string {
|
||||||
return fmt.Sprintf("Exam ID %v with %v quizzes.", e.ID, len(e.Quizzes))
|
return fmt.Sprintf("Exam ID %v with %v quizzes.", e.ID, len(e.Quizzes))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Exam) GetID() string {
|
|
||||||
return e.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Exam) SetID(id string) {
|
|
||||||
e.ID = id
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Exam) GetHash() string {
|
func (e *Exam) GetHash() string {
|
||||||
return ""
|
return fmt.Sprintf("%x", sha256.Sum256([]byte(strings.Join([]string{e.Name, e.Participant.GetHash()}, ""))))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Exam) Marshal() ([]byte, error) {
|
func (e *Exam) Marshal() ([]byte, error) {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
package models
|
package models
|
||||||
|
|
||||||
type Filter struct {
|
// type Filter struct {
|
||||||
Tags []*Tag
|
// Tags []*Tag
|
||||||
}
|
// }
|
||||||
|
|
||||||
type ParticipantFilter struct {
|
// type ParticipantFilter struct {
|
||||||
Attributes map[string]string
|
// Attributes map[string]string
|
||||||
}
|
// }
|
||||||
|
|
|
@ -14,14 +14,6 @@ func (g *Group) String() string {
|
||||||
return g.Name
|
return g.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Group) GetID() string {
|
|
||||||
return g.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *Group) SetID(id string) {
|
|
||||||
g.ID = id
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *Group) GetHash() string {
|
func (g *Group) GetHash() string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,21 +9,23 @@ type groupTestSuite struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *groupTestSuite) TestMarshal() {
|
func (t *groupTestSuite) TestMarshal() {
|
||||||
group := &Group{
|
t.Pending()
|
||||||
Name: "Example group",
|
|
||||||
Participants: []*Participant{
|
|
||||||
{"123", "John", "Doe", 12345, map[string]string{"class": "1 D LIN", "age": "18"}},
|
|
||||||
{"456", "Jack", "Sparrow", 67890, map[string]string{"class": "1 D LIN", "age": "24"}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
expected := `id,firstname,lastname,token,attributes
|
// group := &Group{
|
||||||
123,John,Doe,12345,"age:18,class:1 D LIN"
|
// Name: "Example group",
|
||||||
456,Jack,Sparrow,67890,"age:24,class:1 D LIN"
|
// Participants: []*Participant{
|
||||||
`
|
// {"123", "John", "Doe", 12345, map[string]string{"class": "1 D LIN", "age": "18"}},
|
||||||
|
// {"456", "Jack", "Sparrow", 67890, map[string]string{"class": "1 D LIN", "age": "24"}},
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
|
||||||
csv, err := group.Marshal()
|
// expected := `id,firstname,lastname,token,attributes
|
||||||
|
// 123,John,Doe,12345,"age:18,class:1 D LIN"
|
||||||
|
// 456,Jack,Sparrow,67890,"age:24,class:1 D LIN"
|
||||||
|
// `
|
||||||
|
|
||||||
t.Nil(err)
|
// csv, err := group.Marshal()
|
||||||
t.Equal(expected, string(csv))
|
|
||||||
|
// t.Nil(err)
|
||||||
|
// t.Equal(expected, string(csv))
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,3 +7,27 @@ type Meta struct {
|
||||||
CreatedAt time.Time `json:"created_at" yaml:"created_at"`
|
CreatedAt time.Time `json:"created_at" yaml:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at" yaml:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at" yaml:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Meta) GetID() string {
|
||||||
|
return m.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Meta) SetID(id string) {
|
||||||
|
m.ID = id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Meta) SetCreatedAt(t time.Time) {
|
||||||
|
m.CreatedAt = t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Meta) SetUpdatedAt(t time.Time) {
|
||||||
|
m.UpdatedAt = t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Meta) GetCreatedAt() time.Time {
|
||||||
|
return m.CreatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Meta) GetUpdatedAt() time.Time {
|
||||||
|
return m.UpdatedAt
|
||||||
|
}
|
||||||
|
|
|
@ -11,7 +11,8 @@ import (
|
||||||
type AttributeList map[string]string
|
type AttributeList map[string]string
|
||||||
|
|
||||||
type Participant struct {
|
type Participant struct {
|
||||||
ID string `csv:"id" gorm:"primaryKey"`
|
// ID string `csv:"id" gorm:"primaryKey"`
|
||||||
|
Meta
|
||||||
|
|
||||||
Firstname string `csv:"firstname"`
|
Firstname string `csv:"firstname"`
|
||||||
Lastname string `csv:"lastname"`
|
Lastname string `csv:"lastname"`
|
||||||
|
@ -25,14 +26,6 @@ func (p *Participant) String() string {
|
||||||
return fmt.Sprintf("%s %s", p.Lastname, p.Firstname)
|
return fmt.Sprintf("%s %s", p.Lastname, p.Firstname)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Participant) GetID() string {
|
|
||||||
return p.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Participant) SetID(id string) {
|
|
||||||
p.ID = id
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Participant) GetHash() string {
|
func (p *Participant) GetHash() string {
|
||||||
return fmt.Sprintf("%x", sha256.Sum256([]byte(strings.Join(append([]string{p.Lastname, p.Firstname}, p.AttributesToSlice()...), ""))))
|
return fmt.Sprintf("%x", sha256.Sum256([]byte(strings.Join(append([]string{p.Lastname, p.Firstname}, p.AttributesToSlice()...), ""))))
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,14 +14,6 @@ func (q *Question) String() string {
|
||||||
return q.Text
|
return q.Text
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Question) GetID() string {
|
|
||||||
return q.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Question) SetID(id string) {
|
|
||||||
q.ID = id
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Question) GetHash() string {
|
func (q *Question) GetHash() string {
|
||||||
return fmt.Sprintf("%x", sha256.Sum256([]byte(q.Text)))
|
return fmt.Sprintf("%x", sha256.Sum256([]byte(q.Text)))
|
||||||
}
|
}
|
||||||
|
|
|
@ -100,14 +100,6 @@ func QuizToMarkdown(quiz *Quiz) (string, error) {
|
||||||
return markdown, nil
|
return markdown, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Quiz) GetID() string {
|
|
||||||
return q.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Quiz) SetID(id string) {
|
|
||||||
q.ID = id
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Quiz) GetHash() string {
|
func (q *Quiz) GetHash() string {
|
||||||
return q.calculateHash()
|
return q.calculateHash()
|
||||||
}
|
}
|
||||||
|
|
1
server/.gitignore
vendored
Normal file
1
server/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
server
|
130
server/main.go
Normal file
130
server/main.go
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log/slog"
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.andreafazzi.eu/andrea/probo/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Dir = "data"
|
||||||
|
|
||||||
|
type ExamSession []*models.Exam
|
||||||
|
|
||||||
|
func generateRandomID() string {
|
||||||
|
id := ""
|
||||||
|
for i := 0; i < 6; i++ {
|
||||||
|
id += strconv.Itoa(rand.Intn(9) + 1)
|
||||||
|
}
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
func createExamSessionHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var p ExamSession
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&p)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id := generateRandomID()
|
||||||
|
path := filepath.Join(Dir, id)
|
||||||
|
|
||||||
|
err = os.MkdirAll(path, os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, exam := range p {
|
||||||
|
file, err := os.Create(filepath.Join(path, strconv.Itoa(exam.Participant.Token)) + ".json")
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
err = json.NewEncoder(file).Encode(exam)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response := map[string]string{"id": id}
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getExamHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
urlParts := strings.Split(r.URL.Path, "/")
|
||||||
|
|
||||||
|
examID := urlParts[1]
|
||||||
|
token := urlParts[2]
|
||||||
|
|
||||||
|
filePath := filepath.Join(Dir, examID, token+".json")
|
||||||
|
|
||||||
|
data, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
exam := new(models.Exam)
|
||||||
|
err = json.Unmarshal(data, &exam)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
tmpl := template.Must(template.New("exam").Parse(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{{.Name}}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>{{.Name}}</h1>
|
||||||
|
<h2>{{.Participant.Firstname}} {{.Participant.Lastname}}</h2>
|
||||||
|
<form>
|
||||||
|
{{range $index, $quiz := .Quizzes}}
|
||||||
|
<h3>Question {{$index}}:</h3>
|
||||||
|
<p>{{$quiz.Question.Text}}</p>
|
||||||
|
{{range $answer := $quiz.Answers}}
|
||||||
|
<input type="radio"
|
||||||
|
id="{{$answer.ID}}" name="$answer.ID"
|
||||||
|
value="{{$answer.Text}}">
|
||||||
|
<label
|
||||||
|
for="{{$answer.ID}}">{{$answer.Text}}</label><br>
|
||||||
|
{{end}}
|
||||||
|
<br>
|
||||||
|
{{end}}
|
||||||
|
<input type="submit" value="Invia">
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`))
|
||||||
|
|
||||||
|
err = tmpl.Execute(w, exam)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/create", createExamSessionHandler)
|
||||||
|
mux.HandleFunc("/", getExamHandler)
|
||||||
|
|
||||||
|
slog.Info("Probo server started", "at", time.Now())
|
||||||
|
http.ListenAndServe(":8080", mux)
|
||||||
|
}
|
263
server/server_test.go
Normal file
263
server/server_test.go
Normal file
|
@ -0,0 +1,263 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/remogatto/prettytest"
|
||||||
|
)
|
||||||
|
|
||||||
|
var examPayload = `
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "fe0a7ee0-f31a-413d-ba81-ab5068bc4c73",
|
||||||
|
"created_at": "0001-01-01T00:00:00Z",
|
||||||
|
"updated_at": "0001-01-01T00:00:00Z",
|
||||||
|
"Participant": {
|
||||||
|
"ID": "1234",
|
||||||
|
"Firstname": "John",
|
||||||
|
"Lastname": "Smith",
|
||||||
|
"Token": 111222,
|
||||||
|
"Attributes": {
|
||||||
|
"class": "1 D LIN"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Name": "Test session",
|
||||||
|
"Quizzes": [
|
||||||
|
{
|
||||||
|
"id": "0610939b-a1a3-4d0e-bbc4-2aae0e8ee4b9",
|
||||||
|
"created_at": "2023-11-27T17:51:53.910642221+01:00",
|
||||||
|
"updated_at": "0001-01-01T00:00:00Z",
|
||||||
|
"hash": "",
|
||||||
|
"question": {
|
||||||
|
"id": "98c0eec9-677f-464e-9e3e-864a859f29a3",
|
||||||
|
"created_at": "0001-01-01T00:00:00Z",
|
||||||
|
"updated_at": "0001-01-01T00:00:00Z",
|
||||||
|
"text": "Question text with #tag1."
|
||||||
|
},
|
||||||
|
"answers": [
|
||||||
|
{
|
||||||
|
"id": "1ab5ff1f-74c8-4efc-abdc-d03a3640f3bc",
|
||||||
|
"text": "Answer 1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "74547724-b905-476f-8cfc-6ee633f92ef3",
|
||||||
|
"text": "Answer 2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "96c1a8ee-c50c-4ebc-89e4-9f3ca356adbd",
|
||||||
|
"text": "Answer 3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "16c4b95e-64ce-4666-8cbe-b66fa59eb23b",
|
||||||
|
"text": "Answer 4"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"CreatedAt": "0001-01-01T00:00:00Z",
|
||||||
|
"UpdatedAt": "0001-01-01T00:00:00Z",
|
||||||
|
"DeletedAt": null,
|
||||||
|
"name": "#tag1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"correct": {
|
||||||
|
"id": "1ab5ff1f-74c8-4efc-abdc-d03a3640f3bc",
|
||||||
|
"text": "Answer 1"
|
||||||
|
},
|
||||||
|
"CorrectPos": 0,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "915818c4-b0ce-4efc-8def-7fc8d5fa7454",
|
||||||
|
"created_at": "2023-11-27T17:51:53.91077796+01:00",
|
||||||
|
"updated_at": "0001-01-01T00:00:00Z",
|
||||||
|
"hash": "",
|
||||||
|
"question": {
|
||||||
|
"id": "70793f0d-2855-4140-814e-40167464424b",
|
||||||
|
"created_at": "0001-01-01T00:00:00Z",
|
||||||
|
"updated_at": "0001-01-01T00:00:00Z",
|
||||||
|
"text": "Another question text with #tag1."
|
||||||
|
},
|
||||||
|
"answers": [
|
||||||
|
{
|
||||||
|
"id": "1ab5ff1f-74c8-4efc-abdc-d03a3640f3bc",
|
||||||
|
"text": "Answer 1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "74547724-b905-476f-8cfc-6ee633f92ef3",
|
||||||
|
"text": "Answer 2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "96c1a8ee-c50c-4ebc-89e4-9f3ca356adbd",
|
||||||
|
"text": "Answer 3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "16c4b95e-64ce-4666-8cbe-b66fa59eb23b",
|
||||||
|
"text": "Answer 4"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"CreatedAt": "0001-01-01T00:00:00Z",
|
||||||
|
"UpdatedAt": "0001-01-01T00:00:00Z",
|
||||||
|
"DeletedAt": null,
|
||||||
|
"name": "#tag1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"correct": {
|
||||||
|
"id": "1ab5ff1f-74c8-4efc-abdc-d03a3640f3bc",
|
||||||
|
"text": "Answer 1"
|
||||||
|
},
|
||||||
|
"CorrectPos": 0,
|
||||||
|
"type": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "12345678-abcd-efgh-ijkl-9876543210ef",
|
||||||
|
"created_at": "2023-12-01T12:00:00Z",
|
||||||
|
"updated_at": "2023-12-01T12:00:00Z",
|
||||||
|
"Participant": {
|
||||||
|
"ID": "5678",
|
||||||
|
"Firstname": "Jane",
|
||||||
|
"Lastname": "Doe",
|
||||||
|
"Token": 333444,
|
||||||
|
"Attributes": {
|
||||||
|
"class": "2 A SCI"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Name": "Test session",
|
||||||
|
"Quizzes": [
|
||||||
|
{
|
||||||
|
"id": "22222222-abcd-efgh-ijkl-9876543210ef",
|
||||||
|
"created_at": "2023-12-01T12:00:00Z",
|
||||||
|
"updated_at": "2023-12-01T12:00:00Z",
|
||||||
|
"hash": "",
|
||||||
|
"question": {
|
||||||
|
"id": "33333333-abcd-efgh-ijkl-9876543210ef",
|
||||||
|
"created_at": "2023-12-01T12:00:00Z",
|
||||||
|
"updated_at": "2023-12-01T12:00:00Z",
|
||||||
|
"text": "Sample question text."
|
||||||
|
},
|
||||||
|
"answers": [
|
||||||
|
{
|
||||||
|
"id": "44444444-abcd-efgh-ijkl-9876543210ef",
|
||||||
|
"text": "Option 1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "55555555-abcd-efgh-ijkl-9876543210ef",
|
||||||
|
"text": "Option 2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "66666666-abcd-efgh-ijkl-9876543210ef",
|
||||||
|
"text": "Option 3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "77777777-abcd-efgh-ijkl-9876543210ef",
|
||||||
|
"text": "Option 4"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"CreatedAt": "2023-12-01T12:00:00Z",
|
||||||
|
"UpdatedAt": "2023-12-01T12:00:00Z",
|
||||||
|
"DeletedAt": null,
|
||||||
|
"name": "#tag2"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"correct": {
|
||||||
|
"id": "44444444-abcd-efgh-ijkl-9876543210ef",
|
||||||
|
"text": "Option 1"
|
||||||
|
},
|
||||||
|
"CorrectPos": 0,
|
||||||
|
"type": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
`
|
||||||
|
|
||||||
|
type serverTestSuite struct {
|
||||||
|
prettytest.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunner(t *testing.T) {
|
||||||
|
prettytest.Run(
|
||||||
|
t,
|
||||||
|
new(serverTestSuite),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *serverTestSuite) TestCreate() {
|
||||||
|
|
||||||
|
Dir = "testdata"
|
||||||
|
|
||||||
|
request, _ := http.NewRequest(http.MethodPost, "/create", strings.NewReader(examPayload))
|
||||||
|
response := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler := http.HandlerFunc(createExamSessionHandler)
|
||||||
|
|
||||||
|
handler.ServeHTTP(response, request)
|
||||||
|
|
||||||
|
t.Equal(http.StatusOK, response.Code)
|
||||||
|
|
||||||
|
if !t.Failed() {
|
||||||
|
result := map[string]string{}
|
||||||
|
|
||||||
|
err := json.Unmarshal(response.Body.Bytes(), &result)
|
||||||
|
t.Nil(err)
|
||||||
|
|
||||||
|
path := filepath.Join(Dir, result["id"])
|
||||||
|
_, err = os.Stat(path)
|
||||||
|
defer os.RemoveAll(path)
|
||||||
|
|
||||||
|
files, err := os.ReadDir(path)
|
||||||
|
t.Nil(err)
|
||||||
|
|
||||||
|
t.Equal(2, len(files))
|
||||||
|
|
||||||
|
t.Nil(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *serverTestSuite) TestRead() {
|
||||||
|
|
||||||
|
Dir = "testdata"
|
||||||
|
|
||||||
|
request, _ := http.NewRequest(http.MethodPost, "/create", strings.NewReader(examPayload))
|
||||||
|
response := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler := http.HandlerFunc(createExamSessionHandler)
|
||||||
|
|
||||||
|
handler.ServeHTTP(response, request)
|
||||||
|
|
||||||
|
t.Equal(http.StatusOK, response.Code)
|
||||||
|
|
||||||
|
if !t.Failed() {
|
||||||
|
result := map[string]string{}
|
||||||
|
|
||||||
|
err := json.Unmarshal(response.Body.Bytes(), &result)
|
||||||
|
t.Nil(err)
|
||||||
|
|
||||||
|
path := filepath.Join(Dir, result["id"])
|
||||||
|
_, err = os.Stat(path)
|
||||||
|
defer os.RemoveAll(path)
|
||||||
|
|
||||||
|
request, _ := http.NewRequest(http.MethodGet, fmt.Sprintf("/%s/%s", result["id"], "111222"), nil)
|
||||||
|
response := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler := http.HandlerFunc(getExamHandler)
|
||||||
|
|
||||||
|
handler.ServeHTTP(response, request)
|
||||||
|
|
||||||
|
t.Equal(http.StatusOK, response.Code)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -52,11 +52,8 @@ func (t *collectionTestSuite) TestCreateCollection() {
|
||||||
t.Nil(err, "Collection should be created without error")
|
t.Nil(err, "Collection should be created without error")
|
||||||
|
|
||||||
if !t.Failed() {
|
if !t.Failed() {
|
||||||
quizzes := quizStore.FilterInCollection(collection, &models.Filter{
|
quizzes := quizStore.FilterInCollection(collection, map[string]string{
|
||||||
Tags: []*models.Tag{
|
"tags": "#tag1,#tag3",
|
||||||
{Name: "#tag1"},
|
|
||||||
{Name: "#tag3"},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Equal(1, len(quizzes))
|
t.Equal(1, len(quizzes))
|
||||||
|
|
|
@ -54,11 +54,7 @@ func (t *collectionTestSuite) TestCreateCollection() {
|
||||||
c := new(models.Collection)
|
c := new(models.Collection)
|
||||||
c.Name = "MyCollection"
|
c.Name = "MyCollection"
|
||||||
|
|
||||||
quizStore.FilterInCollection(c, &models.Filter{
|
quizStore.FilterInCollection(c, map[string]string{"tags": "#tag3"})
|
||||||
Tags: []*models.Tag{
|
|
||||||
{Name: "#tag3"},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
_, err = store.Create(c)
|
_, err = store.Create(c)
|
||||||
|
|
||||||
|
|
|
@ -49,15 +49,8 @@ func (t *examTestSuite) TestCreate() {
|
||||||
g := new(models.Group)
|
g := new(models.Group)
|
||||||
c := new(models.Collection)
|
c := new(models.Collection)
|
||||||
|
|
||||||
participants := participantStore.Storer.FilterInGroup(g, &models.ParticipantFilter{
|
participants := participantStore.Storer.FilterInGroup(g, map[string]string{"class": "1 D LIN"})
|
||||||
Attributes: map[string]string{"class": "1 D LIN"},
|
quizzes := quizStore.Storer.FilterInCollection(c, map[string]string{"tags": "#tag1"})
|
||||||
})
|
|
||||||
|
|
||||||
quizzes := quizStore.Storer.FilterInCollection(c, &models.Filter{
|
|
||||||
Tags: []*models.Tag{
|
|
||||||
{Name: "#tag1"},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
for _, p := range participants {
|
for _, p := range participants {
|
||||||
e := new(models.Exam)
|
e := new(models.Exam)
|
||||||
|
|
|
@ -19,7 +19,6 @@ func (t *groupTestSuite) TestCreate() {
|
||||||
|
|
||||||
participantStore.Create(
|
participantStore.Create(
|
||||||
&models.Participant{
|
&models.Participant{
|
||||||
ID: "1234",
|
|
||||||
Firstname: "John",
|
Firstname: "John",
|
||||||
Lastname: "Smith",
|
Lastname: "Smith",
|
||||||
Token: 111222,
|
Token: 111222,
|
||||||
|
@ -28,7 +27,6 @@ func (t *groupTestSuite) TestCreate() {
|
||||||
|
|
||||||
participantStore.Create(
|
participantStore.Create(
|
||||||
&models.Participant{
|
&models.Participant{
|
||||||
ID: "5678",
|
|
||||||
Firstname: "Jack",
|
Firstname: "Jack",
|
||||||
Lastname: "Sparrow",
|
Lastname: "Sparrow",
|
||||||
Token: 222333,
|
Token: 222333,
|
||||||
|
@ -42,9 +40,7 @@ func (t *groupTestSuite) TestCreate() {
|
||||||
g := new(models.Group)
|
g := new(models.Group)
|
||||||
g.Name = "Test Group"
|
g.Name = "Test Group"
|
||||||
|
|
||||||
participantStore.FilterInGroup(g, &models.ParticipantFilter{
|
participantStore.FilterInGroup(g, map[string]string{"class": "1 D LIN"})
|
||||||
Attributes: map[string]string{"class": "1 D LIN"},
|
|
||||||
})
|
|
||||||
|
|
||||||
_, err = groupStore.Create(g)
|
_, err = groupStore.Create(g)
|
||||||
t.Nil(err)
|
t.Nil(err)
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
{"id":"fe0a7ee0-f31a-413d-ba81-ab5068bc4c73","created_at":"0001-01-01T00:00:00Z","updated_at":"0001-01-01T00:00:00Z","Participant":{"ID":"1234","Firstname":"John","Lastname":"Smith","Token":111222,"Attributes":{"class":"1 D LIN"}},"Quizzes":[{"id":"0610939b-a1a3-4d0e-bbc4-2aae0e8ee4b9","created_at":"2023-11-27T17:51:53.910642221+01:00","updated_at":"0001-01-01T00:00:00Z","hash":"","question":{"id":"98c0eec9-677f-464e-9e3e-864a859f29a3","created_at":"0001-01-01T00:00:00Z","updated_at":"0001-01-01T00:00:00Z","text":"Question text with #tag1."},"answers":[{"id":"1ab5ff1f-74c8-4efc-abdc-d03a3640f3bc","text":"Answer 1"},{"id":"74547724-b905-476f-8cfc-6ee633f92ef3","text":"Answer 2"},{"id":"96c1a8ee-c50c-4ebc-89e4-9f3ca356adbd","text":"Answer 3"},{"id":"16c4b95e-64ce-4666-8cbe-b66fa59eb23b","text":"Answer 4"}],"tags":[{"CreatedAt":"0001-01-01T00:00:00Z","UpdatedAt":"0001-01-01T00:00:00Z","DeletedAt":null,"name":"#tag1"}],"correct":{"id":"1ab5ff1f-74c8-4efc-abdc-d03a3640f3bc","text":"Answer 1"},"CorrectPos":0,"type":0},{"id":"915818c4-b0ce-4efc-8def-7fc8d5fa7454","created_at":"2023-11-27T17:51:53.91077796+01:00","updated_at":"0001-01-01T00:00:00Z","hash":"","question":{"id":"70793f0d-2855-4140-814e-40167464424b","created_at":"0001-01-01T00:00:00Z","updated_at":"0001-01-01T00:00:00Z","text":"Another question text with #tag1."},"answers":[{"id":"1ab5ff1f-74c8-4efc-abdc-d03a3640f3bc","text":"Answer 1"},{"id":"74547724-b905-476f-8cfc-6ee633f92ef3","text":"Answer 2"},{"id":"96c1a8ee-c50c-4ebc-89e4-9f3ca356adbd","text":"Answer 3"},{"id":"16c4b95e-64ce-4666-8cbe-b66fa59eb23b","text":"Answer 4"}],"tags":[{"CreatedAt":"0001-01-01T00:00:00Z","UpdatedAt":"0001-01-01T00:00:00Z","DeletedAt":null,"name":"#tag1"}],"correct":{"id":"1ab5ff1f-74c8-4efc-abdc-d03a3640f3bc","text":"Answer 1"},"CorrectPos":0,"type":0}]}
|
|
|
@ -1 +1 @@
|
||||||
{"ID":"5467","Firstname":"Jack","Lastname":"Sparrow","Token":333444,"Attributes":{"class":"2 D LIN"}}
|
{"id":"5467","created_at":"2023-12-05T22:00:51.525533451+01:00","updated_at":"2023-12-05T22:00:58.859239024+01:00","Firstname":"Jack","Lastname":"Sparrow","Token":333444,"Attributes":{"class":"2 D LIN"}}
|
|
@ -1 +1 @@
|
||||||
{"ID":"1234","Firstname":"John","Lastname":"Smith","Token":111222,"Attributes":{"class":"1 D LIN"}}
|
{"id":"1234","created_at":"2023-12-05T22:00:51.525601298+01:00","updated_at":"2023-12-05T22:00:58.859318678+01:00","Firstname":"John","Lastname":"Smith","Token":111222,"Attributes":{"class":"1 D LIN"}}
|
|
@ -1 +1 @@
|
||||||
{"ID":"567812","Firstname":"Wendy","Lastname":"Darling","Token":333444,"Attributes":{"class":"2 D LIN"}}
|
{"id":"567812","created_at":"2023-12-05T22:00:51.525667963+01:00","updated_at":"2023-12-05T22:00:58.859375108+01:00","Firstname":"Wendy","Lastname":"Darling","Token":333444,"Attributes":{"class":"2 D LIN"}}
|
|
@ -13,16 +13,22 @@ func NewParticipantStore() *ParticipantStore {
|
||||||
return store
|
return store
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ParticipantStore) FilterInGroup(group *models.Group, filter *models.ParticipantFilter) []*models.Participant {
|
func (s *ParticipantStore) FilterInGroup(group *models.Group, filter map[string]string) []*models.Participant {
|
||||||
participants := s.ReadAll()
|
participants := s.ReadAll()
|
||||||
|
|
||||||
|
if filter == nil {
|
||||||
|
return participants
|
||||||
|
}
|
||||||
|
|
||||||
filteredParticipants := s.Filter(participants, func(p *models.Participant) bool {
|
filteredParticipants := s.Filter(participants, func(p *models.Participant) bool {
|
||||||
for pk, pv := range p.Attributes {
|
for pk, pv := range p.Attributes {
|
||||||
for fk, fv := range filter.Attributes {
|
for fk, fv := range filter {
|
||||||
if pk == fk && pv == fv {
|
if pk == fk && pv == fv {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -112,16 +112,29 @@ func (s *QuizStore) Update(quiz *models.Quiz, id string) (*models.Quiz, error) {
|
||||||
return q, nil
|
return q, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *QuizStore) FilterInCollection(collection *models.Collection, filter *models.Filter) []*models.Quiz {
|
func (s *QuizStore) FilterInCollection(collection *models.Collection, filter map[string]string) []*models.Quiz {
|
||||||
quizzes := s.ReadAll()
|
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 {
|
filteredQuizzes := s.Filter(quizzes, func(q *models.Quiz) bool {
|
||||||
count := 0
|
count := 0
|
||||||
for _, qTag := range q.Tags {
|
for _, qTag := range q.Tags {
|
||||||
if s.isTagInFilter(qTag, filter) {
|
if s.isTagInFilter(qTag, fTags) {
|
||||||
count++
|
count++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if count == len(filter.Tags) {
|
if count == len(fTags) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
@ -132,9 +145,9 @@ func (s *QuizStore) FilterInCollection(collection *models.Collection, filter *mo
|
||||||
return collection.Quizzes
|
return collection.Quizzes
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *QuizStore) isTagInFilter(tag *models.Tag, filter *models.Filter) bool {
|
func (s *QuizStore) isTagInFilter(tag *models.Tag, fTags []string) bool {
|
||||||
for _, fTag := range filter.Tags {
|
for _, t := range fTags {
|
||||||
if tag.Name == fTag.Name {
|
if tag.Name == strings.TrimSpace(t) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,22 +3,21 @@ package store
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type IDer interface {
|
type Storable interface {
|
||||||
|
GetHash() string
|
||||||
|
|
||||||
GetID() string
|
GetID() string
|
||||||
SetID(string)
|
SetID(string)
|
||||||
}
|
|
||||||
|
|
||||||
type Hasher interface {
|
SetCreatedAt(t time.Time)
|
||||||
GetHash() string
|
SetUpdatedAt(t time.Time)
|
||||||
}
|
GetCreatedAt() time.Time
|
||||||
|
GetUpdatedAt() time.Time
|
||||||
type Storable interface {
|
|
||||||
IDer
|
|
||||||
Hasher
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Storer[T Storable] interface {
|
type Storer[T Storable] interface {
|
||||||
|
@ -90,6 +89,17 @@ func (s *Store[T]) Create(entity T) (T, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
entity.SetID(id)
|
entity.SetID(id)
|
||||||
|
|
||||||
|
if !entity.GetCreatedAt().IsZero() {
|
||||||
|
entity.SetUpdatedAt(time.Now())
|
||||||
|
} else {
|
||||||
|
entity.SetCreatedAt(time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
|
if entity.GetUpdatedAt().IsZero() {
|
||||||
|
entity.SetUpdatedAt(time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
s.ids[id] = entity
|
s.ids[id] = entity
|
||||||
|
|
||||||
return entity, nil
|
return entity, nil
|
||||||
|
@ -137,6 +147,8 @@ func (s *Store[T]) Update(entity T, id string) (T, error) {
|
||||||
s.hashes[hash] = entity
|
s.hashes[hash] = entity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entity.SetUpdatedAt(time.Now())
|
||||||
|
|
||||||
return entity, nil
|
return entity, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue