Something like an MVP

This commit is contained in:
andrea 2023-12-17 18:56:20 +01:00
parent e05ea6dd25
commit 68b7efa585
24 changed files with 295 additions and 356 deletions

2
cli/.gitignore vendored
View file

@ -1,3 +1,3 @@
cli
testdata
*.bk

View file

@ -1,10 +1,11 @@
package main
import (
"fmt"
"log"
"os"
sessionmanager "git.andreafazzi.eu/andrea/probo/session"
"git.andreafazzi.eu/andrea/probo/sessionmanager"
"git.andreafazzi.eu/andrea/probo/store/file"
"github.com/urfave/cli/v2"
)
@ -12,10 +13,43 @@ import (
func main() {
file.DefaultBaseDir = "testdata"
sStore, err := file.NewDefaultSessionFileStore()
if err != nil {
log.Fatalf("An error occurred: %v", err)
}
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)
}
sm, err := sessionmanager.NewSessionManager(
"http://localhost:8080/",
pStore.Storer,
qStore.Storer,
nil,
nil,
)
if err != nil {
log.Fatalf("An error occurred: %v", err)
}
app := &cli.App{
Name: "probo-cli",
Usage: "Quiz Management System for Power Teachers!",
Usage: "Quiz Management System for Hackers!",
Commands: []*cli.Command{
{
Name: "init",
Aliases: []string{"i"},
Usage: "Initialize the current directory",
Action: func(cCtx *cli.Context) error {
return nil
},
},
{
Name: "session",
Aliases: []string{"s"},
@ -28,42 +62,77 @@ func main() {
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()
session, err := sm.Push(cCtx.Args().First())
if err != nil {
log.Fatalf("An error occurred: %v", err)
}
sm, err := sessionmanager.NewSessionManager(
"http://localhost:8080/create",
cCtx.Args().First(),
pStore.Storer,
qStore.Storer,
nil,
nil,
)
_, err = sStore.Create(session)
if err != nil {
log.Fatalf("An error occurred: %v", err)
}
id, err := sm.Push()
if err != nil {
log.Fatalf("An error occurred: %v", err)
}
log.Printf("Session upload completed with success. URL: https://localhost:8080/%v", id)
log.Println("Session upload completed with success!")
for _, p := range pStore.ReadAll() {
log.Printf("http://localhost:8080/%v/%v", id, p.Token)
log.Printf("http://localhost:8080/%v/%v", session.ID, p.Token)
}
return nil
},
},
{
Name: "pull",
Usage: "Download responses from a session",
Action: func(cCtx *cli.Context) error {
if cCtx.Args().Len() < 1 {
log.Fatalf("Please provide a session ID as first argument of pull.")
}
rStore, err := file.NewDefaultResponseFileStore()
if err != nil {
log.Fatalf("An error occurred: %v", err)
}
err = sm.Pull(rStore, cCtx.Args().First())
if err != nil {
log.Fatalf("An error occurred: %v", err)
}
log.Println("Responses download completed with success!")
return nil
},
},
{
Name: "scores",
Usage: "Show the scores for the given session",
Action: func(cCtx *cli.Context) error {
if cCtx.Args().Len() < 1 {
log.Fatalf("Please provide a session name as first argument of 'score'.")
}
session, err := sStore.Read(cCtx.Args().First())
if err != nil {
log.Fatalf("An error occurred: %v", err)
}
rStore, err := file.NewDefaultResponseFileStore()
if err != nil {
log.Fatalf("An error occurred: %v", err)
}
scores, err := sessionmanager.NewScores(rStore, session)
if err != nil {
log.Fatalf("An error occurred: %v", err)
}
fmt.Println(scores)
return nil
},
},
},
},
},

View file

@ -1,12 +1,16 @@
package models
import (
"crypto/sha256"
"encoding/json"
"fmt"
"strings"
)
type Exam struct {
Meta
SessionID string
Participant *Participant
Quizzes []*Quiz
}
@ -16,7 +20,11 @@ func (e *Exam) String() string {
}
func (e *Exam) GetHash() string {
return ""
qHashes := ""
for _, q := range e.Quizzes {
qHashes += q.GetHash()
}
return fmt.Sprintf("%x", sha256.Sum256([]byte(strings.Join([]string{e.Participant.GetHash(), qHashes}, ""))))
}
func (e *Exam) Marshal() ([]byte, error) {

View file

@ -6,9 +6,14 @@ type Meta struct {
ID string `json:"id" yaml:"id" gorm:"primaryKey"`
CreatedAt time.Time `json:"created_at" yaml:"created_at"`
UpdatedAt time.Time `json:"updated_at" yaml:"updated_at"`
UniqueIDFunc func() string `json:"-" yaml:"-"`
}
func (m *Meta) GetID() string {
if m.UniqueIDFunc != nil {
return m.UniqueIDFunc()
}
return m.ID
}

View file

@ -1,23 +1,23 @@
package models
import (
"crypto/sha256"
"encoding/json"
"fmt"
)
type Response struct {
Meta
QuestionID string
AnswerID string
Questions map[string]string
}
func (r *Response) String() string {
return fmt.Sprintf("QID: %v, AID:%v", r.QuestionID, r.AnswerID)
return fmt.Sprintf("Questions/Answers: %v", r.Questions)
}
func (r *Response) GetHash() string {
return fmt.Sprintf("%x", sha256.Sum256([]byte(r.QuestionID+r.AnswerID)))
// return fmt.Sprintf("%x", sha256.Sum256([]byte(r.QuestionID+r.AnswerID)))
return ""
}
func (r *Response) Marshal() ([]byte, error) {

1
server/.gitignore vendored
View file

@ -1 +1,2 @@
server
data

View file

@ -1 +0,0 @@
{"id":"24e5a5f5-8293-4e13-a825-656117db4d5b","created_at":"2023-12-12T09:00:57.405268351+01:00","updated_at":"2023-12-12T09:20:41.050431537+01:00","Name":"My session","Exams":{"111222":{"id":"c0140b05-a843-4599-b405-3517357bd2b5","created_at":"2023-12-12T09:00:57.402306603+01:00","updated_at":"2023-12-12T09:00:57.402306722+01:00","Participant":{"id":"1234","created_at":"2023-12-05T21:11:37.566330708+01:00","updated_at":"2023-12-12T09:00:57.402100526+01:00","Firstname":"Giuly","Lastname":"Mon Amour","Token":"111222","Attributes":{"class":"1 D LIN"}},"Quizzes":[{"id":"908bb025-6370-4273-8448-8470f9336f26","created_at":"2023-12-05T21:22:06.322398595+01:00","updated_at":"2023-12-12T09:00:57.40230252+01:00","hash":"","question":{"id":"8e963b00-477a-4a1a-bf22-d74b931f9058","created_at":"2023-12-12T09:00:57.402266804+01:00","updated_at":"2023-12-12T09:00:57.402266953+01:00","text":"Chi è l'#amore più grande della mia vita?"},"answers":[{"id":"df78ba57-b53b-411d-a8fc-ef57bce961d2","created_at":"2023-12-12T09:00:57.402274057+01:00","updated_at":"2023-12-12T09:00:57.40227419+01:00","text":"Giuly (❤️)"},{"id":"eb89e7fa-0909-4f84-bcbb-5fe89a015757","created_at":"2023-12-12T09:00:57.402276551+01:00","updated_at":"2023-12-12T09:00:57.402276604+01:00","text":"Daisy"},{"id":"3185a66b-5fce-4e22-bcd0-6dc06f2d4261","created_at":"2023-12-12T09:00:57.402278951+01:00","updated_at":"2023-12-12T09:00:57.402279003+01:00","text":"Jenny"},{"id":"49bbe2ca-9408-40e9-8625-442b27b32026","created_at":"2023-12-12T09:00:57.402281071+01:00","updated_at":"2023-12-12T09:00:57.402281125+01:00","text":"Lilly"}],"tags":[],"correct":{"id":"df78ba57-b53b-411d-a8fc-ef57bce961d2","created_at":"2023-12-12T09:00:57.402274057+01:00","updated_at":"2023-12-12T09:00:57.40227419+01:00","text":"Giuly (❤️)"},"CorrectPos":0,"type":0}]},"333222":{"id":"5b6e3f92-1a6f-4028-b618-ad434a1f6e16","created_at":"2023-12-12T09:00:57.402308673+01:00","updated_at":"2023-12-12T09:00:57.402308733+01:00","Participant":{"id":"564d0c7f-4840-443c-8325-ecd02d03624d","created_at":"2023-12-05T21:11:37.566330708+01:00","updated_at":"2023-12-12T09:00:57.401968656+01:00","Firstname":"Andrea","Lastname":"Fazzi","Token":"333222","Attributes":{"class":"1 D LIN"}},"Quizzes":[{"id":"908bb025-6370-4273-8448-8470f9336f26","created_at":"2023-12-05T21:22:06.322398595+01:00","updated_at":"2023-12-12T09:00:57.40230252+01:00","hash":"","question":{"id":"8e963b00-477a-4a1a-bf22-d74b931f9058","created_at":"2023-12-12T09:00:57.402266804+01:00","updated_at":"2023-12-12T09:00:57.402266953+01:00","text":"Chi è l'#amore più grande della mia vita?"},"answers":[{"id":"df78ba57-b53b-411d-a8fc-ef57bce961d2","created_at":"2023-12-12T09:00:57.402274057+01:00","updated_at":"2023-12-12T09:00:57.40227419+01:00","text":"Giuly (❤️)"},{"id":"eb89e7fa-0909-4f84-bcbb-5fe89a015757","created_at":"2023-12-12T09:00:57.402276551+01:00","updated_at":"2023-12-12T09:00:57.402276604+01:00","text":"Daisy"},{"id":"3185a66b-5fce-4e22-bcd0-6dc06f2d4261","created_at":"2023-12-12T09:00:57.402278951+01:00","updated_at":"2023-12-12T09:00:57.402279003+01:00","text":"Jenny"},{"id":"49bbe2ca-9408-40e9-8625-442b27b32026","created_at":"2023-12-12T09:00:57.402281071+01:00","updated_at":"2023-12-12T09:00:57.402281125+01:00","text":"Lilly"}],"tags":[],"correct":{"id":"df78ba57-b53b-411d-a8fc-ef57bce961d2","created_at":"2023-12-12T09:00:57.402274057+01:00","updated_at":"2023-12-12T09:00:57.40227419+01:00","text":"Giuly (❤️)"},"CorrectPos":0,"type":0}]}}}

View file

@ -119,6 +119,7 @@ func NewServer(config *Config) (*Server, error) {
s.mux.Handle("/static/", http.StripPrefix("/static", http.FileServer(http.Dir(config.StaticDir))))
s.mux.HandleFunc("/create", s.createExamSessionHandler)
s.mux.HandleFunc("/responses/", s.getResponsesHandler)
s.mux.HandleFunc("/", s.getExamHandler)
return s, nil
@ -133,6 +134,35 @@ func NewDefaultServer() (*Server, error) {
})
}
func (s *Server) getResponsesHandler(w http.ResponseWriter, r *http.Request) {
result := make([]*models.Response, 0)
urlParts := strings.Split(r.URL.Path, "/")
sessionID := urlParts[2]
if r.Method == "GET" {
session, err := s.sessionFileStore.Read(sessionID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
for _, exam := range session.Exams {
responses := s.responseFileStore.ReadAll()
for _, r := range responses {
if r.ID == exam.ID {
result = append(result, r)
}
}
}
err = json.NewEncoder(w).Encode(result)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}
func (s *Server) createExamSessionHandler(w http.ResponseWriter, r *http.Request) {
session := new(models.Session)
@ -154,8 +184,12 @@ func (s *Server) createExamSessionHandler(w http.ResponseWriter, r *http.Request
return
}
response := map[string]string{"id": memorySession.ID}
json.NewEncoder(w).Encode(response)
err = json.NewEncoder(w).Encode(memorySession)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func (s *Server) getExamHandler(w http.ResponseWriter, r *http.Request) {
@ -197,22 +231,25 @@ func (s *Server) getExamHandler(w http.ResponseWriter, r *http.Request) {
return
}
parts := strings.Split(r.FormValue("answer"), "_")
_, err = s.responseFileStore.Create(&models.Response{
QuestionID: parts[0],
AnswerID: parts[1],
})
response := new(models.Response)
response.UniqueIDFunc = func() string {
return exam.GetID()
}
response.Questions = make(map[string]string)
for qID, values := range r.Form {
for _, aID := range values {
response.Questions[qID] = aID
}
}
_, err = s.responseFileStore.Create(response)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "text/html")
if parts[1] == exam.Quizzes[0].Correct.ID {
w.Write([]byte("<p>Corretto!</p>"))
return
}
w.Write([]byte("<p>Errato!</p>"))
w.Write([]byte("<p>Thank you for your response.</p>"))
return
}
@ -236,6 +273,6 @@ func main() {
panic(err)
}
log.Println("Probo server started.", "Config", server.config)
log.Println("Probo server started.")
http.ListenAndServe(":8080", server)
}

View file

@ -10,6 +10,7 @@ import (
"strings"
"testing"
"git.andreafazzi.eu/andrea/probo/models"
"github.com/remogatto/prettytest"
)
@ -215,9 +216,9 @@ func (t *serverTestSuite) TestCreate() {
t.Equal(http.StatusOK, response.Code)
if !t.Failed() {
result := map[string]string{}
var session *models.Session
err := json.Unmarshal(response.Body.Bytes(), &result)
err := json.Unmarshal(response.Body.Bytes(), &session)
t.Nil(err)
path := filepath.Join(GetDefaultSessionDir(), "session_fe0a7ee0-f31a-413d-f123-ab5068bcaaaa.json")
@ -227,7 +228,7 @@ func (t *serverTestSuite) TestCreate() {
defer os.Remove(path)
t.Equal("fe0a7ee0-f31a-413d-f123-ab5068bcaaaa", result["id"])
t.Equal("fe0a7ee0-f31a-413d-f123-ab5068bcaaaa", session.ID)
}
}
@ -251,9 +252,9 @@ func (t *serverTestSuite) TestRead() {
t.Equal(http.StatusOK, response.Code)
if !t.Failed() {
result := map[string]string{}
var session *models.Session
err := json.Unmarshal(response.Body.Bytes(), &result)
err := json.Unmarshal(response.Body.Bytes(), &session)
t.Nil(err)
path := filepath.Join(GetDefaultSessionDir(), "session_fe0a7ee0-f31a-413d-f123-ab5068bcaaaa.json")
@ -263,7 +264,7 @@ func (t *serverTestSuite) TestRead() {
if !t.Failed() {
defer os.RemoveAll(path)
request, _ := http.NewRequest(http.MethodGet, fmt.Sprintf("/%s/%s", result["id"], "111222"), nil)
request, _ := http.NewRequest(http.MethodGet, fmt.Sprintf("/%s/%s", session.ID, "111222"), nil)
response := httptest.NewRecorder()
handler := http.HandlerFunc(s.getExamHandler)

View file

@ -2,6 +2,7 @@
<html>
<head>
<meta charset="UTF-8">
<!-- <link rel="stylesheet" href="https://unpkg.com/mvp.css"> -->
<link rel="stylesheet" type="text/css" href="/static/css/neat.css">
<title>Exam</title>
</head>
@ -14,8 +15,8 @@
<p>{{$quiz.Question.Text}}</p>
{{range $answer := $quiz.Answers}}
<input type="radio"
id="{{$answer.ID}}" name="answer"
value="{{$quiz.Question.ID}}_{{$answer.ID}}">
id="{{$quiz.Question.ID}}_{{$answer.ID}}" name="{{$quiz.Question.ID}}"
value="{{$answer.ID}}">
<label
for="{{$answer.ID}}">{{$answer.Text}}</label><br>
{{end}}

52
sessionmanager/score.go Normal file
View file

@ -0,0 +1,52 @@
package sessionmanager
import (
"fmt"
"log"
"git.andreafazzi.eu/andrea/probo/models"
"git.andreafazzi.eu/andrea/probo/store/file"
)
type Score struct {
Exam *models.Exam
Score float32
}
type Scores []*Score
func NewScores(responseFileStore *file.ResponseFileStore, session *models.Session) (Scores, error) {
var scores Scores
for _, exam := range session.Exams {
response, err := responseFileStore.Read(exam.ID)
if err != nil {
log.Println(err)
continue
}
scores = append(scores, CalcScore(response, exam))
}
return scores, nil
}
func (ss Scores) String() string {
var result string
for _, s := range ss {
result += fmt.Sprintf("%v\t%f\n", s.Exam.Participant, s.Score)
}
return result
}
func CalcScore(response *models.Response, exam *models.Exam) *Score {
var score float32
for _, q := range exam.Quizzes {
answerId := response.Questions[q.Question.ID]
if answerId == q.Correct.ID {
score++
}
}
return &Score{exam, score}
}

View file

@ -5,14 +5,14 @@ import (
"encoding/json"
"io"
"net/http"
"net/url"
"git.andreafazzi.eu/andrea/probo/models"
"git.andreafazzi.eu/andrea/probo/store"
"git.andreafazzi.eu/andrea/probo/store/file"
)
type SessionManager struct {
Name string
ParticipantStore *store.ParticipantStore
QuizStore *store.QuizStore
@ -21,18 +21,23 @@ type SessionManager struct {
ServerURL string
Token int
examStore *store.ExamStore
examFileStore *file.ExamFileStore
}
func NewSessionManager(url string, name string, pStore *store.ParticipantStore, qStore *store.QuizStore, pFilter map[string]string, qFilter map[string]string) (*SessionManager, error) {
func NewSessionManager(url string, pStore *store.ParticipantStore, qStore *store.QuizStore, pFilter map[string]string, qFilter map[string]string) (*SessionManager, error) {
sm := new(SessionManager)
sm.Name = name
sm.ServerURL = url
sm.examStore = store.NewStore[*models.Exam]()
var err error
sm.examFileStore, err = file.NewDefaultExamFileStore()
if err != nil {
return nil, err
}
for _, p := range pStore.ReadAll() {
_, err := sm.examStore.Create(&models.Exam{
_, err := sm.examFileStore.Create(&models.Exam{
Participant: p,
Quizzes: qStore.ReadAll(),
})
@ -45,12 +50,45 @@ func NewSessionManager(url string, name string, pStore *store.ParticipantStore,
}
func (sm *SessionManager) GetExams() []*models.Exam {
return sm.examStore.ReadAll()
return sm.examFileStore.ReadAll()
}
func (sm *SessionManager) Push() (string, error) {
func (sm *SessionManager) Pull(rStore *file.ResponseFileStore, sessionID string) error {
url, err := url.JoinPath(sm.ServerURL, "responses/", sessionID)
if err != nil {
return err
}
rr, err := http.Get(url)
if err != nil {
return err
}
responseBody, err := io.ReadAll(rr.Body)
if err != nil {
return err
}
responses := make([]*models.Response, 0)
err = json.Unmarshal(responseBody, &responses)
if err != nil {
return err
}
for _, response := range responses {
_, err := rStore.Create(response)
if err != nil {
return err
}
}
return nil
}
func (sm *SessionManager) Push(name string) (*models.Session, error) {
session := &models.Session{
Name: sm.Name,
Name: name,
Exams: make(map[string]*models.Exam),
}
@ -60,24 +98,28 @@ func (sm *SessionManager) Push() (string, error) {
payload, err := session.Marshal()
if err != nil {
return "", err
return nil, err
}
response, err := http.Post(sm.ServerURL, "application/json", bytes.NewReader(payload))
url, err := url.JoinPath(sm.ServerURL, "create")
if err != nil {
return "", err
return nil, err
}
response, err := http.Post(url, "application/json", bytes.NewReader(payload))
if err != nil {
return nil, err
}
responseBody, err := io.ReadAll(response.Body)
if err != nil {
return "", err
return nil, err
}
result := map[string]string{}
err = json.Unmarshal(responseBody, &result)
err = json.Unmarshal(responseBody, &session)
if err != nil {
return "", err
return nil, err
}
return result["id"], nil
return session, nil
}

View file

@ -1,5 +0,0 @@
package store
import "git.andreafazzi.eu/andrea/probo/models"
type CollectionStore = Store[*models.Collection]

View file

@ -1,72 +0,0 @@
package store
import (
"git.andreafazzi.eu/andrea/probo/models"
"github.com/remogatto/prettytest"
)
type collectionTestSuite struct {
prettytest.Suite
}
func (t *collectionTestSuite) TestCreateCollection() {
quizStore := NewQuizStore()
quiz_1, _ := quizStore.Create(
&models.Quiz{
Question: &models.Question{Text: "Question text #tag1 #tag3."},
Answers: []*models.Answer{
{Text: "Answer 1"},
{Text: "Answer 2"},
{Text: "Answer 3"},
{Text: "Answer 4"},
},
})
quizStore.Create(
&models.Quiz{
Question: &models.Question{Text: "Question text #tag2."},
Answers: []*models.Answer{
{Text: "Answer 1"},
{Text: "Answer 2"},
{Text: "Answer 3"},
{Text: "Answer 4"},
},
})
quiz_2, _ := quizStore.Create(
&models.Quiz{
Question: &models.Question{Text: "Question text #tag3."},
Answers: []*models.Answer{
{Text: "Answer 1"},
{Text: "Answer 2"},
{Text: "Answer 3"},
{Text: "Answer 4"},
},
})
collectionStore := NewStore[*models.Collection]()
collection, err := collectionStore.Create(
&models.Collection{
Name: "My Collection",
})
t.Nil(err, "Collection should be created without error")
if !t.Failed() {
quizzes := quizStore.FilterInCollection(collection, map[string]string{
"tags": "#tag1,#tag3",
})
t.Equal(1, len(quizzes))
count := 0
for _, q := range collection.Quizzes {
if quiz_1.ID == q.ID || quiz_2.ID == q.ID {
count++
}
}
t.Equal(1, count)
t.Equal(1, len(collection.Quizzes))
}
}

View file

@ -1,27 +0,0 @@
package file
import (
"git.andreafazzi.eu/andrea/probo/models"
"git.andreafazzi.eu/andrea/probo/store"
)
type CollectionFileStore = FileStore[*models.Collection, *store.Store[*models.Collection]]
func NewCollectionFileStore(config *FileStoreConfig[*models.Collection, *store.CollectionStore]) (*CollectionFileStore, error) {
return NewFileStore[*models.Collection](config, store.NewStore[*models.Collection]())
}
func NewDefaultCollectionFileStore() (*CollectionFileStore, error) {
return NewCollectionFileStore(
&FileStoreConfig[*models.Collection, *store.CollectionStore]{
FilePathConfig: FilePathConfig{GetDefaultCollectionsDir(), "collection", ".json"},
IndexDirFunc: DefaultIndexDirFunc[*models.Collection, *store.CollectionStore],
CreateEntityFunc: func() *models.Collection {
return &models.Collection{
Quizzes: make([]*models.Quiz, 0),
}
},
},
)
}

View file

@ -1,68 +0,0 @@
package file
import (
"os"
"git.andreafazzi.eu/andrea/probo/models"
"git.andreafazzi.eu/andrea/probo/store"
"github.com/remogatto/prettytest"
)
type collectionTestSuite struct {
prettytest.Suite
}
func (t *collectionTestSuite) TestCreateCollection() {
quizStore := store.NewQuizStore()
quizStore.Create(
&models.Quiz{
Question: &models.Question{Text: "Question text #tag1 #tag3."},
Answers: []*models.Answer{
{Text: "Answer 1"},
{Text: "Answer 2"},
{Text: "Answer 3"},
{Text: "Answer 4"},
},
})
quizStore.Create(
&models.Quiz{
Question: &models.Question{Text: "Question text #tag2."},
Answers: []*models.Answer{
{Text: "Answer 1"},
{Text: "Answer 2"},
{Text: "Answer 3"},
{Text: "Answer 4"},
},
})
quizStore.Create(
&models.Quiz{
Question: &models.Question{Text: "Question text #tag3."},
Answers: []*models.Answer{
{Text: "Answer 1"},
{Text: "Answer 2"},
{Text: "Answer 3"},
{Text: "Answer 4"},
},
})
store, err := NewDefaultCollectionFileStore()
t.Nil(err)
c := new(models.Collection)
c.Name = "MyCollection"
quizStore.FilterInCollection(c, map[string]string{"tags": "#tag3"})
_, err = store.Create(c)
exists, err := os.Stat(store.GetPath(c))
t.Nil(err)
t.Not(t.Nil(exists))
t.Equal(2, len(c.Quizzes))
defer os.Remove(store.GetPath(c))
}

View file

@ -11,6 +11,13 @@ var (
DefaultExamsSubdir = "exams"
DefaultResponsesSubdir = "responses"
DefaultSessionSubdir = "sessions"
Dirs = []string{
GetDefaultQuizzesDir(),
GetDefaultParticipantsDir(),
GetDefaultExamsDir(),
GetDefaultSessionDir(),
}
)
func GetDefaultQuizzesDir() string {

View file

@ -14,9 +14,7 @@ func TestRunner(t *testing.T) {
prettytest.Run(
t,
new(quizTestSuite),
new(collectionTestSuite),
new(participantTestSuite),
new(groupTestSuite),
new(examTestSuite),
)
}

View file

@ -1,27 +0,0 @@
package file
import (
"git.andreafazzi.eu/andrea/probo/models"
"git.andreafazzi.eu/andrea/probo/store"
)
type GroupFileStore = FileStore[*models.Group, *store.Store[*models.Group]]
func NewGroupFileStore(config *FileStoreConfig[*models.Group, *store.GroupStore]) (*GroupFileStore, error) {
return NewFileStore[*models.Group](config, store.NewStore[*models.Group]())
}
func NewDefaultGroupFileStore() (*GroupFileStore, error) {
return NewGroupFileStore(
&FileStoreConfig[*models.Group, *store.GroupStore]{
FilePathConfig: FilePathConfig{GetDefaultGroupsDir(), "group", ".csv"},
IndexDirFunc: DefaultIndexDirFunc[*models.Group, *store.GroupStore],
CreateEntityFunc: func() *models.Group {
return &models.Group{
Participants: make([]*models.Participant, 0),
}
},
},
)
}

View file

@ -1,77 +0,0 @@
package file
import (
"fmt"
"os"
"git.andreafazzi.eu/andrea/probo/models"
"git.andreafazzi.eu/andrea/probo/store"
"github.com/gocarina/gocsv"
"github.com/remogatto/prettytest"
)
type groupTestSuite struct {
prettytest.Suite
}
func (t *groupTestSuite) TestCreate() {
participantStore := store.NewParticipantStore()
participantStore.Create(
&models.Participant{
Firstname: "John",
Lastname: "Smith",
Token: 111222,
Attributes: models.AttributeList{"class": "1 D LIN"},
})
participantStore.Create(
&models.Participant{
Firstname: "Jack",
Lastname: "Sparrow",
Token: 222333,
Attributes: models.AttributeList{"class": "2 D LIN"},
})
groupStore, err := NewDefaultGroupFileStore()
t.Nil(err)
if !t.Failed() {
g := new(models.Group)
g.Name = "Test Group"
participantStore.FilterInGroup(g, map[string]string{"class": "1 D LIN"})
_, err = groupStore.Create(g)
t.Nil(err)
defer os.Remove(groupStore.GetPath(g))
participantsFromDisk, err := readGroupFromCSV(g.GetID())
t.Nil(err)
if !t.Failed() {
t.Equal("Smith", participantsFromDisk[0].Lastname)
}
}
}
func readGroupFromCSV(groupID string) ([]*models.Participant, error) {
// Build the path to the CSV file
csvPath := fmt.Sprintf("testdata/groups/group_%s.csv", groupID)
// Open the CSV file
file, err := os.Open(csvPath)
if err != nil {
return nil, fmt.Errorf("failed to open CSV file: %w", err)
}
defer file.Close()
// Parse the CSV file
var participants []*models.Participant
if err := gocsv.UnmarshalFile(file, &participants); err != nil {
return nil, fmt.Errorf("failed to parse CSV file: %w", err)
}
return participants, nil
}

View file

@ -1 +1 @@
{"id":"5467","created_at":"2023-12-05T22:00:51.525533451+01:00","updated_at":"2023-12-11T17:20:07.682915159+01:00","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-17T18:54:31.169024304+01:00","Firstname":"Jack","Lastname":"Sparrow","Token":"333444","Attributes":{"class":"2 D LIN"}}

View file

@ -1 +1 @@
{"id":"1234","created_at":"2023-12-05T22:00:51.525601298+01:00","updated_at":"2023-12-11T17:20:07.682995386+01:00","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-17T18:54:31.169085845+01:00","Firstname":"John","Lastname":"Smith","Token":"111222","Attributes":{"class":"1 D LIN"}}

View file

@ -1 +1 @@
{"id":"567812","created_at":"2023-12-05T22:00:51.525667963+01:00","updated_at":"2023-12-11T17:20:07.683058985+01:00","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-17T18:54:31.169144803+01:00","Firstname":"Wendy","Lastname":"Darling","Token":"333444","Attributes":{"class":"2 D LIN"}}

View file

@ -1,5 +0,0 @@
package store
import "git.andreafazzi.eu/andrea/probo/models"
type GroupStore = Store[*models.Group]