First implementation of Group
This commit is contained in:
parent
ac95b38fe8
commit
4045a9c705
25 changed files with 413 additions and 975 deletions
|
@ -2,10 +2,6 @@ package models
|
||||||
|
|
||||||
import "encoding/json"
|
import "encoding/json"
|
||||||
|
|
||||||
type Filter struct {
|
|
||||||
Tags []*Tag
|
|
||||||
}
|
|
||||||
|
|
||||||
type Collection struct {
|
type Collection struct {
|
||||||
Meta
|
Meta
|
||||||
|
|
||||||
|
@ -39,3 +35,7 @@ func (c *Collection) Marshal() ([]byte, error) {
|
||||||
func (c *Collection) Unmarshal(data []byte) error {
|
func (c *Collection) Unmarshal(data []byte) error {
|
||||||
return json.Unmarshal(data, c)
|
return json.Unmarshal(data, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Collection) Create() *Collection {
|
||||||
|
return &Collection{}
|
||||||
|
}
|
||||||
|
|
9
models/filters.go
Normal file
9
models/filters.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
type Filter struct {
|
||||||
|
Tags []*Tag
|
||||||
|
}
|
||||||
|
|
||||||
|
type ParticipantFilter struct {
|
||||||
|
Attributes map[string]string
|
||||||
|
}
|
|
@ -1,6 +1,35 @@
|
||||||
package models
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gocarina/gocsv"
|
||||||
|
)
|
||||||
|
|
||||||
type Group struct {
|
type Group struct {
|
||||||
|
Meta
|
||||||
Name string
|
Name string
|
||||||
Participants []*Participant
|
Participants []*Participant
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) Marshal() ([]byte, error) {
|
||||||
|
return gocsv.MarshalBytes(g.Participants)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) Unmarshal(data []byte) error {
|
||||||
|
return gocsv.UnmarshalBytes(data, g.Participants)
|
||||||
|
}
|
||||||
|
|
29
models/group_test.go
Normal file
29
models/group_test.go
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/remogatto/prettytest"
|
||||||
|
)
|
||||||
|
|
||||||
|
type groupTestSuite struct {
|
||||||
|
prettytest.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
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"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
`
|
||||||
|
|
||||||
|
csv, err := group.Marshal()
|
||||||
|
|
||||||
|
t.Nil(err)
|
||||||
|
t.Equal(expected, string(csv))
|
||||||
|
}
|
|
@ -16,6 +16,7 @@ func TestRunner(t *testing.T) {
|
||||||
prettytest.Run(
|
prettytest.Run(
|
||||||
t,
|
t,
|
||||||
new(testSuite),
|
new(testSuite),
|
||||||
|
new(groupTestSuite),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,9 +4,12 @@ import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type AttributeList map[string]string
|
||||||
|
|
||||||
type Participant struct {
|
type Participant struct {
|
||||||
ID string `csv:"id" gorm:"primaryKey"`
|
ID string `csv:"id" gorm:"primaryKey"`
|
||||||
|
|
||||||
|
@ -15,7 +18,7 @@ type Participant struct {
|
||||||
|
|
||||||
Token uint `csv:"token"`
|
Token uint `csv:"token"`
|
||||||
|
|
||||||
Attributes map[string]string
|
Attributes AttributeList `csv:"attributes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Participant) String() string {
|
func (p *Participant) String() string {
|
||||||
|
@ -52,3 +55,27 @@ func (p *Participant) Marshal() ([]byte, error) {
|
||||||
func (p *Participant) Unmarshal(data []byte) error {
|
func (p *Participant) Unmarshal(data []byte) error {
|
||||||
return json.Unmarshal(data, p)
|
return json.Unmarshal(data, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (al AttributeList) MarshalCSV() (string, error) {
|
||||||
|
result := convertMapToKeyValueOrderedString(al)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertMapToKeyValueOrderedString(m map[string]string) string {
|
||||||
|
keys := make([]string, 0, len(m))
|
||||||
|
for key := range m {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
var result strings.Builder
|
||||||
|
for _, key := range keys {
|
||||||
|
result.WriteString(key)
|
||||||
|
result.WriteString(":")
|
||||||
|
result.WriteString(m[key])
|
||||||
|
result.WriteString(",")
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.TrimSuffix(result.String(), ",")
|
||||||
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ func NewCollectionFileStore(config *FileStoreConfig[*models.Collection, *store.C
|
||||||
func NewDefaultCollectionFileStore() (*CollectionFileStore, error) {
|
func NewDefaultCollectionFileStore() (*CollectionFileStore, error) {
|
||||||
return NewCollectionFileStore(
|
return NewCollectionFileStore(
|
||||||
&FileStoreConfig[*models.Collection, *store.CollectionStore]{
|
&FileStoreConfig[*models.Collection, *store.CollectionStore]{
|
||||||
FilePathConfig: FilePathConfig{DefaultBaseDir, "collection", ".json"},
|
FilePathConfig: FilePathConfig{GetDefaultCollectionsDir(), "collection", ".json"},
|
||||||
IndexDirFunc: DefaultIndexDirFunc[*models.Collection, *store.CollectionStore],
|
IndexDirFunc: DefaultIndexDirFunc[*models.Collection, *store.CollectionStore],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,6 +7,7 @@ var (
|
||||||
DefaultQuizzesSubdir = "quizzes"
|
DefaultQuizzesSubdir = "quizzes"
|
||||||
DefaultCollectionsSubdir = "collections"
|
DefaultCollectionsSubdir = "collections"
|
||||||
DefaultParticipantsSubdir = "participants"
|
DefaultParticipantsSubdir = "participants"
|
||||||
|
DefaultGroupsSubdir = "groups"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetDefaultQuizzesDir() string {
|
func GetDefaultQuizzesDir() string {
|
||||||
|
@ -20,3 +21,7 @@ func GetDefaultCollectionsDir() string {
|
||||||
func GetDefaultParticipantsDir() string {
|
func GetDefaultParticipantsDir() string {
|
||||||
return filepath.Join(DefaultBaseDir, DefaultParticipantsSubdir)
|
return filepath.Join(DefaultBaseDir, DefaultParticipantsSubdir)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetDefaultGroupsDir() string {
|
||||||
|
return filepath.Join(DefaultBaseDir, DefaultGroupsSubdir)
|
||||||
|
}
|
||||||
|
|
|
@ -23,6 +23,8 @@ type FileStorable interface {
|
||||||
|
|
||||||
Marshal() ([]byte, error)
|
Marshal() ([]byte, error)
|
||||||
Unmarshal([]byte) error
|
Unmarshal([]byte) error
|
||||||
|
|
||||||
|
Create() FileStorable
|
||||||
}
|
}
|
||||||
|
|
||||||
type Storer[T store.Storable] interface {
|
type Storer[T store.Storable] interface {
|
||||||
|
|
|
@ -16,5 +16,6 @@ func TestRunner(t *testing.T) {
|
||||||
new(quizTestSuite),
|
new(quizTestSuite),
|
||||||
new(collectionTestSuite),
|
new(collectionTestSuite),
|
||||||
new(participantTestSuite),
|
new(participantTestSuite),
|
||||||
|
new(groupTestSuite),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
22
store/file/group.go
Normal file
22
store/file/group.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
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],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
80
store/file/group_test.go
Normal file
80
store/file/group_test.go
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
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{
|
||||||
|
ID: "1234",
|
||||||
|
Firstname: "John",
|
||||||
|
Lastname: "Smith",
|
||||||
|
Token: 111222,
|
||||||
|
Attributes: models.AttributeList{"class": "1 D LIN"},
|
||||||
|
})
|
||||||
|
|
||||||
|
participantStore.Create(
|
||||||
|
&models.Participant{
|
||||||
|
ID: "5678",
|
||||||
|
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, &models.ParticipantFilter{
|
||||||
|
Attributes: 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
|
||||||
|
}
|
|
@ -5,8 +5,17 @@ import (
|
||||||
"git.andreafazzi.eu/andrea/probo/store"
|
"git.andreafazzi.eu/andrea/probo/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ParticipantFileStore = FileStore[*models.Participant, *store.Store[*models.Participant]]
|
type ParticipantFileStore = FileStore[*models.Participant, *store.ParticipantStore]
|
||||||
|
|
||||||
func NewParticipantFileStore(config *FileStoreConfig[*models.Participant, *store.Store[*models.Participant]]) (*ParticipantFileStore, error) {
|
func NewParticipantFileStore(config *FileStoreConfig[*models.Participant, *store.ParticipantStore]) (*ParticipantFileStore, error) {
|
||||||
return NewFileStore[*models.Participant](config, store.NewStore[*models.Participant]())
|
return NewFileStore[*models.Participant, *store.ParticipantStore](config, store.NewParticipantStore())
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewParticipantDefaultFileStore() (*ParticipantFileStore, error) {
|
||||||
|
return NewParticipantFileStore(
|
||||||
|
&FileStoreConfig[*models.Participant, *store.ParticipantStore]{
|
||||||
|
FilePathConfig: FilePathConfig{GetDefaultParticipantsDir(), "participant", ".json"},
|
||||||
|
IndexDirFunc: DefaultIndexDirFunc[*models.Participant, *store.ParticipantStore],
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,7 +65,7 @@ func DefaultQuizIndexDirFunc(s *QuizFileStore) error {
|
||||||
|
|
||||||
var errQuizAlreadyPresent *store.ErrQuizAlreadyPresent
|
var errQuizAlreadyPresent *store.ErrQuizAlreadyPresent
|
||||||
|
|
||||||
mEntity, err := s.Create(entity)
|
mEntity, err := s.Storer.Create(entity)
|
||||||
if err != nil && !errors.As(err, &errQuizAlreadyPresent) {
|
if err != nil && !errors.As(err, &errQuizAlreadyPresent) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,8 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"git.andreafazzi.eu/andrea/probo/models"
|
||||||
|
"git.andreafazzi.eu/andrea/probo/store"
|
||||||
"github.com/remogatto/prettytest"
|
"github.com/remogatto/prettytest"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -35,167 +37,184 @@ func (t *quizTestSuite) TestReadAll() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// func (t *quizTestSuite) TestCreate() {
|
func (t *quizTestSuite) TestCreate() {
|
||||||
// store, err := NewDefaultQuizFileStore()
|
store, err := NewQuizFileStore(
|
||||||
// t.Nil(err)
|
&FileStoreConfig[*models.Quiz, *store.QuizStore]{
|
||||||
|
FilePathConfig: FilePathConfig{GetDefaultQuizzesDir(), "quiz", ".md"},
|
||||||
|
IndexDirFunc: DefaultQuizIndexDirFunc,
|
||||||
|
NoIndexOnCreate: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
t.Nil(err)
|
||||||
|
|
||||||
// if !t.Failed() {
|
if !t.Failed() {
|
||||||
// quiz, err := store.Create(
|
quiz, err := store.Create(
|
||||||
// &models.Quiz{
|
&models.Quiz{
|
||||||
// Question: &models.Question{Text: "Newly created question text with #tag1 #tag2."},
|
Question: &models.Question{Text: "Newly created question text with #tag1 #tag2."},
|
||||||
// Answers: []*models.Answer{
|
Answers: []*models.Answer{
|
||||||
// {Text: "Answer 1"},
|
{Text: "Answer 1"},
|
||||||
// {Text: "Answer 2"},
|
{Text: "Answer 2"},
|
||||||
// {Text: "Answer 3"},
|
{Text: "Answer 3"},
|
||||||
// {Text: "Answer 4"},
|
{Text: "Answer 4"},
|
||||||
// },
|
},
|
||||||
// CorrectPos: 0,
|
CorrectPos: 0,
|
||||||
// })
|
})
|
||||||
// t.Nil(err)
|
t.Nil(err)
|
||||||
// t.Equal(2, len(quiz.Tags))
|
t.Equal(2, len(quiz.Tags))
|
||||||
|
|
||||||
// if !t.Failed() {
|
if !t.Failed() {
|
||||||
// path := store.GetPath(quiz)
|
path := store.GetPath(quiz)
|
||||||
// t.True(path != "", "Path should not be empty.")
|
t.True(path != "", "Path should not be empty.")
|
||||||
|
|
||||||
// exists, err := os.Stat(path)
|
exists, err := os.Stat(path)
|
||||||
// t.Nil(err)
|
t.Nil(err)
|
||||||
|
|
||||||
// if !t.Failed() {
|
if !t.Failed() {
|
||||||
// t.True(exists != nil, "The new quiz file was not created.")
|
t.True(exists != nil, "The new quiz file was not created.")
|
||||||
|
|
||||||
// if !t.Failed() {
|
if !t.Failed() {
|
||||||
// quizFromDisk, err := readQuizFromDisk(path)
|
quizFromDisk, err := readQuizFromDisk(path)
|
||||||
// defer os.Remove(path)
|
defer os.Remove(path)
|
||||||
|
|
||||||
// quizFromDisk.Correct = quiz.Answers[0]
|
quizFromDisk.Correct = quiz.Answers[0]
|
||||||
// quizFromDisk.Tags = quiz.Tags
|
quizFromDisk.Tags = quiz.Tags
|
||||||
|
|
||||||
// t.Nil(err)
|
t.Nil(err)
|
||||||
|
|
||||||
// if !t.Failed() {
|
if !t.Failed() {
|
||||||
// t.Equal(quizFromDisk.Question.Text, quiz.Question.Text)
|
t.Equal(quizFromDisk.Question.Text, quiz.Question.Text)
|
||||||
// for i, a := range quizFromDisk.Answers {
|
for i, a := range quizFromDisk.Answers {
|
||||||
// t.Equal(a.Text, quiz.Answers[i].Text)
|
t.Equal(a.Text, quiz.Answers[i].Text)
|
||||||
// }
|
}
|
||||||
// for i, tag := range quizFromDisk.Tags {
|
for i, tag := range quizFromDisk.Tags {
|
||||||
// t.Equal(tag.Name, quiz.Tags[i].Name)
|
t.Equal(tag.Name, quiz.Tags[i].Name)
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
// func (t *quizTestSuite) TestDelete() {
|
func (t *quizTestSuite) TestDelete() {
|
||||||
// store, err := NewDefaultQuizFileStore()
|
store, err := NewQuizFileStore(
|
||||||
// t.Nil(err)
|
&FileStoreConfig[*models.Quiz, *store.QuizStore]{
|
||||||
|
FilePathConfig: FilePathConfig{GetDefaultQuizzesDir(), "quiz", ".md"},
|
||||||
|
IndexDirFunc: DefaultQuizIndexDirFunc,
|
||||||
|
NoIndexOnCreate: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
t.Nil(err)
|
||||||
|
|
||||||
// if !t.Failed() {
|
if !t.Failed() {
|
||||||
// quiz, err := store.Create(
|
quiz, err := store.Create(
|
||||||
// &models.Quiz{
|
&models.Quiz{
|
||||||
// Question: &models.Question{Text: "This quiz should be deleted."},
|
Question: &models.Question{Text: "This quiz should be deleted."},
|
||||||
// Answers: []*models.Answer{
|
Answers: []*models.Answer{
|
||||||
// {Text: "Answer 1"},
|
{Text: "Answer 1"},
|
||||||
// {Text: "Answer 2"},
|
{Text: "Answer 2"},
|
||||||
// {Text: "Answer 3"},
|
{Text: "Answer 3"},
|
||||||
// {Text: "Answer 4"},
|
{Text: "Answer 4"},
|
||||||
// },
|
},
|
||||||
// CorrectPos: 0,
|
CorrectPos: 0,
|
||||||
// })
|
})
|
||||||
// t.Nil(err)
|
t.Nil(err)
|
||||||
// if !t.Failed() {
|
if !t.Failed() {
|
||||||
// path := store.GetPath(quiz)
|
path := store.GetPath(quiz)
|
||||||
// _, err := store.Delete(quiz.ID)
|
_, err := store.Delete(quiz.ID)
|
||||||
// t.Nil(err, fmt.Sprintf("Quiz should be deleted without errors: %v", err))
|
t.Nil(err, fmt.Sprintf("Quiz should be deleted without errors: %v", err))
|
||||||
// if !t.Failed() {
|
if !t.Failed() {
|
||||||
// _, err := os.Stat(path)
|
_, err := os.Stat(path)
|
||||||
// t.Not(t.Nil(err))
|
t.Not(t.Nil(err))
|
||||||
|
|
||||||
// }
|
}
|
||||||
|
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
// func (t *quizTestSuite) TestUpdate() {
|
func (t *quizTestSuite) TestUpdate() {
|
||||||
// store, err := NewDefaultQuizFileStore()
|
store, err := NewQuizFileStore(
|
||||||
// t.Nil(err)
|
&FileStoreConfig[*models.Quiz, *store.QuizStore]{
|
||||||
|
FilePathConfig: FilePathConfig{GetDefaultQuizzesDir(), "quiz", ".md"},
|
||||||
|
IndexDirFunc: DefaultQuizIndexDirFunc,
|
||||||
|
NoIndexOnCreate: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
// if !t.Failed() {
|
t.Nil(err)
|
||||||
// quiz, err := store.Create(
|
|
||||||
// &models.Quiz{
|
|
||||||
// Question: &models.Question{Text: "Newly created question text with #tag1 #tag2."},
|
|
||||||
// Answers: []*models.Answer{
|
|
||||||
// {Text: "Answer 1"},
|
|
||||||
// {Text: "Answer 2"},
|
|
||||||
// {Text: "Answer 3"},
|
|
||||||
// {Text: "Answer 4"},
|
|
||||||
// },
|
|
||||||
// CorrectPos: 0,
|
|
||||||
// })
|
|
||||||
// t.Nil(err)
|
|
||||||
|
|
||||||
// _, err = store.Update(&models.Quiz{
|
if !t.Failed() {
|
||||||
// Question: &models.Question{Text: "Newly created question text with #tag1 #tag2 #tag3."},
|
quiz, err := store.Create(
|
||||||
// Answers: []*models.Answer{
|
&models.Quiz{
|
||||||
// {Text: "Answer 1"},
|
Question: &models.Question{Text: "Newly created question text with #tag1 #tag2."},
|
||||||
// {Text: "Answer 2"},
|
Answers: []*models.Answer{
|
||||||
// {Text: "Answer 3"},
|
{Text: "Answer 1"},
|
||||||
// {Text: "Answer 4"},
|
{Text: "Answer 2"},
|
||||||
// },
|
{Text: "Answer 3"},
|
||||||
// CorrectPos: 1,
|
{Text: "Answer 4"},
|
||||||
// }, quiz.ID)
|
},
|
||||||
|
CorrectPos: 0,
|
||||||
|
})
|
||||||
|
t.Nil(err)
|
||||||
|
|
||||||
// t.Nil(err)
|
_, err = store.Update(&models.Quiz{
|
||||||
|
Question: &models.Question{Text: "Newly created question text with #tag1 #tag2 #tag3."},
|
||||||
|
Answers: []*models.Answer{
|
||||||
|
{Text: "Answer 1"},
|
||||||
|
{Text: "Answer 2"},
|
||||||
|
{Text: "Answer 3"},
|
||||||
|
{Text: "Answer 4"},
|
||||||
|
},
|
||||||
|
CorrectPos: 1,
|
||||||
|
}, quiz.ID)
|
||||||
|
|
||||||
// updatedQuizFromMemory, err := store.Read(quiz.ID)
|
t.Nil(err)
|
||||||
// t.Equal(len(updatedQuizFromMemory.Tags), 3)
|
|
||||||
// t.Equal("Answer 2", updatedQuizFromMemory.Correct.Text)
|
|
||||||
|
|
||||||
// defer os.Remove(store.GetPath(quiz))
|
updatedQuizFromMemory, err := store.Read(quiz.ID)
|
||||||
|
t.Equal(len(updatedQuizFromMemory.Tags), 3)
|
||||||
|
t.Equal("Answer 2", updatedQuizFromMemory.Correct.Text)
|
||||||
|
|
||||||
// }
|
defer os.Remove(store.GetPath(quiz))
|
||||||
// }
|
|
||||||
|
|
||||||
// func (t *quizTestSuite) TestAutowriteHeader() {
|
}
|
||||||
// store, err := NewDefaultQuizFileStore()
|
}
|
||||||
// t.Nil(err)
|
|
||||||
|
|
||||||
// if !t.Failed() {
|
func (t *quizTestSuite) TestAutowriteHeader() {
|
||||||
|
store, err := NewDefaultQuizFileStore()
|
||||||
|
t.Nil(err)
|
||||||
|
|
||||||
// meta, err := readQuizHeader(filepath.Join(store.Dir, "quiz_5.md"))
|
if !t.Failed() {
|
||||||
// t.Nil(err)
|
meta, err := readQuizHeader(filepath.Join(store.Dir, "quiz_5.md"))
|
||||||
|
t.Nil(err)
|
||||||
|
if !t.Failed() {
|
||||||
|
t.Not(t.Nil(meta))
|
||||||
|
|
||||||
// if !t.Failed() {
|
if !t.Failed() {
|
||||||
// t.Not(t.Nil(meta))
|
t.True(meta.ID != "", "ID should not be empty")
|
||||||
|
|
||||||
// if !t.Failed() {
|
if !t.Failed() {
|
||||||
// t.True(meta.ID != "", "ID should not be empty")
|
_, err = removeQuizHeader(filepath.Join(store.Dir, "quiz_5.md"))
|
||||||
|
t.True(err == nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// if !t.Failed() {
|
func readQuizFromDisk(path string) (*models.Quiz, error) {
|
||||||
// _, err = removeQuizHeader(filepath.Join(store.Dir, "quiz_5.md"))
|
content, err := os.ReadFile(path)
|
||||||
// t.True(err == nil)
|
if err != nil {
|
||||||
// }
|
return nil, err
|
||||||
// }
|
}
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func readQuizFromDisk(path string) (*models.Quiz, error) {
|
result := new(models.Quiz)
|
||||||
// content, err := os.ReadFile(path)
|
|
||||||
// if err != nil {
|
|
||||||
// return nil, err
|
|
||||||
// }
|
|
||||||
|
|
||||||
// result := new(models.Quiz)
|
err = result.Unmarshal(content)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// err = result.Unmarshal(content)
|
return result, nil
|
||||||
// if err != nil {
|
}
|
||||||
// return nil, err
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return result, nil
|
|
||||||
// }
|
|
||||||
|
|
2
store/file/testdata/groups/group_96a300bb-0b29-4b32-93cf-5bcde7fcef61.csv
vendored
Normal file
2
store/file/testdata/groups/group_96a300bb-0b29-4b32-93cf-5bcde7fcef61.csv
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
id,firstname,lastname,token,attributes
|
||||||
|
1234,John,Smith,111222,class:1 D LIN
|
|
5
store/group.go
Normal file
5
store/group.go
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
package store
|
||||||
|
|
||||||
|
import "git.andreafazzi.eu/andrea/probo/models"
|
||||||
|
|
||||||
|
type GroupStore = Store[*models.Group]
|
|
@ -1,124 +0,0 @@
|
||||||
package memory
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.andreafazzi.eu/andrea/probo/client"
|
|
||||||
"git.andreafazzi.eu/andrea/probo/models"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *MemoryProboCollectorStore) getCollectionFromID(id string) *models.Collection {
|
|
||||||
s.lock.RLock()
|
|
||||||
defer s.lock.RUnlock()
|
|
||||||
|
|
||||||
collection, ok := s.collections[id]
|
|
||||||
if ok {
|
|
||||||
return collection
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MemoryProboCollectorStore) ReadAllCollections() ([]*models.Collection, error) {
|
|
||||||
result := make([]*models.Collection, 0)
|
|
||||||
for id := range s.collections {
|
|
||||||
if collection := s.getCollectionFromID(id); collection != nil {
|
|
||||||
result = append(result, collection)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MemoryProboCollectorStore) CreateCollection(r *client.CreateUpdateCollectionRequest) (*models.Collection, error) {
|
|
||||||
q, _, err := s.createOrUpdateCollection(r, "")
|
|
||||||
return q, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MemoryProboCollectorStore) UpdateCollection(r *client.CreateUpdateCollectionRequest, id string) (*models.Collection, bool, error) {
|
|
||||||
return s.createOrUpdateCollection(r, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MemoryProboCollectorStore) ReadCollectionByID(id string) (*models.Collection, error) {
|
|
||||||
if id == "" {
|
|
||||||
return nil, errors.New("ID should not be an empty string!")
|
|
||||||
}
|
|
||||||
collection := s.getCollectionFromID(id)
|
|
||||||
if collection == nil {
|
|
||||||
return nil, fmt.Errorf("Collection ID %v not found in the store", id)
|
|
||||||
}
|
|
||||||
return collection, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MemoryProboCollectorStore) DeleteCollection(r *client.DeleteCollectionRequest) (*models.Collection, error) {
|
|
||||||
return s.deleteCollection(r.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MemoryProboCollectorStore) deleteCollection(id string) (*models.Collection, error) {
|
|
||||||
s.lock.Lock()
|
|
||||||
defer s.lock.Unlock()
|
|
||||||
|
|
||||||
collection := s.collections[id]
|
|
||||||
if collection == nil {
|
|
||||||
return nil, fmt.Errorf("Trying to delete a collection that doesn't exist in memory (ID: %v)", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(s.collections, id)
|
|
||||||
|
|
||||||
return collection, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MemoryProboCollectorStore) createOrUpdateCollection(r *client.CreateUpdateCollectionRequest, id string) (*models.Collection, bool, error) {
|
|
||||||
var collection *models.Collection
|
|
||||||
|
|
||||||
if r.Collection == nil {
|
|
||||||
return nil, false, errors.New("A request was made passing a nil collection object")
|
|
||||||
}
|
|
||||||
|
|
||||||
if id != "" { // we're updating a collection
|
|
||||||
collection = s.getCollectionFromID(id)
|
|
||||||
if collection == nil { // Quiz is not present in the store
|
|
||||||
return nil, false, fmt.Errorf("Collection ID %v doesn't exist in the store!", id)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
id = uuid.New().String()
|
|
||||||
collection = new(models.Collection)
|
|
||||||
}
|
|
||||||
|
|
||||||
collection.Name = r.Collection.Name
|
|
||||||
collection.Query = r.Collection.Query
|
|
||||||
|
|
||||||
collection.Quizzes = s.query(collection.Query)
|
|
||||||
|
|
||||||
return s.createCollectionFromID(id, collection), true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MemoryProboCollectorStore) query(query string) []*models.Quiz {
|
|
||||||
s.lock.Lock()
|
|
||||||
defer s.lock.Unlock()
|
|
||||||
|
|
||||||
result := make([]*models.Quiz, 0)
|
|
||||||
|
|
||||||
for _, quiz := range s.quizzes {
|
|
||||||
for _, tag := range quiz.Tags {
|
|
||||||
if query == tag.Name {
|
|
||||||
result = append(result, quiz)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MemoryProboCollectorStore) createCollectionFromID(id string, collection *models.Collection) *models.Collection {
|
|
||||||
s.lock.Lock()
|
|
||||||
defer s.lock.Unlock()
|
|
||||||
|
|
||||||
collection.ID = id
|
|
||||||
|
|
||||||
s.collections[id] = collection
|
|
||||||
|
|
||||||
return collection
|
|
||||||
}
|
|
|
@ -1,98 +0,0 @@
|
||||||
package memory
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.andreafazzi.eu/andrea/probo/client"
|
|
||||||
"git.andreafazzi.eu/andrea/probo/hasher/sha256"
|
|
||||||
"github.com/remogatto/prettytest"
|
|
||||||
)
|
|
||||||
|
|
||||||
type collectionTestSuite struct {
|
|
||||||
prettytest.Suite
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *collectionTestSuite) TestUpdateCollection() {
|
|
||||||
store := NewMemoryProboCollectorStore(
|
|
||||||
sha256.NewDefault256Hasher(sha256.DefaultSHA256HashingFn),
|
|
||||||
)
|
|
||||||
|
|
||||||
quiz_1, _ := store.CreateQuiz(
|
|
||||||
&client.CreateUpdateQuizRequest{
|
|
||||||
Quiz: &client.Quiz{
|
|
||||||
Question: &client.Question{Text: "Question text with #tag1."},
|
|
||||||
Answers: []*client.Answer{
|
|
||||||
{Text: "Answer 1", Correct: true},
|
|
||||||
{Text: "Answer 2", Correct: false},
|
|
||||||
{Text: "Answer 3", Correct: false},
|
|
||||||
{Text: "Answer 4", Correct: false},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
quiz_2, _ := store.CreateQuiz(
|
|
||||||
&client.CreateUpdateQuizRequest{
|
|
||||||
Quiz: &client.Quiz{
|
|
||||||
Question: &client.Question{Text: "Another question text with #tag1."},
|
|
||||||
Answers: []*client.Answer{
|
|
||||||
{Text: "Answer 1", Correct: true},
|
|
||||||
{Text: "Answer 2", Correct: false},
|
|
||||||
{Text: "Answer 3", Correct: false},
|
|
||||||
{Text: "Answer 4", Correct: false},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
collection, _ := store.CreateCollection(
|
|
||||||
&client.CreateUpdateCollectionRequest{
|
|
||||||
Collection: &client.Collection{
|
|
||||||
Name: "MyCollection",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
updatedCollection, updated, err := store.UpdateCollection(
|
|
||||||
&client.CreateUpdateCollectionRequest{
|
|
||||||
Collection: &client.Collection{
|
|
||||||
Name: "MyUpdatedCollection",
|
|
||||||
Query: "#tag1",
|
|
||||||
},
|
|
||||||
}, collection.ID)
|
|
||||||
|
|
||||||
t.Nil(err, fmt.Sprintf("The update returned an error: %v", err))
|
|
||||||
|
|
||||||
if !t.Failed() {
|
|
||||||
t.True(updated)
|
|
||||||
t.Equal("MyUpdatedCollection", updatedCollection.Name)
|
|
||||||
t.True(len(updatedCollection.Quizzes) == 2)
|
|
||||||
if !t.Failed() {
|
|
||||||
count := 0
|
|
||||||
for _, q := range updatedCollection.Quizzes {
|
|
||||||
if quiz_1.ID == q.ID || quiz_2.ID == q.ID {
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
t.Equal(2, count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *collectionTestSuite) TestDeleteCollection() {
|
|
||||||
store := NewMemoryProboCollectorStore(
|
|
||||||
sha256.NewDefault256Hasher(sha256.DefaultSHA256HashingFn),
|
|
||||||
)
|
|
||||||
collection, _ := store.CreateCollection(
|
|
||||||
&client.CreateUpdateCollectionRequest{
|
|
||||||
Collection: &client.Collection{
|
|
||||||
Name: "Collection to be deleted",
|
|
||||||
Query: "#tag1",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
deletedCollection, err := store.DeleteCollection(&client.DeleteCollectionRequest{ID: collection.ID})
|
|
||||||
|
|
||||||
t.Equal(collection.ID, deletedCollection.ID, "Returned deleted collection ID should be equal to the request")
|
|
||||||
t.Nil(err, fmt.Sprintf("The update returned an error: %v", err))
|
|
||||||
|
|
||||||
_, err = store.ReadCollectionByID(deletedCollection.ID)
|
|
||||||
t.True(err != nil, "Reading a non existent quiz should return an error")
|
|
||||||
}
|
|
|
@ -1,48 +0,0 @@
|
||||||
package memory
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"git.andreafazzi.eu/andrea/probo/hasher"
|
|
||||||
"git.andreafazzi.eu/andrea/probo/models"
|
|
||||||
"git.andreafazzi.eu/andrea/probo/store"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Store[T Storable] struct {
|
|
||||||
ids map[string]T
|
|
||||||
hashes map[string]T
|
|
||||||
|
|
||||||
// A mutex is used to synchronize read/write access to the map
|
|
||||||
lock sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewStore[T store.Storable]() *Store[T] {
|
|
||||||
store := new(Store[T])
|
|
||||||
|
|
||||||
store.ids = make(map[string]T)
|
|
||||||
|
|
||||||
return store
|
|
||||||
}
|
|
||||||
|
|
||||||
type QuizStore struct {
|
|
||||||
*Store[*Quiz]
|
|
||||||
|
|
||||||
questions *Store[*Question]
|
|
||||||
answers *Store[*Answer]
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMemoryProboCollectorStore(hasher hasher.Hasher) *MemoryProboCollectorStore {
|
|
||||||
s := new(MemoryProboCollectorStore)
|
|
||||||
|
|
||||||
s.hasher = hasher
|
|
||||||
|
|
||||||
s.questionsHashes = make(map[string]*models.Question)
|
|
||||||
s.answersHashes = make(map[string]*models.Answer)
|
|
||||||
s.quizzesHashes = make(map[string]*models.Quiz)
|
|
||||||
|
|
||||||
s.quizzes = make(map[string]*models.Quiz)
|
|
||||||
s.collections = make(map[string]*models.Collection)
|
|
||||||
s.participants = make(map[string]*models.Participant)
|
|
||||||
|
|
||||||
return s
|
|
||||||
}
|
|
|
@ -1,143 +0,0 @@
|
||||||
package memory
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.andreafazzi.eu/andrea/probo/client"
|
|
||||||
"git.andreafazzi.eu/andrea/probo/hasher/sha256"
|
|
||||||
"github.com/remogatto/prettytest"
|
|
||||||
)
|
|
||||||
|
|
||||||
type testSuite struct {
|
|
||||||
prettytest.Suite
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunner(t *testing.T) {
|
|
||||||
prettytest.Run(
|
|
||||||
t,
|
|
||||||
new(testSuite),
|
|
||||||
new(collectionTestSuite),
|
|
||||||
new(participantTestSuite),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *testSuite) TestReadQuizByHash() {
|
|
||||||
store := NewMemoryProboCollectorStore(
|
|
||||||
sha256.NewDefault256Hasher(sha256.DefaultSHA256HashingFn),
|
|
||||||
)
|
|
||||||
quiz, _ := store.CreateQuiz(
|
|
||||||
&client.CreateUpdateQuizRequest{
|
|
||||||
Quiz: &client.Quiz{
|
|
||||||
Question: &client.Question{Text: "Newly created question text."},
|
|
||||||
Answers: []*client.Answer{
|
|
||||||
{Text: "Answer 1", Correct: true},
|
|
||||||
{Text: "Answer 2", Correct: false},
|
|
||||||
{Text: "Answer 3", Correct: false},
|
|
||||||
{Text: "Answer 4", Correct: false},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
quizFromMemory, err := store.ReadQuizByHash(quiz.Hash)
|
|
||||||
t.Nil(err, "Quiz should be found in the store")
|
|
||||||
if !t.Failed() {
|
|
||||||
t.True(reflect.DeepEqual(quizFromMemory, quiz), "Quiz should be equal")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *testSuite) TestParseTextForTags() {
|
|
||||||
store := NewMemoryProboCollectorStore(
|
|
||||||
sha256.NewDefault256Hasher(sha256.DefaultSHA256HashingFn),
|
|
||||||
)
|
|
||||||
quiz, _ := store.CreateQuiz(
|
|
||||||
&client.CreateUpdateQuizRequest{
|
|
||||||
Quiz: &client.Quiz{
|
|
||||||
Question: &client.Question{Text: "Newly created question text with #tag1."},
|
|
||||||
Answers: []*client.Answer{
|
|
||||||
{Text: "Answer 1", Correct: true},
|
|
||||||
{Text: "Answer 2 with #tag2", Correct: false},
|
|
||||||
{Text: "Answer 3", Correct: false},
|
|
||||||
{Text: "Answer 4", Correct: false},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
quizFromMemory, err := store.ReadQuizByHash(quiz.Hash)
|
|
||||||
t.Nil(err, "Quiz should be found in the store")
|
|
||||||
if !t.Failed() {
|
|
||||||
t.True(len(quizFromMemory.Tags) == 2, "Two tags should be present.")
|
|
||||||
t.Equal("#tag1", quizFromMemory.Tags[0].Name)
|
|
||||||
t.Equal("#tag2", quizFromMemory.Tags[1].Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *testSuite) TestUpdateQuiz() {
|
|
||||||
store := NewMemoryProboCollectorStore(
|
|
||||||
sha256.NewDefault256Hasher(sha256.DefaultSHA256HashingFn),
|
|
||||||
)
|
|
||||||
quiz, _ := store.CreateQuiz(
|
|
||||||
&client.CreateUpdateQuizRequest{
|
|
||||||
Quiz: &client.Quiz{
|
|
||||||
Question: &client.Question{Text: "Newly created question text."},
|
|
||||||
Answers: []*client.Answer{
|
|
||||||
{Text: "Answer 1", Correct: true},
|
|
||||||
{Text: "Answer 2", Correct: false},
|
|
||||||
{Text: "Answer 3", Correct: false},
|
|
||||||
{Text: "Answer 4", Correct: false},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
createdQuizHash := quiz.Hash
|
|
||||||
|
|
||||||
updatedQuiz, updated, err := store.UpdateQuiz(
|
|
||||||
&client.CreateUpdateQuizRequest{
|
|
||||||
Quiz: &client.Quiz{
|
|
||||||
Question: &client.Question{Text: "Updated question text."},
|
|
||||||
Answers: []*client.Answer{
|
|
||||||
{Text: "Answer 1", Correct: true},
|
|
||||||
{Text: "Updated Answer 2", Correct: false},
|
|
||||||
{Text: "Answer 3", Correct: false},
|
|
||||||
{Text: "Answer 4", Correct: false},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, quiz.ID)
|
|
||||||
|
|
||||||
t.Nil(err, fmt.Sprintf("The update returned an error: %v", err))
|
|
||||||
|
|
||||||
if !t.Failed() {
|
|
||||||
t.True(updated)
|
|
||||||
t.True(createdQuizHash != updatedQuiz.Hash, "The two hashes should not be equal.")
|
|
||||||
t.Equal(4, len(updatedQuiz.Answers))
|
|
||||||
t.Equal("Updated question text.", updatedQuiz.Question.Text)
|
|
||||||
t.Equal("Updated Answer 2", updatedQuiz.Answers[1].Text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *testSuite) TestDeleteQuiz() {
|
|
||||||
store := NewMemoryProboCollectorStore(
|
|
||||||
sha256.NewDefault256Hasher(sha256.DefaultSHA256HashingFn),
|
|
||||||
)
|
|
||||||
quiz, _ := store.CreateQuiz(
|
|
||||||
&client.CreateUpdateQuizRequest{
|
|
||||||
Quiz: &client.Quiz{
|
|
||||||
Question: &client.Question{Text: "This test should be removed."},
|
|
||||||
Answers: []*client.Answer{
|
|
||||||
{Text: "Answer 1", Correct: true},
|
|
||||||
{Text: "Answer 2", Correct: false},
|
|
||||||
{Text: "Answer 3", Correct: false},
|
|
||||||
{Text: "Answer 4", Correct: false},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
deletedQuiz, err := store.DeleteQuiz(&client.DeleteQuizRequest{ID: quiz.ID})
|
|
||||||
|
|
||||||
t.Equal(quiz.ID, deletedQuiz.ID, "Returned deleted quiz ID should be equal to the request")
|
|
||||||
t.Nil(err, fmt.Sprintf("The update returned an error: %v", err))
|
|
||||||
|
|
||||||
_, err = store.ReadQuizByHash(deletedQuiz.Hash)
|
|
||||||
t.True(err != nil, "Reading a non existent quiz should return an error")
|
|
||||||
}
|
|
|
@ -1,108 +0,0 @@
|
||||||
package memory
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.andreafazzi.eu/andrea/probo/client"
|
|
||||||
"git.andreafazzi.eu/andrea/probo/models"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *MemoryProboCollectorStore) ReadAllParticipants() ([]*models.Participant, error) {
|
|
||||||
result := make([]*models.Participant, 0)
|
|
||||||
for id := range s.participants {
|
|
||||||
if participant := s.getParticipantFromID(id); participant != nil {
|
|
||||||
result = append(result, participant)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MemoryProboCollectorStore) getParticipantFromID(id string) *models.Participant {
|
|
||||||
s.lock.RLock()
|
|
||||||
defer s.lock.RUnlock()
|
|
||||||
|
|
||||||
participant, ok := s.participants[id]
|
|
||||||
if ok {
|
|
||||||
return participant
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MemoryProboCollectorStore) CreateParticipant(r *client.CreateUpdateParticipantRequest) (*models.Participant, error) {
|
|
||||||
q, _, err := s.createOrUpdateParticipant(r, "")
|
|
||||||
return q, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MemoryProboCollectorStore) UpdateParticipant(r *client.CreateUpdateParticipantRequest, id string) (*models.Participant, bool, error) {
|
|
||||||
return s.createOrUpdateParticipant(r, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MemoryProboCollectorStore) ReadParticipantByID(id string) (*models.Participant, error) {
|
|
||||||
if id == "" {
|
|
||||||
return nil, errors.New("ID should not be an empty string!")
|
|
||||||
}
|
|
||||||
participant := s.getParticipantFromID(id)
|
|
||||||
if participant == nil {
|
|
||||||
return nil, fmt.Errorf("Participant ID %v not found in the store", id)
|
|
||||||
}
|
|
||||||
return participant, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MemoryProboCollectorStore) DeleteParticipant(r *client.DeleteParticipantRequest) (*models.Participant, error) {
|
|
||||||
return s.deleteParticipant(r.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MemoryProboCollectorStore) deleteParticipant(id string) (*models.Participant, error) {
|
|
||||||
s.lock.Lock()
|
|
||||||
defer s.lock.Unlock()
|
|
||||||
|
|
||||||
participant := s.participants[id]
|
|
||||||
if participant == nil {
|
|
||||||
return nil, fmt.Errorf("Trying to delete a participant that doesn't exist in memory (ID: %v)", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(s.participants, id)
|
|
||||||
|
|
||||||
return participant, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MemoryProboCollectorStore) createOrUpdateParticipant(r *client.CreateUpdateParticipantRequest, id string) (*models.Participant, bool, error) {
|
|
||||||
var participant *models.Participant
|
|
||||||
|
|
||||||
if r.Participant == nil {
|
|
||||||
return nil, false, errors.New("A request was made passing a nil participant object")
|
|
||||||
}
|
|
||||||
|
|
||||||
if id != "" { // we're updating a participant
|
|
||||||
participant = s.getParticipantFromID(id)
|
|
||||||
if participant == nil { // Participant is not present in the store
|
|
||||||
return nil, false, fmt.Errorf("Participant ID %v doesn't exist in the store!", id)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
id = uuid.New().String()
|
|
||||||
participant = new(models.Participant)
|
|
||||||
}
|
|
||||||
|
|
||||||
participant.Attributes = make(map[string]string)
|
|
||||||
|
|
||||||
participant.Firstname = r.Participant.Firstname
|
|
||||||
participant.Lastname = r.Participant.Lastname
|
|
||||||
participant.Token = r.Participant.Token
|
|
||||||
participant.Attributes = r.Participant.Attributes
|
|
||||||
|
|
||||||
return s.createParticipantFromID(id, participant), true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MemoryProboCollectorStore) createParticipantFromID(id string, participant *models.Participant) *models.Participant {
|
|
||||||
s.lock.Lock()
|
|
||||||
defer s.lock.Unlock()
|
|
||||||
|
|
||||||
participant.ID = id
|
|
||||||
|
|
||||||
s.participants[id] = participant
|
|
||||||
|
|
||||||
return participant
|
|
||||||
}
|
|
|
@ -1,62 +0,0 @@
|
||||||
package memory
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.andreafazzi.eu/andrea/probo/client"
|
|
||||||
"git.andreafazzi.eu/andrea/probo/hasher/sha256"
|
|
||||||
"github.com/remogatto/prettytest"
|
|
||||||
)
|
|
||||||
|
|
||||||
type participantTestSuite struct {
|
|
||||||
prettytest.Suite
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *participantTestSuite) TestUpdateParticipant() {
|
|
||||||
store := NewMemoryProboCollectorStore(
|
|
||||||
sha256.NewDefault256Hasher(sha256.DefaultSHA256HashingFn),
|
|
||||||
)
|
|
||||||
|
|
||||||
participant, _ := store.CreateParticipant(&client.CreateUpdateParticipantRequest{
|
|
||||||
Participant: &client.Participant{
|
|
||||||
Firstname: "John",
|
|
||||||
Lastname: "Doe",
|
|
||||||
Token: 1234,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
updatedParticipant, updated, err := store.UpdateParticipant(&client.CreateUpdateParticipantRequest{
|
|
||||||
Participant: &client.Participant{
|
|
||||||
Firstname: "Jack",
|
|
||||||
Lastname: "Smith",
|
|
||||||
},
|
|
||||||
}, participant.ID)
|
|
||||||
|
|
||||||
t.Nil(err, fmt.Sprintf("The update returned an error: %v", err))
|
|
||||||
|
|
||||||
if !t.Failed() {
|
|
||||||
t.True(updated)
|
|
||||||
t.Equal("Jack", updatedParticipant.Firstname)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *participantTestSuite) TestDeleteParticipant() {
|
|
||||||
store := NewMemoryProboCollectorStore(
|
|
||||||
sha256.NewDefault256Hasher(sha256.DefaultSHA256HashingFn),
|
|
||||||
)
|
|
||||||
participant, _ := store.CreateParticipant(
|
|
||||||
&client.CreateUpdateParticipantRequest{
|
|
||||||
Participant: &client.Participant{
|
|
||||||
Firstname: "Jack",
|
|
||||||
Lastname: "Smith",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
deletedParticipant, err := store.DeleteParticipant(&client.DeleteParticipantRequest{ID: participant.ID})
|
|
||||||
|
|
||||||
t.Equal(participant.ID, deletedParticipant.ID, "Returned deleted participant ID should be equal to the request")
|
|
||||||
t.Nil(err, fmt.Sprintf("The update returned an error: %v", err))
|
|
||||||
|
|
||||||
_, err = store.ReadParticipantByID(deletedParticipant.ID)
|
|
||||||
t.True(err != nil, "Reading a non existent participant should return an error")
|
|
||||||
}
|
|
|
@ -1,246 +0,0 @@
|
||||||
package memory
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.andreafazzi.eu/andrea/probo/client"
|
|
||||||
"git.andreafazzi.eu/andrea/probo/models"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *MemoryProboCollectorStore) ReadAllQuizzes() ([]*models.Quiz, error) {
|
|
||||||
result := make([]*models.Quiz, 0)
|
|
||||||
for id := range s.quizzes {
|
|
||||||
if quiz := s.getQuizFromID(id); quiz != nil {
|
|
||||||
result = append(result, quiz)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MemoryProboCollectorStore) ReadQuizByID(id string) (*models.Quiz, error) {
|
|
||||||
quiz := s.getQuizFromID(id)
|
|
||||||
if quiz == nil {
|
|
||||||
return nil, fmt.Errorf("Quiz with ID %s was not found in the store.", id)
|
|
||||||
}
|
|
||||||
return quiz, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MemoryProboCollectorStore) ReadQuizByHash(hash string) (*models.Quiz, error) {
|
|
||||||
quiz := s.getQuizFromHash(hash)
|
|
||||||
if quiz == nil {
|
|
||||||
return nil, fmt.Errorf("Quiz with hash %s was not found in the store.", hash)
|
|
||||||
}
|
|
||||||
return quiz, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MemoryProboCollectorStore) CreateQuiz(r *client.CreateUpdateQuizRequest) (*models.Quiz, error) {
|
|
||||||
q, _, err := s.createOrUpdateQuiz(r, "")
|
|
||||||
return q, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MemoryProboCollectorStore) UpdateQuiz(r *client.CreateUpdateQuizRequest, id string) (*models.Quiz, bool, error) {
|
|
||||||
return s.createOrUpdateQuiz(r, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MemoryProboCollectorStore) DeleteQuiz(r *client.DeleteQuizRequest) (*models.Quiz, error) {
|
|
||||||
return s.deleteQuiz(r.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MemoryProboCollectorStore) CalculateQuizHash(quiz *client.Quiz) string {
|
|
||||||
hashes := s.hasher.QuizHashes(quiz)
|
|
||||||
return hashes[len(hashes)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MemoryProboCollectorStore) parseTextForTags(text string, tags *[]*models.Tag) string {
|
|
||||||
|
|
||||||
// Trim the following chars
|
|
||||||
trimChars := "*:.,/\\@()[]{}<>"
|
|
||||||
|
|
||||||
// Split the text into words
|
|
||||||
words := strings.Fields(text)
|
|
||||||
|
|
||||||
for _, word := range words {
|
|
||||||
// If the word starts with '#', it is considered as a tag
|
|
||||||
if strings.HasPrefix(word, "#") {
|
|
||||||
// Check if the tag already exists in the tags slice
|
|
||||||
exists := false
|
|
||||||
for _, tag := range *tags {
|
|
||||||
if tag.Name == word {
|
|
||||||
exists = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the tag does not exist in the tags slice, add it
|
|
||||||
if !exists {
|
|
||||||
*tags = append(*tags, &models.Tag{Name: strings.TrimRight(word, trimChars)})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MemoryProboCollectorStore) createOrUpdateQuiz(r *client.CreateUpdateQuizRequest, id string) (*models.Quiz, bool, error) {
|
|
||||||
if r.Quiz == nil {
|
|
||||||
return nil, false, errors.New("A request was made passing a nil quiz object")
|
|
||||||
}
|
|
||||||
hashes := s.hasher.QuizHashes(r.Quiz)
|
|
||||||
quizHash := hashes[len(hashes)-1]
|
|
||||||
|
|
||||||
quiz := s.getQuizFromHash(quizHash)
|
|
||||||
if quiz != nil { // Quiz is already present in the store
|
|
||||||
return quiz, false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if id != "" { // we're updating a quiz
|
|
||||||
quiz = s.getQuizFromID(id)
|
|
||||||
if quiz == nil { // Quiz is not present in the store
|
|
||||||
return nil, false, fmt.Errorf("Quiz ID %v doesn't exist in the store!", id)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if r.Meta != nil {
|
|
||||||
if r.Meta.ID != "" {
|
|
||||||
id = r.Meta.ID
|
|
||||||
} else {
|
|
||||||
id = uuid.New().String()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
id = uuid.New().String()
|
|
||||||
}
|
|
||||||
quiz = new(models.Quiz)
|
|
||||||
}
|
|
||||||
|
|
||||||
if quiz.Tags == nil {
|
|
||||||
quiz.Tags = make([]*models.Tag, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
questionHash := hashes[0]
|
|
||||||
q := s.getQuestionFromHash(questionHash)
|
|
||||||
if q == nil { // if the question is not in the store then we should add it
|
|
||||||
q = s.createQuestionFromHash(questionHash, &models.Question{
|
|
||||||
Meta: models.Meta{ID: uuid.New().String()},
|
|
||||||
Text: s.parseTextForTags(r.Quiz.Question.Text, &quiz.Tags),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate Question field
|
|
||||||
quiz.Question = q
|
|
||||||
|
|
||||||
// Reset answer slice
|
|
||||||
quiz.Answers = make([]*models.Answer, 0)
|
|
||||||
|
|
||||||
for i, answer := range r.Quiz.Answers {
|
|
||||||
answerHash := hashes[i+1]
|
|
||||||
a := s.getAnswerFromHash(answerHash)
|
|
||||||
if a == nil { // if the answer is not in the store add it
|
|
||||||
a = s.createAnswerFromHash(answerHash, &models.Answer{
|
|
||||||
ID: uuid.New().String(),
|
|
||||||
Text: s.parseTextForTags(answer.Text, &quiz.Tags),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if answer.Correct {
|
|
||||||
quiz.Correct = a
|
|
||||||
}
|
|
||||||
quiz.Answers = append(quiz.Answers, a)
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.createQuizFromHash(id, quizHash, quiz), true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MemoryProboCollectorStore) getQuizFromHash(hash string) *models.Quiz {
|
|
||||||
s.lock.RLock()
|
|
||||||
defer s.lock.RUnlock()
|
|
||||||
|
|
||||||
quiz, ok := s.quizzesHashes[hash]
|
|
||||||
if ok {
|
|
||||||
return quiz
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MemoryProboCollectorStore) getQuizFromID(id string) *models.Quiz {
|
|
||||||
s.lock.RLock()
|
|
||||||
defer s.lock.RUnlock()
|
|
||||||
|
|
||||||
quiz, ok := s.quizzes[id]
|
|
||||||
if ok {
|
|
||||||
return quiz
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MemoryProboCollectorStore) getQuestionFromHash(hash string) *models.Question {
|
|
||||||
s.lock.RLock()
|
|
||||||
defer s.lock.RUnlock()
|
|
||||||
|
|
||||||
question, ok := s.questionsHashes[hash]
|
|
||||||
if ok {
|
|
||||||
return question
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MemoryProboCollectorStore) getAnswerFromHash(hash string) *models.Answer {
|
|
||||||
s.lock.RLock()
|
|
||||||
defer s.lock.RUnlock()
|
|
||||||
|
|
||||||
answer, ok := s.answersHashes[hash]
|
|
||||||
if ok {
|
|
||||||
return answer
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MemoryProboCollectorStore) createQuizFromHash(id string, hash string, quiz *models.Quiz) *models.Quiz {
|
|
||||||
s.lock.Lock()
|
|
||||||
defer s.lock.Unlock()
|
|
||||||
|
|
||||||
quiz.ID = id
|
|
||||||
quiz.Hash = hash
|
|
||||||
|
|
||||||
s.quizzesHashes[hash] = quiz
|
|
||||||
s.quizzes[id] = quiz
|
|
||||||
|
|
||||||
return quiz
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MemoryProboCollectorStore) createQuestionFromHash(hash string, question *models.Question) *models.Question {
|
|
||||||
s.lock.Lock()
|
|
||||||
defer s.lock.Unlock()
|
|
||||||
|
|
||||||
s.questionsHashes[hash] = question
|
|
||||||
|
|
||||||
return question
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MemoryProboCollectorStore) createAnswerFromHash(hash string, answer *models.Answer) *models.Answer {
|
|
||||||
s.lock.Lock()
|
|
||||||
defer s.lock.Unlock()
|
|
||||||
|
|
||||||
s.answersHashes[hash] = answer
|
|
||||||
|
|
||||||
return answer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MemoryProboCollectorStore) deleteQuiz(id string) (*models.Quiz, error) {
|
|
||||||
s.lock.Lock()
|
|
||||||
defer s.lock.Unlock()
|
|
||||||
|
|
||||||
quiz := s.quizzes[id]
|
|
||||||
if quiz == nil {
|
|
||||||
return nil, fmt.Errorf("Trying to delete a quiz that doesn't exist in memory (ID: %v)", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(s.quizzes, id)
|
|
||||||
delete(s.quizzesHashes, quiz.Hash)
|
|
||||||
|
|
||||||
return quiz, nil
|
|
||||||
}
|
|
|
@ -2,4 +2,31 @@ package store
|
||||||
|
|
||||||
import "git.andreafazzi.eu/andrea/probo/models"
|
import "git.andreafazzi.eu/andrea/probo/models"
|
||||||
|
|
||||||
type ParticipantStore = Store[*models.Participant]
|
type ParticipantStore struct {
|
||||||
|
*FilterStore[*models.Participant]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewParticipantStore() *ParticipantStore {
|
||||||
|
store := new(ParticipantStore)
|
||||||
|
store.FilterStore = NewFilterStore[*models.Participant]()
|
||||||
|
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ParticipantStore) FilterInGroup(group *models.Group, filter *models.ParticipantFilter) []*models.Participant {
|
||||||
|
participants := s.ReadAll()
|
||||||
|
filteredParticipants := s.Filter(participants, func(p *models.Participant) bool {
|
||||||
|
for pk, pv := range p.Attributes {
|
||||||
|
for fk, fv := range filter.Attributes {
|
||||||
|
if pk == fk && pv == fv {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
group.Participants = filteredParticipants
|
||||||
|
|
||||||
|
return group.Participants
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue