Something like an MVP
This commit is contained in:
parent
e05ea6dd25
commit
68b7efa585
24 changed files with 295 additions and 356 deletions
2
cli/.gitignore
vendored
2
cli/.gitignore
vendored
|
@ -1,3 +1,3 @@
|
||||||
cli
|
cli
|
||||||
testdata
|
testdata
|
||||||
|
*.bk
|
||||||
|
|
113
cli/main.go
113
cli/main.go
|
@ -1,10 +1,11 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
sessionmanager "git.andreafazzi.eu/andrea/probo/session"
|
"git.andreafazzi.eu/andrea/probo/sessionmanager"
|
||||||
"git.andreafazzi.eu/andrea/probo/store/file"
|
"git.andreafazzi.eu/andrea/probo/store/file"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
)
|
)
|
||||||
|
@ -12,10 +13,43 @@ import (
|
||||||
func main() {
|
func main() {
|
||||||
file.DefaultBaseDir = "testdata"
|
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{
|
app := &cli.App{
|
||||||
Name: "probo-cli",
|
Name: "probo-cli",
|
||||||
Usage: "Quiz Management System for Power Teachers!",
|
Usage: "Quiz Management System for Hackers!",
|
||||||
Commands: []*cli.Command{
|
Commands: []*cli.Command{
|
||||||
|
{
|
||||||
|
Name: "init",
|
||||||
|
Aliases: []string{"i"},
|
||||||
|
Usage: "Initialize the current directory",
|
||||||
|
Action: func(cCtx *cli.Context) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "session",
|
Name: "session",
|
||||||
Aliases: []string{"s"},
|
Aliases: []string{"s"},
|
||||||
|
@ -28,42 +62,77 @@ func main() {
|
||||||
if cCtx.Args().Len() < 1 {
|
if cCtx.Args().Len() < 1 {
|
||||||
log.Fatalf("Please provide a session name as first argument of create.")
|
log.Fatalf("Please provide a session name as first argument of create.")
|
||||||
}
|
}
|
||||||
pStore, err := file.NewParticipantDefaultFileStore()
|
|
||||||
if err != nil {
|
session, err := sm.Push(cCtx.Args().First())
|
||||||
log.Fatalf("An error occurred: %v", err)
|
|
||||||
}
|
|
||||||
qStore, err := file.NewDefaultQuizFileStore()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("An error occurred: %v", err)
|
log.Fatalf("An error occurred: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sm, err := sessionmanager.NewSessionManager(
|
_, err = sStore.Create(session)
|
||||||
"http://localhost:8080/create",
|
|
||||||
cCtx.Args().First(),
|
|
||||||
pStore.Storer,
|
|
||||||
qStore.Storer,
|
|
||||||
nil,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("An error occurred: %v", err)
|
log.Fatalf("An error occurred: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
id, err := sm.Push()
|
log.Println("Session upload completed with success!")
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("An error occurred: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("Session upload completed with success. URL: https://localhost:8080/%v", id)
|
|
||||||
|
|
||||||
for _, p := range pStore.ReadAll() {
|
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
|
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
|
||||||
|
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Exam struct {
|
type Exam struct {
|
||||||
Meta
|
Meta
|
||||||
|
|
||||||
|
SessionID string
|
||||||
Participant *Participant
|
Participant *Participant
|
||||||
Quizzes []*Quiz
|
Quizzes []*Quiz
|
||||||
}
|
}
|
||||||
|
@ -16,7 +20,11 @@ func (e *Exam) String() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Exam) GetHash() 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) {
|
func (e *Exam) Marshal() ([]byte, error) {
|
||||||
|
|
|
@ -6,9 +6,14 @@ type Meta struct {
|
||||||
ID string `json:"id" yaml:"id" gorm:"primaryKey"`
|
ID string `json:"id" yaml:"id" gorm:"primaryKey"`
|
||||||
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"`
|
||||||
|
|
||||||
|
UniqueIDFunc func() string `json:"-" yaml:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Meta) GetID() string {
|
func (m *Meta) GetID() string {
|
||||||
|
if m.UniqueIDFunc != nil {
|
||||||
|
return m.UniqueIDFunc()
|
||||||
|
}
|
||||||
return m.ID
|
return m.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,23 +1,23 @@
|
||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Response struct {
|
type Response struct {
|
||||||
Meta
|
Meta
|
||||||
QuestionID string
|
|
||||||
AnswerID string
|
Questions map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Response) 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 {
|
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) {
|
func (r *Response) Marshal() ([]byte, error) {
|
||||||
|
|
1
server/.gitignore
vendored
1
server/.gitignore
vendored
|
@ -1 +1,2 @@
|
||||||
server
|
server
|
||||||
|
data
|
||||||
|
|
|
@ -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}]}}}
|
|
|
@ -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.Handle("/static/", http.StripPrefix("/static", http.FileServer(http.Dir(config.StaticDir))))
|
||||||
s.mux.HandleFunc("/create", s.createExamSessionHandler)
|
s.mux.HandleFunc("/create", s.createExamSessionHandler)
|
||||||
|
s.mux.HandleFunc("/responses/", s.getResponsesHandler)
|
||||||
s.mux.HandleFunc("/", s.getExamHandler)
|
s.mux.HandleFunc("/", s.getExamHandler)
|
||||||
|
|
||||||
return s, nil
|
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) {
|
func (s *Server) createExamSessionHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
session := new(models.Session)
|
session := new(models.Session)
|
||||||
|
|
||||||
|
@ -154,8 +184,12 @@ func (s *Server) createExamSessionHandler(w http.ResponseWriter, r *http.Request
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response := map[string]string{"id": memorySession.ID}
|
err = json.NewEncoder(w).Encode(memorySession)
|
||||||
json.NewEncoder(w).Encode(response)
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) getExamHandler(w http.ResponseWriter, r *http.Request) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
parts := strings.Split(r.FormValue("answer"), "_")
|
response := new(models.Response)
|
||||||
_, err = s.responseFileStore.Create(&models.Response{
|
response.UniqueIDFunc = func() string {
|
||||||
QuestionID: parts[0],
|
return exam.GetID()
|
||||||
AnswerID: parts[1],
|
}
|
||||||
})
|
|
||||||
|
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 {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html")
|
w.Write([]byte("<p>Thank you for your response.</p>"))
|
||||||
if parts[1] == exam.Quizzes[0].Correct.ID {
|
|
||||||
w.Write([]byte("<p>Corretto!</p>"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Write([]byte("<p>Errato!</p>"))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,6 +273,6 @@ func main() {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("Probo server started.", "Config", server.config)
|
log.Println("Probo server started.")
|
||||||
http.ListenAndServe(":8080", server)
|
http.ListenAndServe(":8080", server)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"git.andreafazzi.eu/andrea/probo/models"
|
||||||
"github.com/remogatto/prettytest"
|
"github.com/remogatto/prettytest"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -215,9 +216,9 @@ func (t *serverTestSuite) TestCreate() {
|
||||||
t.Equal(http.StatusOK, response.Code)
|
t.Equal(http.StatusOK, response.Code)
|
||||||
|
|
||||||
if !t.Failed() {
|
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)
|
t.Nil(err)
|
||||||
|
|
||||||
path := filepath.Join(GetDefaultSessionDir(), "session_fe0a7ee0-f31a-413d-f123-ab5068bcaaaa.json")
|
path := filepath.Join(GetDefaultSessionDir(), "session_fe0a7ee0-f31a-413d-f123-ab5068bcaaaa.json")
|
||||||
|
@ -227,7 +228,7 @@ func (t *serverTestSuite) TestCreate() {
|
||||||
|
|
||||||
defer os.Remove(path)
|
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)
|
t.Equal(http.StatusOK, response.Code)
|
||||||
|
|
||||||
if !t.Failed() {
|
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)
|
t.Nil(err)
|
||||||
|
|
||||||
path := filepath.Join(GetDefaultSessionDir(), "session_fe0a7ee0-f31a-413d-f123-ab5068bcaaaa.json")
|
path := filepath.Join(GetDefaultSessionDir(), "session_fe0a7ee0-f31a-413d-f123-ab5068bcaaaa.json")
|
||||||
|
@ -263,7 +264,7 @@ func (t *serverTestSuite) TestRead() {
|
||||||
if !t.Failed() {
|
if !t.Failed() {
|
||||||
defer os.RemoveAll(path)
|
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()
|
response := httptest.NewRecorder()
|
||||||
|
|
||||||
handler := http.HandlerFunc(s.getExamHandler)
|
handler := http.HandlerFunc(s.getExamHandler)
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
<!-- <link rel="stylesheet" href="https://unpkg.com/mvp.css"> -->
|
||||||
<link rel="stylesheet" type="text/css" href="/static/css/neat.css">
|
<link rel="stylesheet" type="text/css" href="/static/css/neat.css">
|
||||||
<title>Exam</title>
|
<title>Exam</title>
|
||||||
</head>
|
</head>
|
||||||
|
@ -14,8 +15,8 @@
|
||||||
<p>{{$quiz.Question.Text}}</p>
|
<p>{{$quiz.Question.Text}}</p>
|
||||||
{{range $answer := $quiz.Answers}}
|
{{range $answer := $quiz.Answers}}
|
||||||
<input type="radio"
|
<input type="radio"
|
||||||
id="{{$answer.ID}}" name="answer"
|
id="{{$quiz.Question.ID}}_{{$answer.ID}}" name="{{$quiz.Question.ID}}"
|
||||||
value="{{$quiz.Question.ID}}_{{$answer.ID}}">
|
value="{{$answer.ID}}">
|
||||||
<label
|
<label
|
||||||
for="{{$answer.ID}}">{{$answer.Text}}</label><br>
|
for="{{$answer.ID}}">{{$answer.Text}}</label><br>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
52
sessionmanager/score.go
Normal file
52
sessionmanager/score.go
Normal 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}
|
||||||
|
}
|
|
@ -5,14 +5,14 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
"git.andreafazzi.eu/andrea/probo/models"
|
"git.andreafazzi.eu/andrea/probo/models"
|
||||||
"git.andreafazzi.eu/andrea/probo/store"
|
"git.andreafazzi.eu/andrea/probo/store"
|
||||||
|
"git.andreafazzi.eu/andrea/probo/store/file"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SessionManager struct {
|
type SessionManager struct {
|
||||||
Name string
|
|
||||||
|
|
||||||
ParticipantStore *store.ParticipantStore
|
ParticipantStore *store.ParticipantStore
|
||||||
QuizStore *store.QuizStore
|
QuizStore *store.QuizStore
|
||||||
|
|
||||||
|
@ -21,18 +21,23 @@ type SessionManager struct {
|
||||||
ServerURL string
|
ServerURL string
|
||||||
Token int
|
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 := new(SessionManager)
|
||||||
|
|
||||||
sm.Name = name
|
|
||||||
sm.ServerURL = url
|
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() {
|
for _, p := range pStore.ReadAll() {
|
||||||
_, err := sm.examStore.Create(&models.Exam{
|
_, err := sm.examFileStore.Create(&models.Exam{
|
||||||
Participant: p,
|
Participant: p,
|
||||||
Quizzes: qStore.ReadAll(),
|
Quizzes: qStore.ReadAll(),
|
||||||
})
|
})
|
||||||
|
@ -45,12 +50,45 @@ func NewSessionManager(url string, name string, pStore *store.ParticipantStore,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sm *SessionManager) GetExams() []*models.Exam {
|
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{
|
session := &models.Session{
|
||||||
Name: sm.Name,
|
Name: name,
|
||||||
Exams: make(map[string]*models.Exam),
|
Exams: make(map[string]*models.Exam),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,24 +98,28 @@ func (sm *SessionManager) Push() (string, error) {
|
||||||
|
|
||||||
payload, err := session.Marshal()
|
payload, err := session.Marshal()
|
||||||
if err != nil {
|
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 {
|
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)
|
responseBody, err := io.ReadAll(response.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
result := map[string]string{}
|
err = json.Unmarshal(responseBody, &session)
|
||||||
err = json.Unmarshal(responseBody, &result)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return result["id"], nil
|
return session, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
package store
|
|
||||||
|
|
||||||
import "git.andreafazzi.eu/andrea/probo/models"
|
|
||||||
|
|
||||||
type CollectionStore = Store[*models.Collection]
|
|
|
@ -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))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
|
@ -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))
|
|
||||||
}
|
|
|
@ -11,6 +11,13 @@ var (
|
||||||
DefaultExamsSubdir = "exams"
|
DefaultExamsSubdir = "exams"
|
||||||
DefaultResponsesSubdir = "responses"
|
DefaultResponsesSubdir = "responses"
|
||||||
DefaultSessionSubdir = "sessions"
|
DefaultSessionSubdir = "sessions"
|
||||||
|
|
||||||
|
Dirs = []string{
|
||||||
|
GetDefaultQuizzesDir(),
|
||||||
|
GetDefaultParticipantsDir(),
|
||||||
|
GetDefaultExamsDir(),
|
||||||
|
GetDefaultSessionDir(),
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetDefaultQuizzesDir() string {
|
func GetDefaultQuizzesDir() string {
|
||||||
|
|
|
@ -14,9 +14,7 @@ func TestRunner(t *testing.T) {
|
||||||
prettytest.Run(
|
prettytest.Run(
|
||||||
t,
|
t,
|
||||||
new(quizTestSuite),
|
new(quizTestSuite),
|
||||||
new(collectionTestSuite),
|
|
||||||
new(participantTestSuite),
|
new(participantTestSuite),
|
||||||
new(groupTestSuite),
|
|
||||||
new(examTestSuite),
|
new(examTestSuite),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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"}}
|
|
@ -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"}}
|
|
@ -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"}}
|
|
@ -1,5 +0,0 @@
|
||||||
package store
|
|
||||||
|
|
||||||
import "git.andreafazzi.eu/andrea/probo/models"
|
|
||||||
|
|
||||||
type GroupStore = Store[*models.Group]
|
|
Loading…
Reference in a new issue