Files
sm-quizzes-plugin/models/quiz.go
Jakub Zych 6c6ecae9d8 feat: add test mode to quizzes and withhold its solutions
- mode, navigation and max_attempts columns with Go-side defaults
- classes.Withheld gates every solution field of the answer feedback
  and the attempt result while a test attempt is open
- a test's quiz detail lists no questions before the attempt starts
- admin fields and lang keys for the three settings
2026-11-09 14:03:34 +01:00

149 lines
4.6 KiB
Go

// Code generated by summer make. DO NOT EDIT.
package models
import (
"strings"
"time"
"unicode"
"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.
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"`
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",
}
}
// 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",
}
}
// 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 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.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(), "-")
}