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:
Jakub Zych
2026-11-09 18:31:19 +01:00
parent 1ce1ebee16
commit 2ad4e8b1ec
20 changed files with 1244 additions and 9 deletions

View File

@@ -11,12 +11,14 @@ import (
"git.golem15.com/golem15/sm-quizzes-plugin/classes"
"git.golem15.com/golem15/sm-quizzes-plugin/console"
"git.golem15.com/golem15/sm-quizzes-plugin/controllers"
"git.golem15.com/golem15/sm-quizzes-plugin/graders"
"git.golem15.com/golem15/sm-quizzes-plugin/jobs"
"git.golem15.com/golem15/sm-quizzes-plugin/models"
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/bonfire"
"git.golem15.com/golem15/summercms/modules/bouncer"
"git.golem15.com/golem15/summercms/modules/cabana"
"git.golem15.com/golem15/summercms/modules/conga"
"git.golem15.com/golem15/summercms/modules/lagoon"
"git.golem15.com/golem15/summercms/modules/pact"
@@ -52,7 +54,7 @@ var langFS embed.FS
//go:embed views/mail
var mailFS embed.FS
//go:embed controllers/*/*.yaml models/*/*.yaml
//go:embed controllers/*/*.yaml controllers/*/*.htm models/*/*.yaml
var adminFS embed.FS
// Plugin is the golem15.quizzes plugin.
@@ -76,7 +78,8 @@ func (p *Plugin) Register(app *backpack.App) error {
}
// Boot keeps the application, collects the grader registry and points the
// admin filter choices at its database.
// admin hooks at it: the filter choices and the review screen at its
// database, the quiz grader dropdown at the registry.
//
// Grading plugins never register themselves: golem15.quizzes pulls their
// graders through graders.HasGraders, as golem15.paymentgateway pulls
@@ -93,9 +96,30 @@ func (p *Plugin) Boot(app *backpack.App) error {
gdb, _ := app.Lookup[*gorm.DB]()
return gdb
}
controllers.GraderOptions = p.graders.Options
controllers.Reviews = func(ctx context.Context) *classes.Service {
return reviewService(ctx, app)
}
return nil
}
// reviewService is the admin review screen's service: over the admin
// request's write transaction when there is one, so a review nests in the
// save, else over the application's database.
func reviewService(ctx context.Context, app *backpack.App) *classes.Service {
if tx, ok := cabana.TxFromContext(ctx); ok && tx != nil {
return classes.ForApp(app, tx)
}
if app == nil {
return nil
}
gdb, ok := app.Lookup[*gorm.DB]()
if !ok || gdb == nil {
return nil
}
return classes.ForApp(app, gdb)
}
// bootGraders builds the grader registry: manual first, then the graders of
// every other activated plugin that implements graders.HasGraders, in
// activation order. A duplicate or reserved code fails boot. The registry
@@ -235,6 +259,7 @@ func (p *Plugin) Navigation() []pact.NavigationItem {
{Code: "quizzes", Label: "golem15.quizzes::lang.quizzes.title", Icon: "icon-list", Controller: "golem15.quizzes.quizzes", Permissions: []string{"golem15.quizzes.access_quizzes"}},
{Code: "questions", Label: "golem15.quizzes::lang.questions.title", Icon: "icon-question", Controller: "golem15.quizzes.questions", Permissions: []string{"golem15.quizzes.access_quizzes"}},
{Code: "answers", Label: "golem15.quizzes::lang.answers.title", Icon: "icon-check-square", Controller: "golem15.quizzes.answers", Permissions: []string{"golem15.quizzes.access_quizzes"}},
{Code: "reviews", Label: "golem15.quizzes::lang.reviews.title", Icon: "icon-check", Controller: "golem15.quizzes.reviews", Permissions: []string{"golem15.quizzes.access_quizzes"}},
{Code: "attempts", Label: "golem15.quizzes::lang.attempts.title", Icon: "icon-bar-chart", Controller: "golem15.quizzes.attempts", Permissions: []string{"golem15.quizzes.access_attempts"}},
},
}}