Files
sm-quizzes-plugin/classes/manage.go
Jakub Zych e615cc88db 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
2026-11-09 14:25:07 +01:00

98 lines
3.1 KiB
Go

package classes
// Owner-side management behind the manage routes under /api/v1/quizzes,
// which the MCP quiz tools bind. There is no owner column: a user whose
// groups grant golem15.quizzes.access_quizzes manages every quiz, as in the
// admin (A7). Quizzes are addressed by slug.
import (
"context"
"errors"
"git.golem15.com/golem15/sm-quizzes-plugin/models"
"gorm.io/gorm"
)
// ManagedQuizRow is one quiz of the owner list with its question count and
// the number of completed attempts (of every user).
type ManagedQuizRow struct {
Quiz models.Quiz
QuestionCount int
AttemptsCount int
}
// ListManagedQuizzes returns every quiz, published or not, optionally only
// those whose context equals quizContext, ordered by context (quizzes
// without one last), title and id.
func ListManagedQuizzes(ctx context.Context, db *gorm.DB, quizContext string) ([]ManagedQuizRow, error) {
db = db.WithContext(ctx)
q := db.Model(&models.Quiz{})
if quizContext != "" {
q = q.Where("context = ?", quizContext)
}
var quizzes []models.Quiz
if err := q.Order("context ASC NULLS LAST, title ASC, id ASC").Find(&quizzes).Error; err != nil {
return nil, err
}
out := make([]ManagedQuizRow, 0, len(quizzes))
if len(quizzes) == 0 {
return out, nil
}
ids := make([]uint, len(quizzes))
for i, quiz := range quizzes {
ids[i] = quiz.ID
}
type countRow struct {
QuizID uint
N int
}
var questions, attempts []countRow
if err := db.Model(&models.Question{}).Select("quiz_id, COUNT(*) AS n").
Where("quiz_id IN ?", ids).Group("quiz_id").Scan(&questions).Error; err != nil {
return nil, err
}
if err := db.Model(&models.Attempt{}).Select("quiz_id, COUNT(*) AS n").
Where("quiz_id IN ? AND completed_at IS NOT NULL", ids).Group("quiz_id").Scan(&attempts).Error; err != nil {
return nil, err
}
questionsBy := map[uint]int{}
for _, c := range questions {
questionsBy[c.QuizID] = c.N
}
attemptsBy := map[uint]int{}
for _, c := range attempts {
attemptsBy[c.QuizID] = c.N
}
for _, quiz := range quizzes {
out = append(out, ManagedQuizRow{Quiz: quiz, QuestionCount: questionsBy[quiz.ID], AttemptsCount: attemptsBy[quiz.ID]})
}
return out, nil
}
// ManagedQuiz loads a quiz, published or not, with every question in sort
// order and every answer of each, solutions included. A missing slug is
// ErrNotFound.
func ManagedQuiz(ctx context.Context, db *gorm.DB, slug string) (*models.Quiz, error) {
var quiz models.Quiz
err := db.WithContext(ctx).
Preload("Questions", func(tx *gorm.DB) *gorm.DB { return tx.Order("sort_order ASC, id ASC") }).
Preload("Questions.Answers", func(tx *gorm.DB) *gorm.DB { return tx.Order("sort_order ASC, id ASC") }).
Where("slug = ?", slug).Take(&quiz).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrNotFound
}
if err != nil {
return nil, err
}
return &quiz, nil
}
// CompletedAttemptCount counts the completed attempts of every user on a
// quiz.
func CompletedAttemptCount(ctx context.Context, db *gorm.DB, quizID uint) (int, error) {
var n int64
err := db.WithContext(ctx).Model(&models.Attempt{}).
Where("quiz_id = ? AND completed_at IS NOT NULL", quizID).Count(&n).Error
return int(n), err
}