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

@@ -1,6 +1,7 @@
package classes_test
import (
"math"
"os"
"path/filepath"
"testing"
@@ -130,3 +131,37 @@ func TestScoringConfigFrom(t *testing.T) {
}
}
}
func TestClampOpenPoints(t *testing.T) {
cases := []struct {
p float64
points int
want float64
ok bool
}{
{1.26, 2, 1.5, true},
{-3, 2, 0, true},
{7, 2, 2, true},
{0.74, 3, 0.5, true},
{0.25, 3, 0.5, true},
{0.2, 3, 0, true},
{2, 2, 2, true},
{1, 0, 0, true},
{5, -1, 0, true},
{math.NaN(), 2, 0, false},
{math.Inf(1), 2, 0, false},
{math.Inf(-1), 2, 0, false},
}
for _, tc := range cases {
got, ok := classes.ClampOpenPoints(tc.p, tc.points)
if got != tc.want || ok != tc.ok {
t.Errorf("ClampOpenPoints(%v, %d) = %v, %v; want %v, %v", tc.p, tc.points, got, ok, tc.want, tc.ok)
}
if math.Signbit(got) {
t.Errorf("ClampOpenPoints(%v, %d) is negative zero", tc.p, tc.points)
}
}
if !classes.OpenCorrect(2, 2) || classes.OpenCorrect(1.5, 2) || classes.OpenCorrect(0, 0) {
t.Error("OpenCorrect: want true only for full points of a question worth points")
}
}