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:
108
updates/20261109180000_add_open_questions_and_grading.go
Normal file
108
updates/20261109180000_add_open_questions_and_grading.go
Normal file
@@ -0,0 +1,108 @@
|
||||
// Code generated by summer make. DO NOT EDIT.
|
||||
|
||||
package updates
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AddOpenQuestionsAndGrading returns the 20261109180000_add_open_questions_and_grading gormigrate entry.
|
||||
// It adds the open question type (answered in free text, graded against the
|
||||
// model answer and an owner-only rubric), the quiz grader and the grading
|
||||
// and review state of each attempt answer. Every existing question, quiz and
|
||||
// answer keeps today's behaviour: the grader is manual, no question is open
|
||||
// and every existing answer row has no grading state.
|
||||
func AddOpenQuestionsAndGrading() *gormigrate.Migration {
|
||||
return &gormigrate.Migration{
|
||||
ID: "20261109180000_add_open_questions_and_grading",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
// The original type CHECK is an unnamed inline constraint, so
|
||||
// it is found by the column it checks rather than by an assumed
|
||||
// name. Its definition cannot be matched on "IN": Postgres
|
||||
// stores it as type = ANY (ARRAY[...]).
|
||||
for _, stmt := range []string{
|
||||
`DO $$
|
||||
DECLARE c RECORD;
|
||||
BEGIN
|
||||
FOR c IN
|
||||
SELECT con.conname FROM pg_constraint con
|
||||
JOIN pg_attribute att ON att.attrelid = con.conrelid AND att.attname = 'type'
|
||||
WHERE con.conrelid = 'golem15_quizzes_questions'::regclass
|
||||
AND con.contype = 'c'
|
||||
AND con.conkey = ARRAY[att.attnum]
|
||||
LOOP
|
||||
EXECUTE format('ALTER TABLE golem15_quizzes_questions DROP CONSTRAINT %I', c.conname);
|
||||
END LOOP;
|
||||
END $$`,
|
||||
`ALTER TABLE golem15_quizzes_questions
|
||||
ADD CONSTRAINT golem15_quizzes_questions_type_check CHECK (type IN ('single', 'multiple', 'self_check', 'open')),
|
||||
ADD COLUMN rubric TEXT NULL,
|
||||
ADD COLUMN max_length INTEGER NULL CHECK (max_length IS NULL OR max_length BETWEEN 1 AND 20000)`,
|
||||
`ALTER TABLE golem15_quizzes_quizzes
|
||||
ADD COLUMN grader VARCHAR(64) NOT NULL DEFAULT 'manual'`,
|
||||
`ALTER TABLE golem15_quizzes_attempt_answers
|
||||
ADD COLUMN text_answer TEXT NULL,
|
||||
ADD COLUMN grading_status VARCHAR(20) NULL CHECK (grading_status IS NULL OR grading_status IN ('pending', 'graded', 'failed', 'reviewed')),
|
||||
ADD COLUMN grading_seq INTEGER NOT NULL DEFAULT 0,
|
||||
ADD COLUMN grading_attempts INTEGER NOT NULL DEFAULT 0,
|
||||
ADD COLUMN grader VARCHAR(64) NULL,
|
||||
ADD COLUMN grader_points DOUBLE PRECISION NULL,
|
||||
ADD COLUMN grader_feedback TEXT NULL,
|
||||
ADD COLUMN grading_error TEXT NULL,
|
||||
ADD COLUMN graded_at TIMESTAMPTZ NULL,
|
||||
ADD COLUMN review_comment TEXT NULL,
|
||||
ADD COLUMN reviewed_at TIMESTAMPTZ NULL,
|
||||
ADD COLUMN reviewed_by_user_id INTEGER NULL,
|
||||
ADD COLUMN reviewed_by_admin_id INTEGER NULL`,
|
||||
`CREATE INDEX golem15_quizzes_attempt_answers_grading_index ON golem15_quizzes_attempt_answers (grading_status)
|
||||
WHERE grading_status IN ('pending', 'graded', 'failed')`,
|
||||
} {
|
||||
if err := tx.Exec(stmt).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
var n int64
|
||||
if err := tx.Raw(`SELECT COUNT(*) FROM golem15_quizzes_questions WHERE type = 'open'`).Scan(&n).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if n > 0 {
|
||||
return fmt.Errorf("quizzes: rollback refused: %d open questions exist; delete them first", n)
|
||||
}
|
||||
for _, stmt := range []string{
|
||||
`DROP INDEX IF EXISTS golem15_quizzes_attempt_answers_grading_index`,
|
||||
`ALTER TABLE golem15_quizzes_attempt_answers
|
||||
DROP COLUMN IF EXISTS reviewed_by_admin_id,
|
||||
DROP COLUMN IF EXISTS reviewed_by_user_id,
|
||||
DROP COLUMN IF EXISTS reviewed_at,
|
||||
DROP COLUMN IF EXISTS review_comment,
|
||||
DROP COLUMN IF EXISTS graded_at,
|
||||
DROP COLUMN IF EXISTS grading_error,
|
||||
DROP COLUMN IF EXISTS grader_feedback,
|
||||
DROP COLUMN IF EXISTS grader_points,
|
||||
DROP COLUMN IF EXISTS grader,
|
||||
DROP COLUMN IF EXISTS grading_attempts,
|
||||
DROP COLUMN IF EXISTS grading_seq,
|
||||
DROP COLUMN IF EXISTS grading_status,
|
||||
DROP COLUMN IF EXISTS text_answer`,
|
||||
`ALTER TABLE golem15_quizzes_quizzes DROP COLUMN IF EXISTS grader`,
|
||||
`ALTER TABLE golem15_quizzes_questions
|
||||
DROP COLUMN IF EXISTS max_length,
|
||||
DROP COLUMN IF EXISTS rubric,
|
||||
DROP CONSTRAINT IF EXISTS golem15_quizzes_questions_type_check`,
|
||||
`ALTER TABLE golem15_quizzes_questions
|
||||
ADD CONSTRAINT golem15_quizzes_questions_type_check CHECK (type IN ('single', 'multiple', 'self_check'))`,
|
||||
} {
|
||||
if err := tx.Exec(stmt).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user