First implementation of SQlite store

This commit is contained in:
andrea 2023-10-28 20:50:06 +02:00
parent 4878bf5e18
commit 578b4e2079
19 changed files with 345 additions and 722 deletions

View file

@ -21,6 +21,21 @@ type Collection struct {
Query string `json:"query"`
}
type Participant struct {
Firstname string `json:"firstname"`
Lastname string `json:"lastname"`
Class string `json:"class"`
Token uint `json:"token"`
}
type Exam struct {
Name string `json:"name"`
Description string `json:"description"`
ParticipantID string `json:"participant_id"`
CollectionID string `json:"collection_id"`
}
type BaseResponse struct {
Status string `json:"status"`
Message string `json:"message"`
@ -65,3 +80,27 @@ type CreateUpdateCollectionRequest struct {
type DeleteCollectionRequest struct {
ID string
}
type CreateUpdateExamRequest struct {
*Exam
}
type DeleteExamRequest struct {
ID string
}
type ReadExamByIDRequest struct {
ID string
}
type CreateUpdateParticipantRequest struct {
*Participant
}
type DeleteParticipantRequest struct {
ID string
}
type ReadParticipantByIDRequest struct {
ID string
}

16
go.mod
View file

@ -5,12 +5,24 @@ go 1.17
require github.com/sirupsen/logrus v1.8.1
require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/glebarez/sqlite v1.9.0 // indirect
github.com/go-yaml/yaml v2.1.0+incompatible // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/julienschmidt/httprouter v1.3.0 // indirect
github.com/kr/pretty v0.2.1 // indirect
github.com/kr/text v0.1.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/remogatto/prettytest v0.0.0-20200211072524-6d385e11dcb8 // indirect
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/sys v0.13.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gorm.io/gorm v1.25.5 // indirect
modernc.org/libc v1.24.1 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.7.2 // indirect
modernc.org/sqlite v1.26.0 // indirect
)

29
go.sum
View file

@ -1,8 +1,20 @@
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/glebarez/sqlite v1.9.0 h1:Aj6bPA12ZEx5GbSF6XADmCkYXlljPNUY+Zf1EQxynXs=
github.com/glebarez/sqlite v1.9.0/go.mod h1:YBYCoyupOao60lzp1MVBLEjZfgkq0tdB1voAQ09K9zw=
github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o=
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
@ -10,13 +22,30 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remogatto/prettytest v0.0.0-20200211072524-6d385e11dcb8 h1:nRDwTcxV9B3elxMt+1xINX0bwaPdpouqp5fbynexY8U=
github.com/remogatto/prettytest v0.0.0-20200211072524-6d385e11dcb8/go.mod h1:jOEnp79oIHy5cvQSHeLcgVJk1GHOOHJHQWps/d1N5Yo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
modernc.org/libc v1.24.1 h1:uvJSeCKL/AgzBo2yYIPPTy82v21KgGnizcGYfBHaNuM=
modernc.org/libc v1.24.1/go.mod h1:FmfO1RLrU3MHJfyi9eYYmZBfi/R+tqZ6+hQ3yQQUkak=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
modernc.org/sqlite v1.26.0 h1:SocQdLRSYlA8W99V8YH0NES75thx19d9sB/aFc4R8Lw=
modernc.org/sqlite v1.26.0/go.mod h1:FL3pVXie73rg3Rii6V/u5BoHlSoyeZeIgKZEgHARyCU=

View file

@ -1,6 +1,6 @@
package models
type Answer struct {
ID string `json:"id"`
ID string `json:"id" gorm:"primaryKey"`
Text string `json:"text"`
}

View file

@ -1,9 +1,10 @@
package models
type Collection struct {
ID string `json:"id"`
Meta
Name string `json:"name"`
Query string `json:"query"`
IDs []string `json:"ids"`
Quizzes []*Quiz `json:"quizzes" gorm:"many2many:collection_quizzes"`
}

View file

@ -1,13 +1,23 @@
package models
import "time"
import (
"time"
"gorm.io/gorm"
)
type Exam struct {
ID string `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Quizzes []*Quiz
Participant *Participant
Name string
Description string
Responses []string
Collection *Collection `gorm:"foreignKey:ID"`
Participant []*Participant `gorm:"many2many:exam_participants"`
// Responses []string
}

View file

@ -3,7 +3,8 @@ package models
import "time"
type Meta struct {
ID string `json:"id" yaml:"id"`
ID string `json:"id" yaml:"id" gorm:"primaryKey"`
CreatedAt time.Time `json:"created_at" yaml:"created_at"`
Tags []*Tag `json:"tags" yaml:"-"`
UpdatedAt time.Time `json:"updated_at" yaml:"updated_at"`
Tags []*Tag `json:"tags" yaml:"-" gorm:"-"`
}

View file

@ -1,7 +1,7 @@
package models
type Participant struct {
ID string
ID string `gorm:"primaryKey"`
Firstname string
Lastname string

View file

@ -1,7 +1,6 @@
package models
type Question struct {
ID string `json:"id"`
Meta
Text string `json:"text"`
AnswerIDs []string `json:"answer_ids"`
}

View file

@ -2,9 +2,10 @@ package models
type Quiz struct {
Meta
Hash string `json:"hash"`
Question *Question `json:"question"`
Answers []*Answer `json:"answers"`
Correct *Answer `json:"correct"`
Question *Question `json:"question" gorm:"foreignKey:ID"`
Answers []*Answer `json:"answers" gorm:"many2many:quiz_answers"`
Correct *Answer `json:"correct" gorm:"foreignKey:ID"`
Type int `json:"type"`
}

View file

@ -1,5 +1,14 @@
package models
import (
"time"
"gorm.io/gorm"
)
type Tag struct {
Name string `json:"name"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Name string `json:"name" gorm:"primaryKey"`
}

View file

@ -1,9 +1,79 @@
package db
import (
"git.andreafazzi.eu/andrea/probo/client"
"git.andreafazzi.eu/andrea/probo/models"
"git.andreafazzi.eu/andrea/probo/store"
"github.com/glebarez/sqlite"
"github.com/google/uuid"
"gorm.io/gorm"
)
type DBProboCollectorStore struct {
Path string
db *gorm.DB
si store.ProboCollectorStore
}
func NewDBProboCollectorStore(path string) (*DBProboCollectorStore, error) {
return nil, nil
func NewDBProboCollectorStore(path string, s store.ProboCollectorStore) (*DBProboCollectorStore, error) {
var err error
store := new(DBProboCollectorStore)
store.db, err = gorm.Open(sqlite.Open(path), &gorm.Config{})
if err != nil {
return nil, err
}
err = store.db.AutoMigrate(
&models.Question{},
&models.Answer{},
&models.Quiz{},
&models.Collection{},
&models.Exam{},
&models.Participant{},
)
if err != nil {
return nil, err
}
store.si = s
return store, nil
}
func (s *DBProboCollectorStore) CreateExam(r *client.CreateUpdateExamRequest) (*models.Exam, error) {
exam := new(models.Exam)
exam.ID = uuid.New().String()
exam.Name = r.Name
exam.Description = r.Description
collection, err := s.si.ReadCollectionByID(r.CollectionID)
if err != nil {
return nil, err
}
exam.Collection = collection
result := s.db.Create(exam)
if result.Error != nil {
return nil, result.Error
}
return exam, nil
}
func (s *DBProboCollectorStore) ReadExamByID(r *client.ReadExamByIDRequest) (*models.Exam, error) {
exam := &models.Exam{
ID: r.ID,
}
s.db.First(&exam)
if err := s.db.Error; err != nil {
return nil, err
}
return exam, nil
}

View file

@ -1,8 +1,12 @@
package db
import (
"os"
"testing"
"git.andreafazzi.eu/andrea/probo/client"
"git.andreafazzi.eu/andrea/probo/hasher/sha256"
"git.andreafazzi.eu/andrea/probo/store/memory"
"github.com/remogatto/prettytest"
)
@ -18,7 +22,55 @@ func TestRunner(t *testing.T) {
}
func (t *dbTestSuite) TestCreateExam() {
store, err := NewDBProboCollectorStore("testdata/test.sqlite")
memStore := memory.NewMemoryProboCollectorStore(
sha256.NewDefault256Hasher(sha256.DefaultSHA256HashingFn),
)
memStore.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", Correct: false},
{Text: "Answer 3", Correct: false},
{Text: "Answer 4", Correct: false},
},
},
})
store, err := NewDBProboCollectorStore("testdata/test.sqlite", memStore)
defer os.Remove("testdata/test.sqlite")
t.Nil(err)
t.Not(t.Nil(store))
if !t.Failed() {
collection, err := memStore.CreateCollection(&client.CreateUpdateCollectionRequest{
Collection: &client.Collection{
Name: "Collection name",
Query: "#tag1",
},
})
t.Nil(err)
if !t.Failed() {
exam, err := store.CreateExam(&client.CreateUpdateExamRequest{
Exam: &client.Exam{
Name: "Exam",
Description: "Exam description",
CollectionID: collection.ID,
},
})
t.Nil(err)
if !t.Failed() {
t.Not(t.Nil(exam))
examFromDb, err := store.ReadExamByID(&client.ReadExamByIDRequest{ID: exam.ID})
t.Nil(err)
if !t.Failed() {
t.Equal(examFromDb.ID, exam.ID)
}
}
}
}
}

View file

@ -86,7 +86,7 @@ func (t *collectionTestSuite) TestCreateCollection() {
collectionPath, _ := store.GetCollectionPath(collection)
if !t.Failed() {
t.Equal(2, len(collection.IDs))
t.Equal(2, len(collection.Quizzes))
os.Remove(path_1)
os.Remove(path_2)

View file

@ -1,26 +1,12 @@
package file
import (
"bufio"
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"io/ioutil"
"os"
"path"
"path/filepath"
"strings"
"sync"
"time"
"git.andreafazzi.eu/andrea/probo/client"
"git.andreafazzi.eu/andrea/probo/hasher/sha256"
"git.andreafazzi.eu/andrea/probo/models"
"git.andreafazzi.eu/andrea/probo/store/memory"
"github.com/go-yaml/yaml"
)
var (
@ -61,109 +47,6 @@ func NewFileProboCollectorStore(dirname string) (*FileProboCollectorStore, error
return s, nil
}
func (s *FileProboCollectorStore) GetQuizzesDir() string {
return s.quizzesDir
}
func (s *FileProboCollectorStore) GetCollectionsDir() string {
return s.collectionsDir
}
func (s *FileProboCollectorStore) reindexQuizzes() error {
files, err := ioutil.ReadDir(s.quizzesDir)
if err != nil {
return err
}
markdownFiles := make([]fs.FileInfo, 0)
for _, file := range files {
filename := file.Name()
if !file.IsDir() && strings.HasSuffix(filename, ".md") {
markdownFiles = append(markdownFiles, file)
}
}
if len(markdownFiles) == 0 {
return fmt.Errorf("The directory is empty.")
}
for _, file := range markdownFiles {
filename := file.Name()
fullPath := filepath.Join(s.quizzesDir, filename)
content, err := os.ReadFile(fullPath)
if err != nil {
return err
}
quiz, meta, err := QuizFromMarkdown(string(content))
if err != nil {
return err
}
q, err := s.memoryStore.CreateQuiz(&client.CreateUpdateQuizRequest{
Quiz: quiz,
Meta: meta,
})
if err != nil {
return err
}
if meta == nil {
s.WriteMetaHeaderToFile(filename, &models.Meta{
ID: q.ID,
CreatedAt: time.Now(),
})
}
s.SetQuizPath(q, fullPath)
}
return nil
}
func (s *FileProboCollectorStore) reindexCollections() error {
files, err := ioutil.ReadDir(s.collectionsDir)
if err != nil {
return err
}
jsonFiles := make([]fs.FileInfo, 0)
for _, file := range files {
filename := file.Name()
if !file.IsDir() && strings.HasSuffix(filename, ".json") {
jsonFiles = append(jsonFiles, file)
}
}
for _, file := range jsonFiles {
filename := file.Name()
fullPath := filepath.Join(s.collectionsDir, filename)
content, err := os.ReadFile(fullPath)
if err != nil {
return err
}
var clientCollection *client.Collection
err = json.Unmarshal(content, &clientCollection)
if err != nil {
return err
}
collection, err := s.memoryStore.CreateCollection(&client.CreateUpdateCollectionRequest{
Collection: clientCollection,
})
if err != nil {
return err
}
s.SetCollectionPath(collection, fullPath)
}
return nil
}
func (s *FileProboCollectorStore) Reindex() error {
s.memoryStore = memory.NewMemoryProboCollectorStore(
sha256.NewDefault256Hasher(sha256.DefaultSHA256HashingFn),
@ -184,501 +67,3 @@ func (s *FileProboCollectorStore) Reindex() error {
return nil
}
func (s *FileProboCollectorStore) ReadAllQuizzes() ([]*models.Quiz, error) {
return s.memoryStore.ReadAllQuizzes()
}
func (s *FileProboCollectorStore) CreateQuiz(r *client.CreateUpdateQuizRequest) (*models.Quiz, error) {
quiz, err := s.memoryStore.CreateQuiz(r)
if err != nil {
return nil, err
}
err = s.createOrUpdateMarkdownFile(quiz)
if err != nil {
return nil, err
}
err = s.Reindex()
if err != nil {
return nil, err
}
return s.memoryStore.ReadQuizByHash(quiz.Hash)
}
func (s *FileProboCollectorStore) UpdateQuiz(r *client.CreateUpdateQuizRequest, id string) (*models.Quiz, error) {
quiz, updated, err := s.memoryStore.UpdateQuiz(r, id)
if err != nil {
return nil, err
}
if updated { // Update and re-index only if quiz hash is changed
err = s.createOrUpdateMarkdownFile(quiz)
if err != nil {
return nil, err
}
err = s.Reindex()
if err != nil {
return nil, err
}
}
return s.memoryStore.ReadQuizByHash(quiz.Hash)
}
func (s *FileProboCollectorStore) DeleteQuiz(r *client.DeleteQuizRequest) (*models.Quiz, error) {
quiz, err := s.memoryStore.DeleteQuiz(r.ID)
if err != nil {
return nil, err
}
path, err := s.GetQuizPath(quiz)
if err != nil {
return nil, err
}
err = os.Remove(path)
if err != nil {
return nil, err
}
err = s.Reindex()
if err != nil {
return nil, err
}
return quiz, nil
}
func (s *FileProboCollectorStore) GetQuizPath(quiz *models.Quiz) (string, error) {
if quiz == nil {
return "", errors.New("Quiz object passed as argument is nil!")
}
s.lock.RLock()
defer s.lock.RUnlock()
path, ok := s.quizzesPaths[quiz.ID]
if !ok {
return "", errors.New(fmt.Sprintf("Path not found for quiz ID %v", quiz.ID))
}
return path, nil
}
func (s *FileProboCollectorStore) GetCollectionPath(collection *models.Collection) (string, error) {
s.lock.RLock()
defer s.lock.RUnlock()
path, ok := s.collectionsPaths[collection.ID]
if !ok {
return "", errors.New(fmt.Sprintf("Path not found for collection ID %v", collection.ID))
}
return path, nil
}
func (s *FileProboCollectorStore) SetQuizPath(quiz *models.Quiz, path string) string {
s.lock.Lock()
defer s.lock.Unlock()
s.quizzesPaths[quiz.ID] = path
return path
}
func (s *FileProboCollectorStore) SetCollectionPath(collection *models.Collection, path string) string {
s.lock.Lock()
defer s.lock.Unlock()
s.collectionsPaths[collection.ID] = path
return path
}
func MarkdownFromQuiz(quiz *models.Quiz) (string, error) {
if quiz.Question == nil {
return "", errors.New("Quiz should contain a question but it wasn't provided.")
}
if len(quiz.Answers) < 2 {
return "", errors.New("Quiz should contain at least 2 answers but none was provided.")
}
if quiz.Correct == nil {
return "", errors.New("Quiz should contain a correct answer but not was provided.")
}
correctAnswer := "* " + quiz.Correct.Text
var otherAnswers string
for _, answer := range quiz.Answers {
if quiz.Correct.ID != answer.ID {
otherAnswers += "* " + answer.Text + "\n"
}
}
markdown := quiz.Question.Text + "\n\n" + correctAnswer + "\n" + otherAnswers
return markdown, nil
}
func QuizFromMarkdown(markdown string) (*client.Quiz, *models.Meta, error) {
meta, remainingMarkdown, err := parseMetaHeaderFromMarkdown(markdown)
if err != nil {
return nil, nil, err
}
lines := strings.Split(remainingMarkdown, "\n")
questionText := ""
answers := []*client.Answer{}
for _, line := range lines {
if strings.HasPrefix(line, "*") {
answerText := strings.TrimPrefix(line, "* ")
correct := len(answers) == 0
answer := &client.Answer{Text: answerText, Correct: correct}
answers = append(answers, answer)
} else {
if questionText != "" {
questionText += "\n"
}
questionText += line
}
}
questionText = strings.TrimRight(questionText, "\n")
if questionText == "" {
return nil, nil, fmt.Errorf("Question text should not be empty.")
}
if len(answers) < 2 {
return nil, nil, fmt.Errorf("Number of answers should be at least 2 but parsed answers are %d.", len(answers))
}
question := &client.Question{Text: questionText}
quiz := &client.Quiz{Question: question, Answers: answers}
return quiz, meta, nil
}
func (s *FileProboCollectorStore) ReadMetaHeaderFromFile(filename string) (*models.Meta, error) {
data, err := ioutil.ReadFile(path.Join(s.quizzesDir, filename))
if err != nil {
return nil, err
}
meta, _, err := parseMetaHeaderFromMarkdown(string(data))
if err != nil {
return nil, err
}
return meta, nil
}
func (s *FileProboCollectorStore) WriteMetaHeaderToFile(filename string, meta *models.Meta) (*models.Meta, error) {
readMeta, err := s.ReadMetaHeaderFromFile(filename)
if err != nil {
return nil, err
}
if readMeta == nil {
_, err := writeMetaHeader(path.Join(s.quizzesDir, filename), meta)
if err != nil {
return nil, err
}
}
return meta, nil
}
func (s *FileProboCollectorStore) ReadAllCollections() ([]*models.Collection, error) {
return s.memoryStore.ReadAllCollections()
}
func (s *FileProboCollectorStore) CreateCollection(r *client.CreateUpdateCollectionRequest) (*models.Collection, error) {
collection, err := s.memoryStore.CreateCollection(r)
if err != nil {
return nil, err
}
err = s.createOrUpdateCollectionFile(collection)
if err != nil {
return nil, err
}
return s.memoryStore.ReadCollectionByID(collection.ID)
}
func (s *FileProboCollectorStore) UpdateCollection(r *client.CreateUpdateCollectionRequest, id string) (*models.Collection, error) {
collection, _, err := s.memoryStore.UpdateCollection(r, id)
if err != nil {
return nil, err
}
err = s.createOrUpdateCollectionFile(collection)
if err != nil {
return nil, err
}
return s.memoryStore.ReadCollectionByID(collection.ID)
}
func (s *FileProboCollectorStore) removeMetaFromFile(filename string) (*models.Meta, error) {
file, err := os.Open(path.Join(s.quizzesDir, filename))
if err != nil {
return nil, err
}
defer file.Close()
var buffer bytes.Buffer
reader := bufio.NewReader(file)
var meta models.Meta
var line string
var sb strings.Builder
for {
line, err = reader.ReadString('\n')
if err != nil {
if err == io.EOF {
break
}
return nil, err
}
if strings.TrimSpace(line) == "---" {
break
}
}
for {
line, err = reader.ReadString('\n')
if err != nil {
if err == io.EOF {
break
}
return nil, err
}
if strings.TrimSpace(line) == "---" {
break
}
sb.WriteString(line)
}
err = yaml.Unmarshal([]byte(sb.String()), &meta)
if err != nil {
return nil, err
}
_, err = io.Copy(&buffer, reader)
if err != nil {
return nil, err
}
file, err = os.Create(path.Join(s.quizzesDir, filename))
if err != nil {
return nil, err
}
defer file.Close()
_, err = io.Copy(file, &buffer)
if err != nil {
return nil, err
}
return &meta, nil
}
func (s *FileProboCollectorStore) createOrUpdateMarkdownFile(quiz *models.Quiz) error {
markdown, err := MarkdownFromQuiz(quiz)
if err != nil {
return err
}
fn, _ := s.GetQuizPath(quiz)
if fn == "" {
fn = filepath.Join(s.quizzesDir, fmt.Sprintf("quiz_%v.%s", quiz.ID, "md"))
}
file, err := os.Create(fn)
if err != nil {
return err
}
defer file.Close()
markdownWithMetaHeader, err := addMetaHeaderToMarkdown(markdown, &quiz.Meta)
if err != nil {
return err
}
_, err = file.Write([]byte(markdownWithMetaHeader))
if err != nil {
return err
}
s.SetQuizPath(quiz, fn)
return nil
}
func (s *FileProboCollectorStore) createOrUpdateCollectionFile(collection *models.Collection) error {
json, err := json.Marshal(collection)
if err != nil {
return err
}
fn, _ := s.GetCollectionPath(collection)
if fn == "" {
fn = filepath.Join(s.collectionsDir, fmt.Sprintf("collection_%v.%s", collection.ID, "json"))
}
file, err := os.Create(fn)
if err != nil {
return err
}
defer file.Close()
_, err = file.Write([]byte(json))
if err != nil {
return err
}
s.SetCollectionPath(collection, fn)
return nil
}
func addMetaHeaderToMarkdown(content string, meta *models.Meta) (string, error) {
var buffer bytes.Buffer
header, err := yaml.Marshal(meta)
if err != nil {
return "", err
}
_, err = buffer.WriteString("---\n" + string(header) + "---\n")
if err != nil {
return "", err
}
_, err = buffer.WriteString(content)
if err != nil {
return "", err
}
return buffer.String(), nil
}
func parseMetaHeaderFromMarkdown(markdown string) (*models.Meta, string, error) {
reader := strings.NewReader(markdown)
var sb strings.Builder
var line string
var err error
for {
line, err = readLine(reader)
if err != nil {
if err == io.EOF {
break
}
return nil, "", err
}
if strings.TrimSpace(line) == "---" {
break
}
}
for {
line, err = readLine(reader)
if err != nil {
if err == io.EOF {
break
}
return nil, "", err
}
if strings.TrimSpace(line) == "---" {
break
}
sb.WriteString(line)
}
if sb.String() == "" {
return nil, markdown, nil
}
var meta models.Meta
err = yaml.Unmarshal([]byte(sb.String()), &meta)
if err != nil {
return nil, markdown, err
}
remainingMarkdown := markdown[strings.Index(markdown, "---\n"+sb.String()+"---\n")+len("---\n"+sb.String()+"---\n"):]
return &meta, remainingMarkdown, nil
}
func readLine(reader *strings.Reader) (string, error) {
var sb strings.Builder
for {
r, _, err := reader.ReadRune()
if err != nil {
if err == io.EOF {
return sb.String(), io.EOF
}
return "", err
}
sb.WriteRune(r)
if r == '\n' {
break
}
}
return sb.String(), nil
}
func writeMetaHeader(filename string, meta *models.Meta) (*models.Meta, error) {
// Apri il file in lettura
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
// Crea un buffer in memoria
var buffer bytes.Buffer
// Scrivi l'intestazione YAML nel buffer
header, err := yaml.Marshal(meta)
if err != nil {
return nil, err
}
_, err = buffer.WriteString("---\n" + string(header) + "---\n")
if err != nil {
return nil, err
}
// Copia il contenuto del file originale nel buffer
_, err = io.Copy(&buffer, file)
if err != nil {
return nil, err
}
// Riapri il file in scrittura
file, err = os.Create(filename)
if err != nil {
return nil, err
}
defer file.Close()
// Scrivi il contenuto del buffer nel file
_, err = io.Copy(file, &buffer)
if err != nil {
return nil, err
}
return meta, nil
}

View file

@ -1,6 +1,7 @@
---
id: e4ee7eb2-5608-4709-ad85-1381b457de35
created_at: !!timestamp 2023-10-16T12:47:06.100160075+02:00
id: 9243c924-5a91-4fa1-9cf3-db9ce1e19ca4
created_at: !!timestamp 2023-10-28T20:49:22.688075744+02:00
updated_at: !!timestamp 0001-01-01T00:00:00Z
---
This quiz is initially without metadata.

View file

@ -140,21 +140,30 @@ func (s *MemoryProboCollectorStore) deleteQuiz(id string) (*models.Quiz, error)
return nil, fmt.Errorf("Trying to delete a quiz that doesn't exist in memory (ID: %v)", id)
}
s.quizzes[id] = nil
s.quizzesHashes[quiz.Hash] = nil
delete(s.quizzes, id)
delete(s.quizzesHashes, quiz.Hash)
return quiz, nil
}
func (s *MemoryProboCollectorStore) ReadAllQuizzes() ([]*models.Quiz, error) {
result := make([]*models.Quiz, 0)
for id := range s.quizzes {
result = append(result, s.getQuizFromID(id))
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 {
@ -236,7 +245,7 @@ func (s *MemoryProboCollectorStore) createOrUpdateQuiz(r *client.CreateUpdateQui
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{
ID: uuid.New().String(),
Meta: models.Meta{ID: uuid.New().String()},
Text: s.parseTextForTags(r.Quiz.Question.Text, &quiz.Tags),
})
}
@ -274,14 +283,16 @@ func (s *MemoryProboCollectorStore) UpdateQuiz(r *client.CreateUpdateQuizRequest
return s.createOrUpdateQuiz(r, id)
}
func (s *MemoryProboCollectorStore) DeleteQuiz(id string) (*models.Quiz, error) {
return s.deleteQuiz(id)
func (s *MemoryProboCollectorStore) DeleteQuiz(r *client.DeleteQuizRequest) (*models.Quiz, error) {
return s.deleteQuiz(r.ID)
}
func (s *MemoryProboCollectorStore) ReadAllCollections() ([]*models.Collection, error) {
result := make([]*models.Collection, 0)
for id := range s.collections {
result = append(result, s.getCollectionFromID(id))
if collection := s.getCollectionFromID(id); collection != nil {
result = append(result, collection)
}
}
return result, nil
}
@ -296,13 +307,34 @@ func (s *MemoryProboCollectorStore) UpdateCollection(r *client.CreateUpdateColle
}
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", collection.ID)
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
@ -323,21 +355,21 @@ func (s *MemoryProboCollectorStore) createOrUpdateCollection(r *client.CreateUpd
collection.Name = r.Collection.Name
collection.Query = r.Collection.Query
collection.IDs = s.query(collection.Query)
collection.Quizzes = s.query(collection.Query)
return s.createCollectionFromID(id, collection), true, nil
}
func (s *MemoryProboCollectorStore) query(query string) []string {
func (s *MemoryProboCollectorStore) query(query string) []*models.Quiz {
s.lock.Lock()
defer s.lock.Unlock()
result := make([]string, 0)
result := make([]*models.Quiz, 0)
for id, quiz := range s.quizzes {
for _, quiz := range s.quizzes {
for _, tag := range quiz.Tags {
if query == tag.Name {
result = append(result, id)
result = append(result, quiz)
break
}
}

View file

@ -18,6 +18,7 @@ func TestRunner(t *testing.T) {
prettytest.Run(
t,
new(testSuite),
new(collectionTestSuite),
)
}
@ -131,70 +132,11 @@ func (t *testSuite) TestDeleteQuiz() {
},
})
deletedQuiz, err := store.DeleteQuiz(quiz.ID)
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(quiz.Hash)
_, err = store.ReadQuizByHash(deletedQuiz.Hash)
t.True(err != nil, "Reading a non existent quiz should return an error")
}
func (t *testSuite) 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.IDs) == 2)
if !t.Failed() {
t.Equal(quiz_1.ID, updatedCollection.IDs[0])
t.Equal(quiz_2.ID, updatedCollection.IDs[1])
}
}
}

View file

@ -5,16 +5,56 @@ import (
"git.andreafazzi.eu/andrea/probo/models"
)
type ProboCollectorStore interface {
type QuizReader interface {
ReadAllQuizzes() ([]*models.Quiz, error)
ReadQuizByID(id string) (*models.Quiz, error)
ReadQuizByHash(hash string) (*models.Quiz, error)
CreateQuiz(r *client.CreateUpdateQuizRequest) (*models.Quiz, error)
UpdateQuiz(r *client.CreateUpdateQuizRequest, id string) (*models.Quiz, error)
DeleteQuiz(r *client.DeleteQuizRequest) (*models.Quiz, error)
}
type QuizWriter interface {
CreateQuiz(r *client.CreateUpdateQuizRequest) (*models.Quiz, error)
UpdateQuiz(r *client.CreateUpdateQuizRequest, id string) (*models.Quiz, bool, error)
DeleteQuiz(r *client.DeleteQuizRequest) (*models.Quiz, error)
}
type CollectionReader interface {
ReadAllCollections() ([]*models.Collection, error)
ReadCollectionByID(id string) (*models.Collection, error)
}
type CollectionWriter interface {
CreateCollection(r *client.CreateUpdateCollectionRequest) (*models.Collection, error)
UpdateCollection(r *client.CreateUpdateCollectionRequest, id string) (*models.Collection, error)
UpdateCollection(r *client.CreateUpdateCollectionRequest, id string) (*models.Collection, bool, error)
DeleteCollection(r *client.DeleteCollectionRequest) (*models.Collection, error)
}
type ParticipantReader interface {
ReadAllParticipants() ([]*models.Participant, error)
ReadParticipantByID(id string) (*models.Participant, error)
}
type ParticipantWriter interface {
CreateParticipant(r *client.CreateUpdateParticipantRequest) (*models.Participant, error)
UpdateParticipant(r *client.CreateUpdateParticipantRequest, id string) (*models.Participant, bool, error)
DeleteParticipant(r *client.DeleteParticipantRequest) (*models.Participant, error)
}
type ExamReader interface {
ReadAllExams() ([]*models.Exam, error)
ReadExamByID(id string) (*models.Exam, error)
}
type ExamWriter interface {
CreateExam(r *client.CreateUpdateExamRequest) (*models.Exam, error)
UpdateExam(r *client.CreateUpdateExamRequest, id string) (*models.Exam, bool, error)
DeleteExam(r *client.DeleteExamRequest) (*models.Exam, error)
}
type ProboCollectorStore interface {
QuizReader
QuizWriter
CollectionReader
CollectionWriter
ParticipantReader
ParticipantWriter
}