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 }