203 lines
4.1 KiB
Go
203 lines
4.1 KiB
Go
package models
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"sort"
|
|
"strings"
|
|
|
|
"gopkg.in/yaml.v2"
|
|
)
|
|
|
|
type Quiz struct {
|
|
Meta
|
|
|
|
Hash string `json:"hash"`
|
|
Question *Question `json:"question" gorm:"foreignKey:ID"`
|
|
Answers []*Answer `json:"answers" gorm:"many2many:quiz_answers"`
|
|
Tags []*Tag `json:"tags" yaml:"-" gorm:"-"`
|
|
Correct *Answer `json:"correct" gorm:"foreignKey:ID"`
|
|
CorrectPos uint `gorm:"-"` // Position of the correct answer during quiz creation
|
|
Type int `json:"type"`
|
|
}
|
|
|
|
func MarkdownToQuiz(quiz *Quiz, markdown string) error {
|
|
meta, remainingMarkdown, err := ParseMetaHeaderFromMarkdown(markdown)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
lines := strings.Split(remainingMarkdown, "\n")
|
|
|
|
questionText := ""
|
|
answers := []*Answer{}
|
|
|
|
for _, line := range lines {
|
|
if strings.HasPrefix(line, "*") {
|
|
answerText := strings.TrimPrefix(line, "* ")
|
|
answer := &Answer{Text: answerText}
|
|
answers = append(answers, answer)
|
|
} else {
|
|
if questionText != "" {
|
|
questionText += "\n"
|
|
}
|
|
questionText += line
|
|
}
|
|
}
|
|
|
|
questionText = strings.TrimRight(questionText, "\n")
|
|
|
|
if questionText == "" {
|
|
return fmt.Errorf("Question text should not be empty.")
|
|
}
|
|
|
|
if len(answers) < 2 {
|
|
return fmt.Errorf("Number of answers should be at least 2 but parsed answers are %d.", len(answers))
|
|
}
|
|
|
|
question := &Question{Text: questionText}
|
|
|
|
quiz.Question = question
|
|
quiz.Answers = answers
|
|
|
|
//quiz = &Quiz{Question: question, Answers: answers, CorrectPos: 0}
|
|
|
|
if meta != nil {
|
|
quiz.Meta = *meta
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func QuizToMarkdown(quiz *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.")
|
|
}
|
|
|
|
quiz.Correct = quiz.Answers[quiz.CorrectPos]
|
|
|
|
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 pos, answer := range quiz.Answers {
|
|
if quiz.CorrectPos != uint(pos) {
|
|
otherAnswers += "* " + answer.Text + "\n"
|
|
}
|
|
}
|
|
|
|
markdown := quiz.Question.Text + "\n\n" + correctAnswer + "\n" + otherAnswers
|
|
|
|
return markdown, nil
|
|
}
|
|
|
|
func (q *Quiz) GetHash() string {
|
|
return q.calculateHash()
|
|
}
|
|
|
|
func (q *Quiz) Marshal() ([]byte, error) {
|
|
result, err := QuizToMarkdown(q)
|
|
|
|
return []byte(result), err
|
|
|
|
}
|
|
|
|
func (q *Quiz) Unmarshal(data []byte) error {
|
|
return MarkdownToQuiz(q, string(data))
|
|
}
|
|
|
|
func (q *Quiz) calculateHash() string {
|
|
result := make([]string, 0)
|
|
|
|
result = append(result, q.Question.GetHash())
|
|
|
|
for _, a := range q.Answers {
|
|
result = append(result, a.GetHash())
|
|
}
|
|
|
|
orderedHashes := make([]string, len(result))
|
|
|
|
copy(orderedHashes, result)
|
|
sort.Strings(orderedHashes)
|
|
|
|
return fmt.Sprintf("%x", sha256.Sum256([]byte(strings.Join(orderedHashes, ""))))
|
|
}
|
|
|
|
func ParseMetaHeaderFromMarkdown(markdown string) (*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 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
|
|
}
|