feat: add open questions with a pending grading state

- 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
This commit is contained in:
Jakub Zych
2026-11-09 17:48:11 +01:00
parent ef38b81fd9
commit 297780cb8e
19 changed files with 893 additions and 48 deletions

33
classes/open.go Normal file
View File

@@ -0,0 +1,33 @@
package classes
import (
"strings"
"unicode/utf8"
"git.golem15.com/golem15/sm-quizzes-plugin/models"
)
// MaxLength is the longest answer an open question accepts, in characters:
// its MaxLength when set and positive, else models.DefaultMaxLength.
func MaxLength(q models.Question) int {
if q.MaxLength != nil && *q.MaxLength > 0 {
return *q.MaxLength
}
return models.DefaultMaxLength
}
// CheckOpenText cleans a student's answer to an open question (D-04): it
// removes every NUL character (Postgres text refuses them) and the
// surrounding whitespace. A blank text is a FieldError text_required and a
// text longer than MaxLength(q) characters (runes, not bytes) is a
// FieldError text_too_long.
func CheckOpenText(text string, q models.Question) (string, error) {
text = strings.TrimSpace(strings.ReplaceAll(text, "\x00", ""))
if text == "" {
return "", &FieldError{Field: "text", Key: "text_required"}
}
if utf8.RuneCountInString(text) > MaxLength(q) {
return "", &FieldError{Field: "text", Key: "text_too_long"}
}
return text, nil
}