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

View File

@@ -152,6 +152,27 @@ func Totals(points []float64, maxPoints int) (score, percent float64) {
return score, round(score / float64(maxPoints) * 100)
}
// ClampOpenPoints keeps a grader's score of an open question in
// 0..points (a negative points value counts as 0), rounded to the nearest
// 0.5 (D-02). NaN and ±Inf are not a score: it returns 0, false.
func ClampOpenPoints(p float64, points int) (float64, bool) {
if math.IsNaN(p) || math.IsInf(p, 0) {
return 0, false
}
full := float64(max(points, 0))
p = math.Round(math.Min(math.Max(p, 0), full)*2) / 2
if p == 0 {
return 0, true // no negative zero in JSON
}
return p, true
}
// OpenCorrect reports whether an open answer scored p earns full marks: a
// question with points earns is_correct only with all of them (D-02).
func OpenCorrect(p float64, points int) bool {
return points > 0 && p == float64(points)
}
// round keeps two decimals.
func round(f float64) float64 {
r := math.Round(f*100) / 100