Last commit before switching to generics
This commit is contained in:
parent
578b4e2079
commit
ce9dd7fd63
15 changed files with 1329 additions and 354 deletions
|
@ -24,8 +24,8 @@ type Collection struct {
|
|||
type Participant struct {
|
||||
Firstname string `json:"firstname"`
|
||||
Lastname string `json:"lastname"`
|
||||
Class string `json:"class"`
|
||||
Token uint `json:"token"`
|
||||
Attributes map[string]string `json:"attributes"`
|
||||
}
|
||||
|
||||
type Exam struct {
|
||||
|
|
12
go.mod
12
go.mod
|
@ -1,18 +1,21 @@
|
|||
module git.andreafazzi.eu/andrea/probo
|
||||
|
||||
go 1.17
|
||||
go 1.21
|
||||
|
||||
require github.com/sirupsen/logrus v1.8.1
|
||||
require (
|
||||
github.com/google/uuid v1.3.1
|
||||
github.com/julienschmidt/httprouter v1.3.0
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
gorm.io/gorm v1.25.5
|
||||
)
|
||||
|
||||
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.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
|
||||
|
@ -20,7 +23,6 @@ require (
|
|||
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
|
||||
|
|
|
@ -52,3 +52,4 @@ func (h *Default256Hasher) Calculate(hashes []string) string {
|
|||
|
||||
return h.hashFn(strings.Join(orderedHashes, ""))
|
||||
}
|
||||
-
|
||||
|
|
6
models/group.go
Normal file
6
models/group.go
Normal file
|
@ -0,0 +1,6 @@
|
|||
package models
|
||||
|
||||
type Group struct {
|
||||
Name string
|
||||
Participants []*Participant
|
||||
}
|
|
@ -5,4 +5,8 @@ type Participant struct {
|
|||
|
||||
Firstname string
|
||||
Lastname string
|
||||
|
||||
Token uint
|
||||
|
||||
Attributes map[string]string
|
||||
}
|
||||
|
|
160
store/file/collection.go
Normal file
160
store/file/collection.go
Normal file
|
@ -0,0 +1,160 @@
|
|||
package file
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"git.andreafazzi.eu/andrea/probo/client"
|
||||
"git.andreafazzi.eu/andrea/probo/models"
|
||||
)
|
||||
|
||||
func (s *FileProboCollectorStore) GetCollectionsDir() string {
|
||||
return s.collectionsDir
|
||||
}
|
||||
|
||||
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) SetCollectionPath(collection *models.Collection, path string) string {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
s.collectionsPaths[collection.ID] = path
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
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) DeleteCollection(r *client.DeleteCollectionRequest) (*models.Collection, error) {
|
||||
collection, err := s.memoryStore.DeleteCollection(&client.DeleteCollectionRequest{ID: r.ID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
path, err := s.GetCollectionPath(collection)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = os.Remove(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return collection, nil
|
||||
}
|
||||
|
||||
func (s *FileProboCollectorStore) reindexCollections() error {
|
||||
files, err := os.ReadDir(s.collectionsDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
jsonFiles := make([]fs.DirEntry, 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) 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
|
||||
}
|
494
store/file/quiz.go
Normal file
494
store/file/quiz.go
Normal file
|
@ -0,0 +1,494 @@
|
|||
package file
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.andreafazzi.eu/andrea/probo/client"
|
||||
"git.andreafazzi.eu/andrea/probo/models"
|
||||
"github.com/go-yaml/yaml"
|
||||
)
|
||||
|
||||
func (s *FileProboCollectorStore) GetQuizzesDir() string {
|
||||
return s.quizzesDir
|
||||
}
|
||||
|
||||
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) 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(&client.DeleteQuizRequest{ID: 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 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 := os.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) reindexQuizzes() error {
|
||||
files, err := os.ReadDir(s.quizzesDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
markdownFiles := make([]fs.DirEntry, 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) 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 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
|
||||
}
|
4
store/file/testdata/quizzes/quiz_5.md
vendored
4
store/file/testdata/quizzes/quiz_5.md
vendored
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
id: 9243c924-5a91-4fa1-9cf3-db9ce1e19ca4
|
||||
created_at: !!timestamp 2023-10-28T20:49:22.688075744+02:00
|
||||
id: b7ec3eb9-55e1-47c6-8652-3a27fe90bc0f
|
||||
created_at: !!timestamp 2023-10-31T10:02:02.869395215+01:00
|
||||
updated_at: !!timestamp 0001-01-01T00:00:00Z
|
||||
---
|
||||
This quiz is initially without metadata.
|
||||
|
|
124
store/memory/collection.go
Normal file
124
store/memory/collection.go
Normal file
|
@ -0,0 +1,124 @@
|
|||
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
|
||||
}
|
98
store/memory/collection_test.go
Normal file
98
store/memory/collection_test.go
Normal file
|
@ -0,0 +1,98 @@
|
|||
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")
|
||||
}
|
|
@ -2,19 +2,20 @@ package memory
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"git.andreafazzi.eu/andrea/probo/client"
|
||||
"git.andreafazzi.eu/andrea/probo/hasher"
|
||||
"git.andreafazzi.eu/andrea/probo/models"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type MemoryProboCollectorStore struct {
|
||||
|
||||
// IDs maps
|
||||
quizzes map[string]*models.Quiz
|
||||
collections map[string]*models.Collection
|
||||
participants map[string]*models.Participant
|
||||
|
||||
// Hashes maps
|
||||
questionsHashes map[string]*models.Question
|
||||
answersHashes map[string]*models.Answer
|
||||
quizzesHashes map[string]*models.Quiz
|
||||
|
@ -36,181 +37,17 @@ func NewMemoryProboCollectorStore(hasher hasher.Hasher) *MemoryProboCollectorSto
|
|||
|
||||
s.quizzes = make(map[string]*models.Quiz)
|
||||
s.collections = make(map[string]*models.Collection)
|
||||
s.participants = make(map[string]*models.Participant)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *MemoryProboCollectorStore) getQuizFromHash(hash string) *models.Quiz {
|
||||
s.lock.RLock()
|
||||
defer s.lock.RUnlock()
|
||||
|
||||
quiz, ok := s.quizzesHashes[hash]
|
||||
if ok {
|
||||
return quiz
|
||||
func Create[T any](s *MemoryProboCollectorStore, elem *T) (*T, error) {
|
||||
if elem == nil {
|
||||
return nil, errors.New("A creation request was made passing a nil element")
|
||||
}
|
||||
|
||||
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) 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) 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
|
||||
}
|
||||
|
||||
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) 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")
|
||||
}
|
||||
// Check for duplicates
|
||||
hashes := s.hasher.QuizHashes(r.Quiz)
|
||||
quizHash := hashes[len(hashes)-1]
|
||||
|
||||
|
@ -219,172 +56,4 @@ func (s *MemoryProboCollectorStore) createOrUpdateQuiz(r *client.CreateUpdateQui
|
|||
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) 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) 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
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ func TestRunner(t *testing.T) {
|
|||
t,
|
||||
new(testSuite),
|
||||
new(collectionTestSuite),
|
||||
new(participantTestSuite),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
108
store/memory/participant.go
Normal file
108
store/memory/participant.go
Normal file
|
@ -0,0 +1,108 @@
|
|||
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
|
||||
}
|
62
store/memory/participant_test.go
Normal file
62
store/memory/participant_test.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
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")
|
||||
}
|
246
store/memory/quiz.go
Normal file
246
store/memory/quiz.go
Normal file
|
@ -0,0 +1,246 @@
|
|||
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
|
||||
}
|
Loading…
Reference in a new issue