- quizzes_list_quizzes and quizzes_show_quiz bound to GET /api/v1/quizzes
and /api/v1/quizzes/{slug} behind user.api_token, the per-user
golem15.quizzes.manage throttle, a scope and the access_quizzes permission
- the permission is grantable through pact.HasFrontendPermissions
- owner JSON shapes with solutions in manage_json.go, never used by a
student route
183 lines
6.4 KiB
Go
183 lines
6.4 KiB
Go
package quizzes
|
|
|
|
import (
|
|
"context"
|
|
"embed"
|
|
"io/fs"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"git.golem15.com/golem15/sm-quizzes-plugin/console"
|
|
"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/lagoon"
|
|
"git.golem15.com/golem15/summercms/modules/pact"
|
|
"git.golem15.com/golem15/summercms/modules/party"
|
|
"git.golem15.com/golem15/summercms/modules/surf"
|
|
"github.com/go-gormigrate/gormigrate/v2"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
var (
|
|
_ pact.HasConfig = (*Plugin)(nil)
|
|
_ pact.HasLang = (*Plugin)(nil)
|
|
_ pact.HasMailTemplates = (*Plugin)(nil)
|
|
_ pact.HasModels = (*Plugin)(nil)
|
|
_ pact.HasMigrations = (*Plugin)(nil)
|
|
_ pact.HasCommands = (*Plugin)(nil)
|
|
_ pact.HasJobs = (*Plugin)(nil)
|
|
_ pact.HasAdminControllers = (*Plugin)(nil)
|
|
_ pact.AdminAssets = (*Plugin)(nil)
|
|
_ pact.HasPermissions = (*Plugin)(nil)
|
|
_ pact.HasNavigation = (*Plugin)(nil)
|
|
|
|
_ pact.HasFrontendPermissions = (*Plugin)(nil)
|
|
_ surf.BucketProvider = (*Plugin)(nil)
|
|
)
|
|
|
|
//go:embed config
|
|
var configFS embed.FS
|
|
|
|
//go:embed lang
|
|
var langFS embed.FS
|
|
|
|
//go:embed views/mail
|
|
var mailFS embed.FS
|
|
|
|
//go:embed controllers/*/*.yaml models/*/*.yaml
|
|
var adminFS embed.FS
|
|
|
|
// Plugin is the golem15.quizzes plugin.
|
|
type Plugin struct {
|
|
app *backpack.App
|
|
}
|
|
|
|
func (p *Plugin) ID() string { return "golem15.quizzes" }
|
|
|
|
// Requires golem15.user: the API runs behind its jwt.auth middleware and
|
|
// attempts reference its users table.
|
|
func (p *Plugin) Requires() []string { return []string{"golem15.user"} }
|
|
|
|
// Register keeps the application, so route handlers and commands can reach
|
|
// its services, such as the database, when they run.
|
|
func (p *Plugin) Register(app *backpack.App) error {
|
|
p.app = app
|
|
return nil
|
|
}
|
|
|
|
// Boot keeps the application and points the admin filter choices at its
|
|
// database.
|
|
func (p *Plugin) Boot(app *backpack.App) error {
|
|
p.app = app
|
|
models.FilterDB = func() *gorm.DB {
|
|
if app == nil {
|
|
return nil
|
|
}
|
|
gdb, _ := app.Lookup[*gorm.DB]()
|
|
return gdb
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *Plugin) ConfigFS() fs.FS { return configFS }
|
|
func (p *Plugin) LangFS() fs.FS { return langFS }
|
|
|
|
func (p *Plugin) MailTemplatesFS() fs.FS { return mailFS }
|
|
func (p *Plugin) MailTemplates() []string { return nil }
|
|
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() }
|
|
func (p *Plugin) AdminControllers() []pact.AdminController {
|
|
return generatedAdminControllers()
|
|
}
|
|
|
|
// Commands returns the generated commands plus quizzes:import, which needs
|
|
// the database and so is built here with the plugin's withDB.
|
|
func (p *Plugin) Commands() []bonfire.Command {
|
|
return append(generatedCommands(), console.ImportCommand(p.withDB))
|
|
}
|
|
|
|
// withDB runs fn with the application's database: the one the serve command
|
|
// published, or, when a console command runs, one opened from the config for
|
|
// the duration of fn.
|
|
func (p *Plugin) withDB(ctx context.Context, fn func(*gorm.DB) error) error {
|
|
if gdb, ok := p.app.Lookup[*gorm.DB](); ok && gdb != nil {
|
|
return fn(gdb)
|
|
}
|
|
sqlDB, gdb, err := lagoon.OpenFromApp(ctx, p.app)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer sqlDB.Close()
|
|
return fn(gdb)
|
|
}
|
|
|
|
// AdminFS is the admin YAML the controllers read.
|
|
func (p *Plugin) AdminFS() fs.FS { return adminFS }
|
|
|
|
// Permissions declares the admin permissions.
|
|
func (p *Plugin) Permissions() []pact.Permission {
|
|
return []pact.Permission{
|
|
{Code: permQuizzes, Tab: "golem15.quizzes::lang.plugin.name", Label: "golem15.quizzes::lang.permissions.access_quizzes"},
|
|
{Code: "golem15.quizzes.access_attempts", Tab: "golem15.quizzes::lang.plugin.name", Label: "golem15.quizzes::lang.permissions.access_attempts"},
|
|
}
|
|
}
|
|
|
|
// FrontendPermissions declares the code the owner manage routes check (the
|
|
// MCP quiz tools), so golem15.user's User Groups editor can grant it.
|
|
// Declaring it grants nothing by itself.
|
|
func (p *Plugin) FrontendPermissions() []pact.FrontendPermission {
|
|
return []pact.FrontendPermission{{
|
|
Code: permQuizzes,
|
|
Label: "golem15.quizzes::lang.permissions.access_quizzes",
|
|
Tab: "golem15.quizzes::lang.plugin.name",
|
|
}}
|
|
}
|
|
|
|
// Buckets declares the per-user rate limit of the owner manage routes:
|
|
// building a quiz question by question is chatty, so 120 calls a minute.
|
|
// A request without a user (never after user.api_token) keys by client IP.
|
|
func (p *Plugin) Buckets() map[string]surf.Bucket {
|
|
trusted := surf.TrustedProxies(nil)
|
|
if p != nil && p.app != nil {
|
|
trusted = surf.TrustedProxies(p.app.Config)
|
|
}
|
|
byUser := func(prefix string) func(*http.Request) string {
|
|
return func(r *http.Request) string {
|
|
if u, ok := bouncer.User(r.Context()); ok && u.ID != 0 {
|
|
return prefix + "|u:" + strconv.FormatUint(uint64(u.ID), 10)
|
|
}
|
|
return prefix + "|ip:" + surf.ClientIP(r, trusted)
|
|
}
|
|
}
|
|
return map[string]surf.Bucket{
|
|
bucketManage: {Max: 120, Decay: time.Minute, Key: byUser(bucketManage)},
|
|
}
|
|
}
|
|
|
|
// Navigation declares the "Quizzes" admin menu with its side menu.
|
|
func (p *Plugin) Navigation() []pact.NavigationItem {
|
|
return []pact.NavigationItem{{
|
|
Code: "quizzes",
|
|
Label: "golem15.quizzes::lang.plugin.name",
|
|
Icon: "icon-question-circle",
|
|
Permissions: []string{"golem15.quizzes.access_quizzes", "golem15.quizzes.access_attempts"},
|
|
Controller: "golem15.quizzes.quizzes",
|
|
SideMenu: []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: "attempts", Label: "golem15.quizzes::lang.attempts.title", Icon: "icon-bar-chart", Controller: "golem15.quizzes.attempts", Permissions: []string{"golem15.quizzes.access_attempts"}},
|
|
},
|
|
}}
|
|
}
|
|
|
|
func init() {
|
|
party.Register(&Plugin{})
|
|
}
|