feat: grade open answers through a grader registry in the background
- graders registry collected at boot from every HasGraders plugin, manual first; a duplicate or reserved code fails boot - grading job on queue quizzes_grading with three attempts; a stale or reviewed answer is never touched; the last failure marks it failed - learning answers enqueue at once, tests at close; a grade recalculates a finished attempt and fires AnswerGradedEvent after commit
This commit is contained in:
69
plugin.go
69
plugin.go
@@ -3,16 +3,21 @@ package quizzes
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"errors"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.golem15.com/golem15/sm-quizzes-plugin/classes"
|
||||
"git.golem15.com/golem15/sm-quizzes-plugin/console"
|
||||
"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/conga"
|
||||
"git.golem15.com/golem15/summercms/modules/lagoon"
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
"git.golem15.com/golem15/summercms/modules/party"
|
||||
@@ -53,6 +58,8 @@ var adminFS embed.FS
|
||||
// Plugin is the golem15.quizzes plugin.
|
||||
type Plugin struct {
|
||||
app *backpack.App
|
||||
// graders is the grader registry built at Boot.
|
||||
graders *graders.Registry
|
||||
}
|
||||
|
||||
func (p *Plugin) ID() string { return "golem15.quizzes" }
|
||||
@@ -68,10 +75,17 @@ func (p *Plugin) Register(app *backpack.App) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Boot keeps the application and points the admin filter choices at its
|
||||
// database.
|
||||
// Boot keeps the application, collects the grader registry and points the
|
||||
// admin filter choices at its database.
|
||||
//
|
||||
// Grading plugins never register themselves: golem15.quizzes pulls their
|
||||
// graders through graders.HasGraders, as golem15.paymentgateway pulls
|
||||
// payment operators.
|
||||
func (p *Plugin) Boot(app *backpack.App) error {
|
||||
p.app = app
|
||||
if err := bootGraders(p, app); err != nil {
|
||||
return err
|
||||
}
|
||||
models.FilterDB = func() *gorm.DB {
|
||||
if app == nil {
|
||||
return nil
|
||||
@@ -82,6 +96,35 @@ func (p *Plugin) Boot(app *backpack.App) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
// is kept on p and published on the app as *graders.Registry.
|
||||
func bootGraders(p *Plugin, app *backpack.App) error {
|
||||
reg := graders.NewRegistry()
|
||||
if app == nil {
|
||||
p.graders = reg
|
||||
return nil
|
||||
}
|
||||
activated, _ := app.Lookup[party.Activated]()
|
||||
for _, plugin := range activated {
|
||||
if plugin == nil || plugin.ID() == p.ID() {
|
||||
continue
|
||||
}
|
||||
hg, ok := plugin.(graders.HasGraders)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, g := range hg.QuizGraders() {
|
||||
if err := reg.Add(plugin.ID(), g); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
p.graders = reg
|
||||
return app.Publish(reg)
|
||||
}
|
||||
|
||||
func (p *Plugin) ConfigFS() fs.FS { return configFS }
|
||||
func (p *Plugin) LangFS() fs.FS { return langFS }
|
||||
|
||||
@@ -91,7 +134,27 @@ func (p *Plugin) MailLayouts() map[string]string { return nil }
|
||||
|
||||
func (p *Plugin) Models() []any { return generatedModels() }
|
||||
func (p *Plugin) Migrations() []*gormigrate.Migration { return generatedMigrations() }
|
||||
func (p *Plugin) Jobs() []pact.Job { return generatedJobs() }
|
||||
|
||||
// Jobs returns the generated jobs plus the grading job: one open answer per
|
||||
// job on its own queue, GradeMaxAttempts attempts, each with GradeTimeout.
|
||||
func (p *Plugin) Jobs() []pact.Job {
|
||||
return append(generatedJobs(), conga.Job(p.gradeAnswer,
|
||||
conga.OnQueue(jobs.Queue),
|
||||
conga.MaxAttempts(classes.GradeMaxAttempts),
|
||||
conga.Timeout(classes.GradeTimeout)))
|
||||
}
|
||||
|
||||
// gradeAnswer runs one grading job on the application's database.
|
||||
func (p *Plugin) gradeAnswer(ctx context.Context, args jobs.GradeAnswerArgs) error {
|
||||
var gdb *gorm.DB
|
||||
if p.app != nil {
|
||||
gdb, _ = p.app.Lookup[*gorm.DB]()
|
||||
}
|
||||
if gdb == nil {
|
||||
return errors.New("quizzes: database unavailable")
|
||||
}
|
||||
return classes.ForApp(p.app, gdb).GradeAnswer(ctx, args.AnswerID, args.Seq)
|
||||
}
|
||||
func (p *Plugin) AdminControllers() []pact.AdminController {
|
||||
return generatedAdminControllers()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user