- migration: open question type, rubric, max_length, quiz grader and the grading and review columns of attempt answers - open answers are checked, stored as pending and never scored on answer; a changed text resets the grading state - student JSON gains text, grading_status, grader_feedback, review_comment, max_length, needs_review and open_answers; the rubric is never serialized
157 lines
5.0 KiB
Go
157 lines
5.0 KiB
Go
// Code generated by summer make. DO NOT EDIT.
|
|
|
|
package models
|
|
|
|
import (
|
|
"strings"
|
|
"time"
|
|
"unicode"
|
|
|
|
"git.golem15.com/golem15/sm-quizzes-plugin/graders"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// Quiz modes.
|
|
const (
|
|
ModeLearning = "learning"
|
|
ModeTest = "test"
|
|
)
|
|
|
|
// QuizModes lists the valid values of Quiz.Mode.
|
|
var QuizModes = []string{ModeLearning, ModeTest}
|
|
|
|
// Test navigation.
|
|
const (
|
|
NavigationFree = "free"
|
|
NavigationOneWay = "one_way"
|
|
)
|
|
|
|
// QuizNavigations lists the valid values of Quiz.Navigation.
|
|
var QuizNavigations = []string{NavigationFree, NavigationOneWay}
|
|
|
|
// Quiz is a set of questions a signed-in user can attempt. Context is a
|
|
// free-form link to a record of another plugin, such as "acme.shop:course:12".
|
|
// Mode test withholds solutions until the attempt is finished; Navigation
|
|
// (free or one_way) and MaxAttempts (nil is unlimited) only apply to tests.
|
|
// Grader is the code of the grader of its open answers (graders.Manual: the
|
|
// owner grades them); an unknown code behaves as manual.
|
|
type Quiz struct {
|
|
ID uint `gorm:"column:id;primaryKey"`
|
|
Title string `gorm:"column:title"`
|
|
Slug string `gorm:"column:slug"`
|
|
Description *string `gorm:"column:description"`
|
|
Context *string `gorm:"column:context"`
|
|
IsPublished bool `gorm:"column:is_published"`
|
|
TimeLimitSeconds *int `gorm:"column:time_limit_seconds"`
|
|
PassingScore int `gorm:"column:passing_score"`
|
|
ShuffleQuestions bool `gorm:"column:shuffle_questions"`
|
|
RoundSize *int `gorm:"column:round_size"`
|
|
Mode string `gorm:"column:mode"`
|
|
Navigation string `gorm:"column:navigation"`
|
|
MaxAttempts *int `gorm:"column:max_attempts"`
|
|
Grader string `gorm:"column:grader"`
|
|
CreatedAt time.Time `gorm:"column:created_at"`
|
|
UpdatedAt time.Time `gorm:"column:updated_at"`
|
|
Questions []Question `gorm:"foreignKey:QuizID"`
|
|
}
|
|
|
|
// TableName is the plugin-prefixed table.
|
|
func (Quiz) TableName() string { return "golem15_quizzes_quizzes" }
|
|
|
|
// Fillable lists the columns mass assignment may set.
|
|
func (Quiz) Fillable() []string {
|
|
return []string{
|
|
"title", "slug", "description", "context", "is_published",
|
|
"time_limit_seconds", "passing_score", "shuffle_questions", "round_size",
|
|
"mode", "navigation", "max_attempts", "grader",
|
|
}
|
|
}
|
|
|
|
// Rules are checked by the admin API on every save; unique ignores the quiz
|
|
// being updated.
|
|
func (Quiz) Rules() map[string]string {
|
|
return map[string]string{
|
|
"title": "required|max:255",
|
|
"slug": "required|max:255|unique:golem15_quizzes_quizzes",
|
|
"context": "nullable|max:255",
|
|
"passing_score": "required|integer|between:0,100",
|
|
"time_limit_seconds": "nullable|integer|min:1",
|
|
"round_size": "nullable|integer|min:1",
|
|
"mode": "nullable|in:learning,test",
|
|
"navigation": "nullable|in:free,one_way",
|
|
"max_attempts": "nullable|integer|min:1",
|
|
"grader": "nullable|max:64",
|
|
}
|
|
}
|
|
|
|
// BeforeValidate fills the slug from the title and the mode defaults before
|
|
// the admin form validates, so the rules see the final values.
|
|
func (q *Quiz) BeforeValidate(*gorm.DB) error {
|
|
q.fillSlug()
|
|
q.fillDefaults()
|
|
return nil
|
|
}
|
|
|
|
// BeforeSave fills the slug and the mode defaults on every other write path.
|
|
func (q *Quiz) BeforeSave(*gorm.DB) error {
|
|
q.fillSlug()
|
|
q.fillDefaults()
|
|
return nil
|
|
}
|
|
|
|
// fillDefaults stores an empty mode or navigation as learning / free, an
|
|
// empty grader as manual and a non-positive attempt limit as unlimited. GORM writes Go zero values, so
|
|
// without it a models.Quiz{} would hit the columns' CHECK constraints.
|
|
func (q *Quiz) fillDefaults() {
|
|
if q.Mode == "" {
|
|
q.Mode = ModeLearning
|
|
}
|
|
if q.Navigation == "" {
|
|
q.Navigation = NavigationFree
|
|
}
|
|
if q.Grader == "" {
|
|
q.Grader = graders.Manual
|
|
}
|
|
if q.MaxAttempts != nil && *q.MaxAttempts <= 0 {
|
|
q.MaxAttempts = nil
|
|
}
|
|
}
|
|
|
|
func (q *Quiz) fillSlug() {
|
|
if strings.TrimSpace(q.Slug) == "" {
|
|
q.Slug = Slugify(q.Title)
|
|
} else {
|
|
q.Slug = Slugify(q.Slug)
|
|
}
|
|
}
|
|
|
|
var transliterations = map[rune]string{
|
|
'ą': "a", 'ć': "c", 'ę': "e", 'ł': "l", 'ń': "n", 'ó': "o", 'ś': "s", 'ź': "z", 'ż': "z",
|
|
'ä': "a", 'ö': "o", 'ü': "u", 'ß': "ss", 'á': "a", 'é': "e", 'í': "i", 'ú': "u",
|
|
'č': "c", 'ř': "r", 'š': "s", 'ž': "z", 'ě': "e", 'ý': "y", 'à': "a", 'è': "e",
|
|
}
|
|
|
|
// Slugify lowercases s, transliterates common Latin diacritics and joins the
|
|
// remaining letters and digits with single hyphens.
|
|
func Slugify(s string) string {
|
|
var b strings.Builder
|
|
dash := false
|
|
for _, r := range strings.ToLower(strings.TrimSpace(s)) {
|
|
if t, ok := transliterations[r]; ok {
|
|
b.WriteString(t)
|
|
dash = false
|
|
continue
|
|
}
|
|
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
|
|
b.WriteRune(r)
|
|
dash = false
|
|
continue
|
|
}
|
|
if (unicode.IsSpace(r) || unicode.IsPunct(r) || unicode.IsSymbol(r)) && b.Len() > 0 && !dash {
|
|
b.WriteByte('-')
|
|
dash = true
|
|
}
|
|
}
|
|
return strings.TrimSuffix(b.String(), "-")
|
|
}
|