- 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
134 lines
5.0 KiB
Go
134 lines
5.0 KiB
Go
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) {
|
|
app, plugins := activate(t, nil, nil)
|
|
routes, err := cabana.Activate(app, plugins)
|
|
if err != nil {
|
|
t.Fatalf("cabana.Activate: %v", err)
|
|
}
|
|
if routes == nil {
|
|
t.Fatal("no admin routes")
|
|
}
|
|
p := quizzesPlugin(t, plugins)
|
|
fsys := p.(pact.AdminAssets).AdminFS()
|
|
var ids []string
|
|
for _, ctl := range p.(pact.HasAdminControllers).AdminControllers() {
|
|
ids = append(ids, ctl.ID())
|
|
if _, err := cabana.CompileList(p.ID(), ctl, fsys); err != nil {
|
|
t.Errorf("%s list: %v", ctl.ID(), err)
|
|
}
|
|
_, formErr := cabana.CompileForm(p.ID(), ctl, fsys)
|
|
if ctl.ID() == "golem15.quizzes.attempts" {
|
|
if formErr == nil {
|
|
t.Error("attempts has a form; it must stay read-only")
|
|
}
|
|
} else if formErr != nil {
|
|
t.Errorf("%s form: %v", ctl.ID(), formErr)
|
|
}
|
|
}
|
|
slices.Sort(ids)
|
|
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)
|
|
}
|
|
|
|
editor := &bouncer.Principal{ID: 1, Backend: true, PermissionGrants: map[string]bool{"golem15.quizzes.access_quizzes": true}}
|
|
for _, ctl := range p.(pact.HasAdminControllers).AdminControllers() {
|
|
perms := ctl.(pact.AdminPermissioned).RequiredPermissions()
|
|
allowed := cabana.Allows(editor, perms)
|
|
if wantAllowed := ctl.ID() != "golem15.quizzes.attempts"; allowed != wantAllowed {
|
|
t.Errorf("%s allowed for a quiz editor = %v, want %v", ctl.ID(), allowed, wantAllowed)
|
|
}
|
|
}
|
|
|
|
nav := p.(pact.HasNavigation).Navigation()
|
|
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) != 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)
|
|
}
|
|
}
|