probo/models/quiz.go

196 lines
4 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(markdown string) (*Quiz, *Meta, error) {
meta, remainingMarkdown, err := ParseMetaHeaderFromMarkdown(markdown)
if err != nil {
return nil, nil, 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 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 := &Question{Text: questionText}
quiz := &Quiz{Question: question, Answers: answers, CorrectPos: 0}
if meta != nil {
quiz.Meta = *meta
}
return quiz, meta, 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) GetID() string {
return q.ID
}
func (q *Quiz) SetID(id string) {
q.ID = id
}
func (q *Quiz) GetHash() string {
return q.calculateHash()
}
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
}