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
|
||||
testdata
|
||||
|
||||
*.bk
|
||||
|
|
113
cli/main.go
113
cli/main.go
|
@ -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
|
||||
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
1
server/.gitignore
vendored
|
@ -1 +1,2 @@
|
|||
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.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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
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"
|
||||
"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
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
DefaultResponsesSubdir = "responses"
|
||||
DefaultSessionSubdir = "sessions"
|
||||
|
||||
Dirs = []string{
|
||||
GetDefaultQuizzesDir(),
|
||||
GetDefaultParticipantsDir(),
|
||||
GetDefaultExamsDir(),
|
||||
GetDefaultSessionDir(),
|
||||
}
|
||||
)
|
||||
|
||||
func GetDefaultQuizzesDir() string {
|
||||
|
|
|
@ -14,9 +14,7 @@ func TestRunner(t *testing.T) {
|
|||
prettytest.Run(
|
||||
t,
|
||||
new(quizTestSuite),
|
||||
new(collectionTestSuite),
|
||||
new(participantTestSuite),
|
||||
new(groupTestSuite),
|
||||
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