Working on CLI

This commit is contained in:
andrea 2023-12-05 22:11:08 +01:00
parent d9dfccf040
commit 1c0119b342
26 changed files with 577 additions and 126 deletions

3
cli/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
cli
testdata

68
cli/main.go Normal file
View 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)
}
}

View file

@ -6,7 +6,8 @@ import (
)
type Answer struct {
ID string `json:"id" gorm:"primaryKey"`
// ID string `json:"id" gorm:"primaryKey"`
Meta
Text string `json:"text"`
}
@ -14,14 +15,6 @@ func (a *Answer) String() string {
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 {
return fmt.Sprintf("%x", sha256.Sum256([]byte(a.Text)))
}

View file

@ -6,7 +6,7 @@ type Collection struct {
Meta
Name string `json:"name"`
Filter *Filter `json:"filter"`
// Filter *Filter `json:"filter"`
Quizzes []*Quiz `json:"quizzes" gorm:"many2many:collection_quizzes"`
}
@ -15,14 +15,6 @@ func (c *Collection) String() string {
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 {
return ""
}

View file

@ -1,8 +1,10 @@
package models
import (
"crypto/sha256"
"encoding/json"
"fmt"
"strings"
)
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))
}
func (e *Exam) GetID() string {
return e.ID
}
func (e *Exam) SetID(id string) {
e.ID = id
}
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) {

View file

@ -1,9 +1,9 @@
package models
type Filter struct {
Tags []*Tag
}
// type Filter struct {
// Tags []*Tag
// }
type ParticipantFilter struct {
Attributes map[string]string
}
// type ParticipantFilter struct {
// Attributes map[string]string
// }

View file

@ -14,14 +14,6 @@ func (g *Group) String() string {
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 {
return ""
}

View file

@ -9,21 +9,23 @@ type groupTestSuite struct {
}
func (t *groupTestSuite) TestMarshal() {
group := &Group{
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"}},
},
}
t.Pending()
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"
`
// group := &Group{
// 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"}},
// },
// }
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)
t.Equal(expected, string(csv))
// csv, err := group.Marshal()
// t.Nil(err)
// t.Equal(expected, string(csv))
}

View file

@ -7,3 +7,27 @@ type Meta struct {
CreatedAt time.Time `json:"created_at" yaml:"created_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
}

View file

@ -11,7 +11,8 @@ import (
type AttributeList map[string]string
type Participant struct {
ID string `csv:"id" gorm:"primaryKey"`
// ID string `csv:"id" gorm:"primaryKey"`
Meta
Firstname string `csv:"firstname"`
Lastname string `csv:"lastname"`
@ -25,14 +26,6 @@ func (p *Participant) String() string {
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 {
return fmt.Sprintf("%x", sha256.Sum256([]byte(strings.Join(append([]string{p.Lastname, p.Firstname}, p.AttributesToSlice()...), ""))))
}

View file

@ -14,14 +14,6 @@ func (q *Question) String() string {
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 {
return fmt.Sprintf("%x", sha256.Sum256([]byte(q.Text)))
}

View file

@ -100,14 +100,6 @@ func QuizToMarkdown(quiz *Quiz) (string, error) {
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 {
return q.calculateHash()
}

1
server/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
server

130
server/main.go Normal file
View 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
View 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)
}
}

View file

@ -52,11 +52,8 @@ func (t *collectionTestSuite) TestCreateCollection() {
t.Nil(err, "Collection should be created without error")
if !t.Failed() {
quizzes := quizStore.FilterInCollection(collection, &models.Filter{
Tags: []*models.Tag{
{Name: "#tag1"},
{Name: "#tag3"},
},
quizzes := quizStore.FilterInCollection(collection, map[string]string{
"tags": "#tag1,#tag3",
})
t.Equal(1, len(quizzes))

View file

@ -54,11 +54,7 @@ func (t *collectionTestSuite) TestCreateCollection() {
c := new(models.Collection)
c.Name = "MyCollection"
quizStore.FilterInCollection(c, &models.Filter{
Tags: []*models.Tag{
{Name: "#tag3"},
},
})
quizStore.FilterInCollection(c, map[string]string{"tags": "#tag3"})
_, err = store.Create(c)

View file

@ -49,15 +49,8 @@ func (t *examTestSuite) TestCreate() {
g := new(models.Group)
c := new(models.Collection)
participants := participantStore.Storer.FilterInGroup(g, &models.ParticipantFilter{
Attributes: map[string]string{"class": "1 D LIN"},
})
quizzes := quizStore.Storer.FilterInCollection(c, &models.Filter{
Tags: []*models.Tag{
{Name: "#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)

View file

@ -19,7 +19,6 @@ func (t *groupTestSuite) TestCreate() {
participantStore.Create(
&models.Participant{
ID: "1234",
Firstname: "John",
Lastname: "Smith",
Token: 111222,
@ -28,7 +27,6 @@ func (t *groupTestSuite) TestCreate() {
participantStore.Create(
&models.Participant{
ID: "5678",
Firstname: "Jack",
Lastname: "Sparrow",
Token: 222333,
@ -42,9 +40,7 @@ func (t *groupTestSuite) TestCreate() {
g := new(models.Group)
g.Name = "Test Group"
participantStore.FilterInGroup(g, &models.ParticipantFilter{
Attributes: map[string]string{"class": "1 D LIN"},
})
participantStore.FilterInGroup(g, map[string]string{"class": "1 D LIN"})
_, err = groupStore.Create(g)
t.Nil(err)

View file

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

View file

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

View file

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

View file

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

View file

@ -13,16 +13,22 @@ func NewParticipantStore() *ParticipantStore {
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()
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.Attributes {
for fk, fv := range filter {
if pk == fk && pv == fv {
return true
}
}
}
return false
})

View file

@ -112,16 +112,29 @@ func (s *QuizStore) Update(quiz *models.Quiz, id string) (*models.Quiz, error) {
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()
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, filter) {
if s.isTagInFilter(qTag, fTags) {
count++
}
}
if count == len(filter.Tags) {
if count == len(fTags) {
return true
}
return false
@ -132,9 +145,9 @@ func (s *QuizStore) FilterInCollection(collection *models.Collection, filter *mo
return collection.Quizzes
}
func (s *QuizStore) isTagInFilter(tag *models.Tag, filter *models.Filter) bool {
for _, fTag := range filter.Tags {
if tag.Name == fTag.Name {
func (s *QuizStore) isTagInFilter(tag *models.Tag, fTags []string) bool {
for _, t := range fTags {
if tag.Name == strings.TrimSpace(t) {
return true
}
}

View file

@ -3,22 +3,21 @@ package store
import (
"fmt"
"sync"
"time"
"github.com/google/uuid"
)
type IDer interface {
type Storable interface {
GetHash() string
GetID() string
SetID(string)
}
type Hasher interface {
GetHash() string
}
type Storable interface {
IDer
Hasher
SetCreatedAt(t time.Time)
SetUpdatedAt(t time.Time)
GetCreatedAt() time.Time
GetUpdatedAt() time.Time
}
type Storer[T Storable] interface {
@ -90,6 +89,17 @@ func (s *Store[T]) Create(entity T) (T, error) {
}
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
return entity, nil
@ -137,6 +147,8 @@ func (s *Store[T]) Update(entity T, id string) (T, error) {
s.hashes[hash] = entity
}
entity.SetUpdatedAt(time.Now())
return entity, nil
}