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

@@ -8,8 +8,31 @@ import (
"git.golem15.com/golem15/summercms/modules/lagoon"
)
// Grading states of an open answer (AttemptAnswer.GradingStatus).
const (
// GradingPending waits for a grader or for the owner.
GradingPending = "pending"
// GradingGraded holds a grader's score the owner has not confirmed.
GradingGraded = "graded"
// GradingFailed is an answer the grader could not score; the owner
// grades it.
GradingFailed = "failed"
// GradingReviewed is final: the owner confirmed or set the score.
GradingReviewed = "reviewed"
)
// GradingStatuses lists the valid values of AttemptAnswer.GradingStatus.
var GradingStatuses = []string{GradingPending, GradingGraded, GradingFailed, GradingReviewed}
// AttemptAnswer is the user's answer to one question of an attempt. There is
// one row per attempt and question; answering again overwrites it.
//
// TextAnswer and the grading fields belong to open questions only:
// GradingStatus is NULL for every other row. GradingSeq grows with every new
// text, so a grading job for an older text is ignored; GradingAttempts counts
// the grader runs for the current text. The review fields record the owner's
// decision: ReviewedByUserID for a frontend user (MCP), ReviewedByAdminID for
// a backend administrator.
type AttemptAnswer struct {
ID uint `gorm:"column:id;primaryKey"`
AttemptID uint `gorm:"column:attempt_id"`
@@ -20,6 +43,20 @@ type AttemptAnswer struct {
Points float64 `gorm:"column:points"`
CreatedAt time.Time `gorm:"column:created_at"`
UpdatedAt time.Time `gorm:"column:updated_at"`
TextAnswer *string `gorm:"column:text_answer"`
GradingStatus *string `gorm:"column:grading_status"`
GradingSeq int `gorm:"column:grading_seq"`
GradingAttempts int `gorm:"column:grading_attempts"`
Grader *string `gorm:"column:grader"`
GraderPoints *float64 `gorm:"column:grader_points"`
GraderFeedback *string `gorm:"column:grader_feedback"`
GradingError *string `gorm:"column:grading_error"`
GradedAt *time.Time `gorm:"column:graded_at"`
ReviewComment *string `gorm:"column:review_comment"`
ReviewedAt *time.Time `gorm:"column:reviewed_at"`
ReviewedByUserID *uint `gorm:"column:reviewed_by_user_id"`
ReviewedByAdminID *uint `gorm:"column:reviewed_by_admin_id"`
}
// TableName is the plugin-prefixed table.