diff --git a/models/exam.go b/models/exam.go index 37cb0b4..86920e2 100644 --- a/models/exam.go +++ b/models/exam.go @@ -1,23 +1,37 @@ package models import ( - "time" - - "gorm.io/gorm" + "encoding/json" + "fmt" ) type Exam struct { - ID string `gorm:"primaryKey"` - - CreatedAt time.Time - UpdatedAt time.Time - DeletedAt gorm.DeletedAt `gorm:"index"` - - Name string - Description string - - Collection *Collection `gorm:"foreignKey:ID"` - Participant []*Participant `gorm:"many2many:exam_participants"` - - // Responses []string + Meta + Participant *Participant + Quizzes []*Quiz +} + +func (e *Exam) String() string { + return fmt.Sprintf("Exam ID %v with %v quizzes.", e.ID, len(e.Quizzes)) +} + +func (e *Exam) GetID() string { + return e.ID +} + +func (e *Exam) SetID(id string) { + e.ID = id +} + +func (e *Exam) GetHash() string { + return "" +} + +func (e *Exam) Marshal() ([]byte, error) { + return json.Marshal(e) + +} + +func (e *Exam) Unmarshal(data []byte) error { + return json.Unmarshal(data, e) } diff --git a/store/exam.go b/store/exam.go new file mode 100644 index 0000000..5bc2d30 --- /dev/null +++ b/store/exam.go @@ -0,0 +1,5 @@ +package store + +import "git.andreafazzi.eu/andrea/probo/models" + +type ExamStore = Store[*models.Exam] diff --git a/store/file/defaults.go b/store/file/defaults.go index 4646515..1caee9b 100644 --- a/store/file/defaults.go +++ b/store/file/defaults.go @@ -8,6 +8,7 @@ var ( DefaultCollectionsSubdir = "collections" DefaultParticipantsSubdir = "participants" DefaultGroupsSubdir = "groups" + DefaultExamsSubdir = "exams" ) func GetDefaultQuizzesDir() string { @@ -25,3 +26,7 @@ func GetDefaultParticipantsDir() string { func GetDefaultGroupsDir() string { return filepath.Join(DefaultBaseDir, DefaultGroupsSubdir) } + +func GetDefaultExamsDir() string { + return filepath.Join(DefaultBaseDir, DefaultExamsSubdir) +} diff --git a/store/file/exam.go b/store/file/exam.go new file mode 100644 index 0000000..3cd44cd --- /dev/null +++ b/store/file/exam.go @@ -0,0 +1,28 @@ +package file + +import ( + "git.andreafazzi.eu/andrea/probo/models" + "git.andreafazzi.eu/andrea/probo/store" +) + +type ExamFileStore = FileStore[*models.Exam, *store.Store[*models.Exam]] + +func NewExamFileStore(config *FileStoreConfig[*models.Exam, *store.ExamStore]) (*ExamFileStore, error) { + return NewFileStore[*models.Exam](config, store.NewStore[*models.Exam]()) +} + +func NewDefaultExamFileStore() (*ExamFileStore, error) { + return NewExamFileStore( + &FileStoreConfig[*models.Exam, *store.ExamStore]{ + FilePathConfig: FilePathConfig{GetDefaultExamsDir(), "exam", ".json"}, + IndexDirFunc: DefaultIndexDirFunc[*models.Exam, *store.ExamStore], + CreateEntityFunc: func() *models.Exam { + return &models.Exam{ + Participant: &models.Participant{}, + Quizzes: make([]*models.Quiz, 0), + } + }, + }, + ) + +} diff --git a/store/file/exam_test.go b/store/file/exam_test.go new file mode 100644 index 0000000..79de3c9 --- /dev/null +++ b/store/file/exam_test.go @@ -0,0 +1,111 @@ +package file + +import ( + "encoding/json" + "fmt" + "io" + "os" + + "git.andreafazzi.eu/andrea/probo/models" + "git.andreafazzi.eu/andrea/probo/store" + "github.com/remogatto/prettytest" +) + +type examTestSuite struct { + prettytest.Suite +} + +func (t *examTestSuite) TestCreate() { + participantStore, err := NewParticipantFileStore( + &FileStoreConfig[*models.Participant, *store.ParticipantStore]{ + FilePathConfig: FilePathConfig{"testdata/exams/participants", "participant", ".json"}, + IndexDirFunc: DefaultIndexDirFunc[*models.Participant, *store.ParticipantStore], + CreateEntityFunc: func() *models.Participant { + return &models.Participant{ + Attributes: make(map[string]string), + } + }, + }, + ) + + t.Nil(err) + + quizStore, err := NewQuizFileStore( + &FileStoreConfig[*models.Quiz, *store.QuizStore]{ + FilePathConfig: FilePathConfig{"testdata/exams/quizzes", "quiz", ".md"}, + IndexDirFunc: DefaultQuizIndexDirFunc, + }, + ) + + t.Nil(err) + + if !t.Failed() { + t.Equal(3, len(participantStore.ReadAll())) + + examStore, err := NewDefaultExamFileStore() + t.Nil(err) + + if !t.Failed() { + g := new(models.Group) + c := new(models.Collection) + + participants := participantStore.Storer.FilterInGroup(g, &models.ParticipantFilter{ + Attributes: map[string]string{"class": "1 D LIN"}, + }) + + quizzes := quizStore.Storer.FilterInCollection(c, &models.Filter{ + Tags: []*models.Tag{ + {Name: "#tag1"}, + }, + }) + + for _, p := range participants { + e := new(models.Exam) + e.Participant = p + e.Quizzes = quizzes + + _, err = examStore.Create(e) + t.Nil(err) + + defer os.Remove(examStore.GetPath(e)) + + examFromDisk, err := readExamFromJSON(e.GetID()) + t.Nil(err) + + if !t.Failed() { + t.Not(t.Nil(examFromDisk.Participant)) + if !t.Failed() { + t.Equal("Smith", examFromDisk.Participant.Lastname) + t.Equal(2, len(examFromDisk.Quizzes)) + } + } + } + } + } +} + +func readExamFromJSON(examID string) (*models.Exam, error) { + // Build the path to the JSON file + jsonPath := fmt.Sprintf("testdata/exams/exam_%s.json", examID) + + // Open the JSON file + file, err := os.Open(jsonPath) + if err != nil { + return nil, fmt.Errorf("failed to open JSON file: %w", err) + } + defer file.Close() + + // Read the JSON data from the file + jsonData, err := io.ReadAll(file) + if err != nil { + return nil, fmt.Errorf("failed to read JSON data: %w", err) + } + + // Unmarshal the JSON data into an Exam object + var exam models.Exam + if err := json.Unmarshal(jsonData, &exam); err != nil { + return nil, fmt.Errorf("failed to parse JSON data: %w", err) + } + + return &exam, nil +} diff --git a/store/file/file.go b/store/file/file.go index 02f629b..067f459 100644 --- a/store/file/file.go +++ b/store/file/file.go @@ -86,7 +86,7 @@ func DefaultIndexDirFunc[T FileStorable, K Storer[T]](s *FileStore[T, K]) error return err } - mEntity, err := s.Create(entity) + mEntity, err := s.Create(entity, fullPath) if err != nil { return err } @@ -116,20 +116,22 @@ func NewFileStore[T FileStorable, K Storer[T]](config *FileStoreConfig[T, K], st return store, nil } -func (s *FileStore[T, K]) Create(entity T) (T, error) { +func (s *FileStore[T, K]) Create(entity T, path ...string) (T, error) { e, err := s.Storer.Create(entity) if err != nil { return e, err } - filePath := filepath.Join(s.Dir, fmt.Sprintf("%s_%v%s", s.FilePrefix, e.GetID(), s.FileSuffix)) - data, err := e.Marshal() if err != nil { return e, err } - file, err := os.Create(filePath) + if len(path) == 0 { + path = append(path, filepath.Join(s.Dir, fmt.Sprintf("%s_%v%s", s.FilePrefix, e.GetID(), s.FileSuffix))) + } + + file, err := os.Create(path[0]) if err != nil { return e, err } @@ -141,7 +143,7 @@ func (s *FileStore[T, K]) Create(entity T) (T, error) { return e, err } - s.SetPath(e, filePath) + s.SetPath(e, path[0]) return e, nil } diff --git a/store/file/file_test.go b/store/file/file_test.go index 3169eae..e3e35d7 100644 --- a/store/file/file_test.go +++ b/store/file/file_test.go @@ -17,5 +17,6 @@ func TestRunner(t *testing.T) { new(collectionTestSuite), new(participantTestSuite), new(groupTestSuite), + new(examTestSuite), ) } diff --git a/store/file/testdata/exams/exam_fe0a7ee0-f31a-413d-ba81-ab5068bc4c73.json b/store/file/testdata/exams/exam_fe0a7ee0-f31a-413d-ba81-ab5068bc4c73.json new file mode 100644 index 0000000..3076446 --- /dev/null +++ b/store/file/testdata/exams/exam_fe0a7ee0-f31a-413d-ba81-ab5068bc4c73.json @@ -0,0 +1 @@ +{"id":"fe0a7ee0-f31a-413d-ba81-ab5068bc4c73","created_at":"0001-01-01T00:00:00Z","updated_at":"0001-01-01T00:00:00Z","Participant":{"ID":"1234","Firstname":"John","Lastname":"Smith","Token":111222,"Attributes":{"class":"1 D LIN"}},"Quizzes":[{"id":"0610939b-a1a3-4d0e-bbc4-2aae0e8ee4b9","created_at":"2023-11-27T17:51:53.910642221+01:00","updated_at":"0001-01-01T00:00:00Z","hash":"","question":{"id":"98c0eec9-677f-464e-9e3e-864a859f29a3","created_at":"0001-01-01T00:00:00Z","updated_at":"0001-01-01T00:00:00Z","text":"Question text with #tag1."},"answers":[{"id":"1ab5ff1f-74c8-4efc-abdc-d03a3640f3bc","text":"Answer 1"},{"id":"74547724-b905-476f-8cfc-6ee633f92ef3","text":"Answer 2"},{"id":"96c1a8ee-c50c-4ebc-89e4-9f3ca356adbd","text":"Answer 3"},{"id":"16c4b95e-64ce-4666-8cbe-b66fa59eb23b","text":"Answer 4"}],"tags":[{"CreatedAt":"0001-01-01T00:00:00Z","UpdatedAt":"0001-01-01T00:00:00Z","DeletedAt":null,"name":"#tag1"}],"correct":{"id":"1ab5ff1f-74c8-4efc-abdc-d03a3640f3bc","text":"Answer 1"},"CorrectPos":0,"type":0},{"id":"915818c4-b0ce-4efc-8def-7fc8d5fa7454","created_at":"2023-11-27T17:51:53.91077796+01:00","updated_at":"0001-01-01T00:00:00Z","hash":"","question":{"id":"70793f0d-2855-4140-814e-40167464424b","created_at":"0001-01-01T00:00:00Z","updated_at":"0001-01-01T00:00:00Z","text":"Another question text with #tag1."},"answers":[{"id":"1ab5ff1f-74c8-4efc-abdc-d03a3640f3bc","text":"Answer 1"},{"id":"74547724-b905-476f-8cfc-6ee633f92ef3","text":"Answer 2"},{"id":"96c1a8ee-c50c-4ebc-89e4-9f3ca356adbd","text":"Answer 3"},{"id":"16c4b95e-64ce-4666-8cbe-b66fa59eb23b","text":"Answer 4"}],"tags":[{"CreatedAt":"0001-01-01T00:00:00Z","UpdatedAt":"0001-01-01T00:00:00Z","DeletedAt":null,"name":"#tag1"}],"correct":{"id":"1ab5ff1f-74c8-4efc-abdc-d03a3640f3bc","text":"Answer 1"},"CorrectPos":0,"type":0}]} \ No newline at end of file diff --git a/store/file/testdata/exams/participants/jack.json b/store/file/testdata/exams/participants/jack.json new file mode 100644 index 0000000..6e8aab6 --- /dev/null +++ b/store/file/testdata/exams/participants/jack.json @@ -0,0 +1 @@ +{"ID":"5467","Firstname":"Jack","Lastname":"Sparrow","Token":333444,"Attributes":{"class":"2 D LIN"}} \ No newline at end of file diff --git a/store/file/testdata/exams/participants/john.json b/store/file/testdata/exams/participants/john.json new file mode 100644 index 0000000..856f752 --- /dev/null +++ b/store/file/testdata/exams/participants/john.json @@ -0,0 +1 @@ +{"ID":"1234","Firstname":"John","Lastname":"Smith","Token":111222,"Attributes":{"class":"1 D LIN"}} \ No newline at end of file diff --git a/store/file/testdata/exams/participants/wendy.json b/store/file/testdata/exams/participants/wendy.json new file mode 100644 index 0000000..61083d2 --- /dev/null +++ b/store/file/testdata/exams/participants/wendy.json @@ -0,0 +1 @@ +{"ID":"567812","Firstname":"Wendy","Lastname":"Darling","Token":333444,"Attributes":{"class":"2 D LIN"}} \ No newline at end of file diff --git a/store/file/testdata/exams/quizzes/quiz_1.md b/store/file/testdata/exams/quizzes/quiz_1.md new file mode 100644 index 0000000..eaa9629 --- /dev/null +++ b/store/file/testdata/exams/quizzes/quiz_1.md @@ -0,0 +1,12 @@ +--- +id: 0610939b-a1a3-4d0e-bbc4-2aae0e8ee4b9 +created_at: 2023-11-27T17:51:53.910642221+01:00 +updated_at: 0001-01-01T00:00:00Z +--- +Question text with #tag1. + +* Answer 1 +* Answer 2 +* Answer 3 +* Answer 4 + diff --git a/store/file/testdata/exams/quizzes/quiz_2.md b/store/file/testdata/exams/quizzes/quiz_2.md new file mode 100644 index 0000000..11c4756 --- /dev/null +++ b/store/file/testdata/exams/quizzes/quiz_2.md @@ -0,0 +1,12 @@ +--- +id: 915818c4-b0ce-4efc-8def-7fc8d5fa7454 +created_at: 2023-11-27T17:51:53.91077796+01:00 +updated_at: 0001-01-01T00:00:00Z +--- +Another question text with #tag1. + +* Answer 1 +* Answer 2 +* Answer 3 +* Answer 4 + diff --git a/store/file/testdata/exams/quizzes/quiz_3.md b/store/file/testdata/exams/quizzes/quiz_3.md new file mode 100644 index 0000000..22d42f5 --- /dev/null +++ b/store/file/testdata/exams/quizzes/quiz_3.md @@ -0,0 +1,12 @@ +--- +id: 87e7825d-c75c-448b-b718-9c0b68407ab6 +created_at: 2023-11-27T17:51:53.910886823+01:00 +updated_at: 0001-01-01T00:00:00Z +--- +Another question text with #tag2. + +* Answer 1 +* Answer 2 +* Answer 3 +* Answer 4 +