First implementation of Group

This commit is contained in:
andrea 2023-11-21 15:12:13 +01:00
parent ac95b38fe8
commit 4045a9c705
25 changed files with 413 additions and 975 deletions

View file

@ -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
View file

@ -0,0 +1,9 @@
package models
type Filter struct {
Tags []*Tag
}
type ParticipantFilter struct {
Attributes map[string]string
}

View file

@ -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
View 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))
}

View file

@ -16,6 +16,7 @@ func TestRunner(t *testing.T) {
prettytest.Run( prettytest.Run(
t, t,
new(testSuite), new(testSuite),
new(groupTestSuite),
) )
} }

View file

@ -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(), ",")
}

View file

@ -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],
}, },
) )

View file

@ -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)
}

View file

@ -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 {

View file

@ -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
View 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
View 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
}

View file

@ -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],
},
)
} }

View file

@ -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
} }

View file

@ -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
// }

View file

@ -0,0 +1,2 @@
id,firstname,lastname,token,attributes
1234,John,Smith,111222,class:1 D LIN
1 id firstname lastname token attributes
2 1234 John Smith 111222 class:1 D LIN

5
store/group.go Normal file
View file

@ -0,0 +1,5 @@
package store
import "git.andreafazzi.eu/andrea/probo/models"
type GroupStore = Store[*models.Group]

View file

@ -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
}

View file

@ -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")
}

View file

@ -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
}

View file

@ -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")
}

View file

@ -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
}

View file

@ -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")
}

View file

@ -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
}

View file

@ -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
}