feat: review open answers in the admin and pick a grader per quiz
- Do oceny admin screen (golem15.quizzes.reviews) over the open answers, with a read-only context partial, score and comment virtual fields saved through SetScore, confirm and regrade record actions, bulk confirm - open question type, rubric and max_length in the question form; grader dropdown on the quiz form from the grader registry - Review model with quiz, status and student filters
This commit is contained in:
@@ -2,12 +2,16 @@ package quizzes_test
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.golem15.com/golem15/sm-quizzes-plugin/models"
|
||||
"git.golem15.com/golem15/summercms/modules/bouncer"
|
||||
"git.golem15.com/golem15/summercms/modules/cabana"
|
||||
"git.golem15.com/golem15/summercms/modules/lagoon"
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/utils/tests"
|
||||
)
|
||||
|
||||
func TestAdminCompiles(t *testing.T) {
|
||||
@@ -37,7 +41,7 @@ func TestAdminCompiles(t *testing.T) {
|
||||
}
|
||||
}
|
||||
slices.Sort(ids)
|
||||
want := []string{"golem15.quizzes.answers", "golem15.quizzes.attempts", "golem15.quizzes.questions", "golem15.quizzes.quizzes"}
|
||||
want := []string{"golem15.quizzes.answers", "golem15.quizzes.attempts", "golem15.quizzes.questions", "golem15.quizzes.quizzes", "golem15.quizzes.reviews"}
|
||||
if !slices.Equal(ids, want) {
|
||||
t.Errorf("controllers = %v, want %v", ids, want)
|
||||
}
|
||||
@@ -52,17 +56,78 @@ func TestAdminCompiles(t *testing.T) {
|
||||
}
|
||||
|
||||
nav := p.(pact.HasNavigation).Navigation()
|
||||
if len(nav) != 1 || nav[0].Label != "golem15.quizzes::lang.plugin.name" || len(nav[0].SideMenu) != 4 {
|
||||
if len(nav) != 1 || nav[0].Label != "golem15.quizzes::lang.plugin.name" || len(nav[0].SideMenu) != 5 {
|
||||
t.Errorf("navigation = %+v", nav)
|
||||
} else {
|
||||
var codes []string
|
||||
for _, item := range nav[0].SideMenu {
|
||||
codes = append(codes, item.Code)
|
||||
}
|
||||
if !slices.Equal(codes, []string{"quizzes", "questions", "answers", "reviews", "attempts"}) {
|
||||
t.Errorf("side menu = %v, want reviews after answers", codes)
|
||||
}
|
||||
if review := nav[0].SideMenu[3]; review.Controller != "golem15.quizzes.reviews" || !slices.Equal(review.Permissions, []string{"golem15.quizzes.access_quizzes"}) {
|
||||
t.Errorf("reviews entry = %+v", review)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuestionFilterScopes(t *testing.T) {
|
||||
q := models.Question{}
|
||||
if got := q.FilterOptions(models.ScopeByType); len(got) != 3 {
|
||||
if got := q.FilterOptions(models.ScopeByType); len(got) != 4 || got[3].Value != models.TypeOpen {
|
||||
t.Errorf("type options = %v", got)
|
||||
}
|
||||
if got := q.FilterScope("unknown", nil, "x"); got != nil {
|
||||
t.Errorf("FilterScope on a nil db = %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReviewFilterScopes(t *testing.T) {
|
||||
r := models.Review{}
|
||||
if got := r.FilterScopes(); !slices.Equal(got, []string{models.ScopeByQuiz, models.ScopeByStatus, models.ScopeByStudent}) {
|
||||
t.Errorf("FilterScopes = %v", got)
|
||||
}
|
||||
opts := r.FilterOptions(models.ScopeByStatus)
|
||||
var values []string
|
||||
for _, o := range opts {
|
||||
values = append(values, o.Value)
|
||||
if o.Label != "golem15.quizzes::lang.reviews.status_"+o.Value {
|
||||
t.Errorf("status %s label = %q", o.Value, o.Label)
|
||||
}
|
||||
}
|
||||
if !slices.Equal(values, []string{"pending", "graded", "failed", "reviewed"}) {
|
||||
t.Errorf("status options = %v", values)
|
||||
}
|
||||
if got := r.FilterScope("unknown", nil, "x"); got != nil {
|
||||
t.Errorf("FilterScope on a nil db = %v", got)
|
||||
}
|
||||
|
||||
db, err := gorm.Open(tests.DummyDialector{}, &gorm.Config{DryRun: true})
|
||||
if err != nil {
|
||||
t.Fatalf("dry-run db: %v", err)
|
||||
}
|
||||
sqlOf := func(scope string, value any) string {
|
||||
var rows []models.Review
|
||||
stmt := r.FilterScope(scope, db.Session(&gorm.Session{DryRun: true}).Model(&models.Review{}), value).Find(&rows).Statement
|
||||
return stmt.SQL.String()
|
||||
}
|
||||
if got := sqlOf(models.ScopeByStatus, []any{"graded"}); !strings.Contains(got, "golem15_quizzes_attempt_answers.grading_status IN") {
|
||||
t.Errorf("status SQL = %s", got)
|
||||
}
|
||||
if got := sqlOf(models.ScopeByStatus, []any{"bogus"}); !strings.Contains(got, "1 = 0") {
|
||||
t.Errorf("unknown status SQL = %s, want no rows", got)
|
||||
}
|
||||
if got := sqlOf(models.ScopeByQuiz, []any{"3"}); !strings.Contains(got, "question_id IN (SELECT id FROM golem15_quizzes_questions WHERE quiz_id IN") {
|
||||
t.Errorf("quiz SQL = %s", got)
|
||||
}
|
||||
if got := sqlOf(models.ScopeByStudent, []any{"7"}); !strings.Contains(got, "attempt_id IN (SELECT id FROM golem15_quizzes_attempts WHERE user_id IN") {
|
||||
t.Errorf("student SQL = %s", got)
|
||||
}
|
||||
if got := sqlOf(models.ScopeByQuiz, nil); strings.Contains(got, "question_id IN") {
|
||||
t.Errorf("empty value SQL = %s, want no filter", got)
|
||||
}
|
||||
|
||||
if name, err := lagoon.MorphType(nil, &models.Review{}); err != nil || name == "golem15_quizzes_attempt_answers" {
|
||||
t.Errorf("Review morph type = %q, %v; it must differ from AttemptAnswer's table", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user