feat: list and show quizzes over MCP for their owner
- 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
This commit is contained in:
89
controllers/api/manage.go
Normal file
89
controllers/api/manage.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package api
|
||||
|
||||
// The owner manage routes under /api/v1/quizzes, behind user.api_token, the
|
||||
// golem15.quizzes.manage throttle, a token scope and the access_quizzes
|
||||
// permission. The MCP quiz tools bind them. Answers use the owner shapes of
|
||||
// manage_json.go, which carry the solutions.
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.golem15.com/golem15/sm-quizzes-plugin/classes"
|
||||
"git.golem15.com/golem15/summercms/modules/wire"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// manageDB returns the application database, or answers 503 as the student
|
||||
// routes do without one.
|
||||
func (h *Handlers) manageDB(w http.ResponseWriter) (*gorm.DB, bool) {
|
||||
var gdb *gorm.DB
|
||||
if h.App != nil {
|
||||
gdb, _ = h.App.Lookup[*gorm.DB]()
|
||||
}
|
||||
if gdb == nil {
|
||||
wire.WriteJSON(w, http.StatusServiceUnavailable, map[string]any{"error": "Database unavailable"})
|
||||
return nil, false
|
||||
}
|
||||
return gdb, true
|
||||
}
|
||||
|
||||
// writeManaged answers {data: data}, or maps err: ErrNotFound is a 404,
|
||||
// ErrQuizExists a 409, anything else an opaque 500.
|
||||
func writeManaged(w http.ResponseWriter, status int, data any, err error) {
|
||||
switch {
|
||||
case err == nil:
|
||||
wire.WriteJSON(w, status, map[string]any{"data": data})
|
||||
case errors.Is(err, classes.ErrNotFound):
|
||||
wire.WriteJSON(w, http.StatusNotFound, map[string]any{"error": true, "message": "Quiz not found."})
|
||||
case errors.Is(err, classes.ErrQuizExists):
|
||||
wire.WriteJSON(w, http.StatusConflict, map[string]any{"error": true, "message": "A quiz with this slug already exists."})
|
||||
default:
|
||||
wire.WriteOpaque500(w)
|
||||
}
|
||||
}
|
||||
|
||||
// ManageListQuizzes answers GET /api/v1/quizzes?context=: every quiz,
|
||||
// published or not, with its question and completed-attempt counts.
|
||||
func (h *Handlers) ManageListQuizzes(w http.ResponseWriter, r *http.Request) {
|
||||
db, ok := h.manageDB(w)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
rows, err := classes.ListManagedQuizzes(r.Context(), db, strings.TrimSpace(r.URL.Query().Get("context")))
|
||||
if err != nil {
|
||||
writeManaged(w, 0, nil, err)
|
||||
return
|
||||
}
|
||||
out := make([]managedQuizJSON, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
out = append(out, managedQuizRow(row))
|
||||
}
|
||||
writeManaged(w, http.StatusOK, out, nil)
|
||||
}
|
||||
|
||||
// ManageShowQuiz answers GET /api/v1/quizzes/{slug}: the quiz with every
|
||||
// question and answer, solutions included.
|
||||
func (h *Handlers) ManageShowQuiz(w http.ResponseWriter, r *http.Request) {
|
||||
db, ok := h.manageDB(w)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
h.writeManagedQuiz(w, r, db, http.StatusOK, r.PathValue("slug"))
|
||||
}
|
||||
|
||||
// writeManagedQuiz answers the quiz with its questions and solutions.
|
||||
func (h *Handlers) writeManagedQuiz(w http.ResponseWriter, r *http.Request, db *gorm.DB, status int, slug string) {
|
||||
quiz, err := classes.ManagedQuiz(r.Context(), db, slug)
|
||||
if err != nil {
|
||||
writeManaged(w, 0, nil, err)
|
||||
return
|
||||
}
|
||||
attempts, err := classes.CompletedAttemptCount(r.Context(), db, quiz.ID)
|
||||
if err != nil {
|
||||
writeManaged(w, 0, nil, err)
|
||||
return
|
||||
}
|
||||
writeManaged(w, status, managedQuizDetail(*quiz, attempts), nil)
|
||||
}
|
||||
Reference in New Issue
Block a user