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