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"
|
||||
|
||||
type Filter struct {
|
||||
Tags []*Tag
|
||||
}
|
||||
|
||||
type Collection struct {
|
||||
Meta
|
||||
|
||||
|
@ -39,3 +35,7 @@ func (c *Collection) Marshal() ([]byte, error) {
|
|||
func (c *Collection) Unmarshal(data []byte) error {
|
||||
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
|
||||
|
||||
import (
|
||||
"github.com/gocarina/gocsv"
|
||||
)
|
||||
|
||||
type Group struct {
|
||||
Meta
|
||||
Name string
|
||||
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(
|
||||
t,
|
||||
new(testSuite),
|
||||
new(groupTestSuite),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -4,9 +4,12 @@ import (
|
|||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type AttributeList map[string]string
|
||||
|
||||
type Participant struct {
|
||||
ID string `csv:"id" gorm:"primaryKey"`
|
||||
|
||||
|
@ -15,7 +18,7 @@ type Participant struct {
|
|||
|
||||
Token uint `csv:"token"`
|
||||
|
||||
Attributes map[string]string
|
||||
Attributes AttributeList `csv:"attributes"`
|
||||
}
|
||||
|
||||
func (p *Participant) String() string {
|
||||
|
@ -52,3 +55,27 @@ func (p *Participant) Marshal() ([]byte, error) {
|
|||
func (p *Participant) Unmarshal(data []byte) error {
|
||||
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) {
|
||||
return NewCollectionFileStore(
|
||||
&FileStoreConfig[*models.Collection, *store.CollectionStore]{
|
||||
FilePathConfig: FilePathConfig{DefaultBaseDir, "collection", ".json"},
|
||||
FilePathConfig: FilePathConfig{GetDefaultCollectionsDir(), "collection", ".json"},
|
||||
IndexDirFunc: DefaultIndexDirFunc[*models.Collection, *store.CollectionStore],
|
||||
},
|
||||
)
|
||||
|
|
|
@ -7,6 +7,7 @@ var (
|
|||
DefaultQuizzesSubdir = "quizzes"
|
||||
DefaultCollectionsSubdir = "collections"
|
||||
DefaultParticipantsSubdir = "participants"
|
||||
DefaultGroupsSubdir = "groups"
|
||||
)
|
||||
|
||||
func GetDefaultQuizzesDir() string {
|
||||
|
@ -20,3 +21,7 @@ func GetDefaultCollectionsDir() string {
|
|||
func GetDefaultParticipantsDir() string {
|
||||
return filepath.Join(DefaultBaseDir, DefaultParticipantsSubdir)
|
||||
}
|
||||
|
||||
func GetDefaultGroupsDir() string {
|
||||
return filepath.Join(DefaultBaseDir, DefaultGroupsSubdir)
|
||||
}
|
||||
|
|
|
@ -23,6 +23,8 @@ type FileStorable interface {
|
|||
|
||||
Marshal() ([]byte, error)
|
||||
Unmarshal([]byte) error
|
||||
|
||||
Create() FileStorable
|
||||
}
|
||||
|
||||
type Storer[T store.Storable] interface {
|
||||
|
|
|
@ -16,5 +16,6 @@ func TestRunner(t *testing.T) {
|
|||
new(quizTestSuite),
|
||||
new(collectionTestSuite),
|
||||
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"
|
||||
)
|
||||
|
||||
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) {
|
||||
return NewFileStore[*models.Participant](config, store.NewStore[*models.Participant]())
|
||||
func NewParticipantFileStore(config *FileStoreConfig[*models.Participant, *store.ParticipantStore]) (*ParticipantFileStore, error) {
|
||||
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
|
||||
|
||||
mEntity, err := s.Create(entity)
|
||||
mEntity, err := s.Storer.Create(entity)
|
||||
if err != nil && !errors.As(err, &errQuizAlreadyPresent) {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"git.andreafazzi.eu/andrea/probo/models"
|
||||
"git.andreafazzi.eu/andrea/probo/store"
|
||||
"github.com/remogatto/prettytest"
|
||||
)
|
||||
|
||||
|
@ -35,167 +37,184 @@ func (t *quizTestSuite) TestReadAll() {
|
|||
}
|
||||
}
|
||||
|
||||
// func (t *quizTestSuite) TestCreate() {
|
||||
// store, err := NewDefaultQuizFileStore()
|
||||
// t.Nil(err)
|
||||
func (t *quizTestSuite) TestCreate() {
|
||||
store, err := NewQuizFileStore(
|
||||
&FileStoreConfig[*models.Quiz, *store.QuizStore]{
|
||||
FilePathConfig: FilePathConfig{GetDefaultQuizzesDir(), "quiz", ".md"},
|
||||
IndexDirFunc: DefaultQuizIndexDirFunc,
|
||||
NoIndexOnCreate: true,
|
||||
},
|
||||
)
|
||||
t.Nil(err)
|
||||
|
||||
// if !t.Failed() {
|
||||
// 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)
|
||||
// t.Equal(2, len(quiz.Tags))
|
||||
if !t.Failed() {
|
||||
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)
|
||||
t.Equal(2, len(quiz.Tags))
|
||||
|
||||
// if !t.Failed() {
|
||||
// path := store.GetPath(quiz)
|
||||
// t.True(path != "", "Path should not be empty.")
|
||||
if !t.Failed() {
|
||||
path := store.GetPath(quiz)
|
||||
t.True(path != "", "Path should not be empty.")
|
||||
|
||||
// exists, err := os.Stat(path)
|
||||
// t.Nil(err)
|
||||
exists, err := os.Stat(path)
|
||||
t.Nil(err)
|
||||
|
||||
// if !t.Failed() {
|
||||
// t.True(exists != nil, "The new quiz file was not created.")
|
||||
if !t.Failed() {
|
||||
t.True(exists != nil, "The new quiz file was not created.")
|
||||
|
||||
// if !t.Failed() {
|
||||
// quizFromDisk, err := readQuizFromDisk(path)
|
||||
// defer os.Remove(path)
|
||||
if !t.Failed() {
|
||||
quizFromDisk, err := readQuizFromDisk(path)
|
||||
defer os.Remove(path)
|
||||
|
||||
// quizFromDisk.Correct = quiz.Answers[0]
|
||||
// quizFromDisk.Tags = quiz.Tags
|
||||
quizFromDisk.Correct = quiz.Answers[0]
|
||||
quizFromDisk.Tags = quiz.Tags
|
||||
|
||||
// t.Nil(err)
|
||||
t.Nil(err)
|
||||
|
||||
// if !t.Failed() {
|
||||
// t.Equal(quizFromDisk.Question.Text, quiz.Question.Text)
|
||||
// for i, a := range quizFromDisk.Answers {
|
||||
// t.Equal(a.Text, quiz.Answers[i].Text)
|
||||
// }
|
||||
// for i, tag := range quizFromDisk.Tags {
|
||||
// t.Equal(tag.Name, quiz.Tags[i].Name)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
if !t.Failed() {
|
||||
t.Equal(quizFromDisk.Question.Text, quiz.Question.Text)
|
||||
for i, a := range quizFromDisk.Answers {
|
||||
t.Equal(a.Text, quiz.Answers[i].Text)
|
||||
}
|
||||
for i, tag := range quizFromDisk.Tags {
|
||||
t.Equal(tag.Name, quiz.Tags[i].Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// func (t *quizTestSuite) TestDelete() {
|
||||
// store, err := NewDefaultQuizFileStore()
|
||||
// t.Nil(err)
|
||||
func (t *quizTestSuite) TestDelete() {
|
||||
store, err := NewQuizFileStore(
|
||||
&FileStoreConfig[*models.Quiz, *store.QuizStore]{
|
||||
FilePathConfig: FilePathConfig{GetDefaultQuizzesDir(), "quiz", ".md"},
|
||||
IndexDirFunc: DefaultQuizIndexDirFunc,
|
||||
NoIndexOnCreate: true,
|
||||
},
|
||||
)
|
||||
t.Nil(err)
|
||||
|
||||
// if !t.Failed() {
|
||||
// quiz, err := store.Create(
|
||||
// &models.Quiz{
|
||||
// Question: &models.Question{Text: "This quiz should be deleted."},
|
||||
// Answers: []*models.Answer{
|
||||
// {Text: "Answer 1"},
|
||||
// {Text: "Answer 2"},
|
||||
// {Text: "Answer 3"},
|
||||
// {Text: "Answer 4"},
|
||||
// },
|
||||
// CorrectPos: 0,
|
||||
// })
|
||||
// t.Nil(err)
|
||||
// if !t.Failed() {
|
||||
// path := store.GetPath(quiz)
|
||||
// _, err := store.Delete(quiz.ID)
|
||||
// t.Nil(err, fmt.Sprintf("Quiz should be deleted without errors: %v", err))
|
||||
// if !t.Failed() {
|
||||
// _, err := os.Stat(path)
|
||||
// t.Not(t.Nil(err))
|
||||
if !t.Failed() {
|
||||
quiz, err := store.Create(
|
||||
&models.Quiz{
|
||||
Question: &models.Question{Text: "This quiz should be deleted."},
|
||||
Answers: []*models.Answer{
|
||||
{Text: "Answer 1"},
|
||||
{Text: "Answer 2"},
|
||||
{Text: "Answer 3"},
|
||||
{Text: "Answer 4"},
|
||||
},
|
||||
CorrectPos: 0,
|
||||
})
|
||||
t.Nil(err)
|
||||
if !t.Failed() {
|
||||
path := store.GetPath(quiz)
|
||||
_, err := store.Delete(quiz.ID)
|
||||
t.Nil(err, fmt.Sprintf("Quiz should be deleted without errors: %v", err))
|
||||
if !t.Failed() {
|
||||
_, err := os.Stat(path)
|
||||
t.Not(t.Nil(err))
|
||||
|
||||
// }
|
||||
}
|
||||
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// func (t *quizTestSuite) TestUpdate() {
|
||||
// store, err := NewDefaultQuizFileStore()
|
||||
// t.Nil(err)
|
||||
func (t *quizTestSuite) TestUpdate() {
|
||||
store, err := NewQuizFileStore(
|
||||
&FileStoreConfig[*models.Quiz, *store.QuizStore]{
|
||||
FilePathConfig: FilePathConfig{GetDefaultQuizzesDir(), "quiz", ".md"},
|
||||
IndexDirFunc: DefaultQuizIndexDirFunc,
|
||||
NoIndexOnCreate: true,
|
||||
},
|
||||
)
|
||||
|
||||
// if !t.Failed() {
|
||||
// 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)
|
||||
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)
|
||||
if !t.Failed() {
|
||||
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)
|
||||
|
||||
// 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.Equal(len(updatedQuizFromMemory.Tags), 3)
|
||||
// t.Equal("Answer 2", updatedQuizFromMemory.Correct.Text)
|
||||
t.Nil(err)
|
||||
|
||||
// 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"))
|
||||
// t.Nil(err)
|
||||
if !t.Failed() {
|
||||
meta, err := readQuizHeader(filepath.Join(store.Dir, "quiz_5.md"))
|
||||
t.Nil(err)
|
||||
if !t.Failed() {
|
||||
t.Not(t.Nil(meta))
|
||||
|
||||
// if !t.Failed() {
|
||||
// t.Not(t.Nil(meta))
|
||||
if !t.Failed() {
|
||||
t.True(meta.ID != "", "ID should not be empty")
|
||||
|
||||
// if !t.Failed() {
|
||||
// t.True(meta.ID != "", "ID should not be empty")
|
||||
if !t.Failed() {
|
||||
_, err = removeQuizHeader(filepath.Join(store.Dir, "quiz_5.md"))
|
||||
t.True(err == nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if !t.Failed() {
|
||||
// _, err = removeQuizHeader(filepath.Join(store.Dir, "quiz_5.md"))
|
||||
// t.True(err == nil)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
func readQuizFromDisk(path string) (*models.Quiz, error) {
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// func readQuizFromDisk(path string) (*models.Quiz, error) {
|
||||
// content, err := os.ReadFile(path)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
result := new(models.Quiz)
|
||||
|
||||
// result := new(models.Quiz)
|
||||
err = result.Unmarshal(content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// err = result.Unmarshal(content)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
// return result, nil
|
||||
// }
|
||||
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"
|
||||
|
||||
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