feat: create, publish, import and report on quizzes over MCP

- quizzes_create_quiz, update_quiz, delete_quiz (confirm repeats the
  slug), publish, unpublish, import_quiz (the existing import route) and
  quiz_stats on new manage routes
- stats close expired open test attempts first, then report per student
  over completed whole-quiz attempts and per question over every
  completed attempt
- input is checked before the database; TestManageAPIAgainstDatabase is
  written and skipped under -short
This commit is contained in:
Jakub Zych
2026-11-09 14:30:31 +01:00
parent e615cc88db
commit 4a60eda425
10 changed files with 1275 additions and 5 deletions

View File

@@ -114,3 +114,78 @@ func managedQuestion(q models.Question) managedQuestionJSON {
}
return out
}
type statsJSON struct {
Quiz statsQuizJSON `json:"quiz"`
Students []studentStatJSON `json:"students"`
Questions []questionStatJSON `json:"questions"`
}
type statsQuizJSON struct {
ID uint `json:"id"`
Slug string `json:"slug"`
Title string `json:"title"`
Mode string `json:"mode"`
PassingScore int `json:"passing_score"`
QuestionCount int `json:"question_count"`
}
type studentStatJSON struct {
UserID uint `json:"user_id"`
Email string `json:"email"`
Name *string `json:"name"`
Surname *string `json:"surname"`
Attempts int `json:"attempts"`
BestScorePercent *float64 `json:"best_score_percent"`
LastScorePercent *float64 `json:"last_score_percent"`
LastCompletedAt *wire.Time `json:"last_completed_at"`
Passed bool `json:"passed"`
}
type questionStatJSON struct {
ID uint `json:"id"`
Text string `json:"text"`
Type string `json:"type"`
Answered int `json:"answered"`
Correct int `json:"correct"`
CorrectPercent *float64 `json:"correct_percent"`
MostChosenWrong *wrongAnswerJSON `json:"most_chosen_wrong"`
}
type wrongAnswerJSON struct {
AnswerID uint `json:"answer_id"`
Text string `json:"text"`
Count int `json:"count"`
}
func quizStats(s *classes.Stats) statsJSON {
out := statsJSON{
Quiz: statsQuizJSON{
ID: s.Quiz.ID, Slug: s.Quiz.Slug, Title: s.Quiz.Title, Mode: quizMode(&s.Quiz),
PassingScore: s.Quiz.PassingScore, QuestionCount: s.QuestionCount,
},
Students: make([]studentStatJSON, 0, len(s.Students)),
Questions: make([]questionStatJSON, 0, len(s.Questions)),
}
for _, st := range s.Students {
row := studentStatJSON{
UserID: st.UserID, Email: st.Email, Name: st.Name, Surname: st.Surname, Attempts: st.Attempts,
BestScorePercent: st.BestScorePercent, LastScorePercent: st.LastScorePercent, Passed: st.Passed,
}
if st.LastCompletedAt != nil {
t := wireTime(*st.LastCompletedAt)
row.LastCompletedAt = &t
}
out.Students = append(out.Students, row)
}
for _, q := range s.Questions {
row := questionStatJSON{
ID: q.ID, Text: q.Text, Type: q.Type, Answered: q.Answered, Correct: q.Correct, CorrectPercent: q.CorrectPercent,
}
if q.MostChosenWrong != nil {
row.MostChosenWrong = &wrongAnswerJSON{AnswerID: q.MostChosenWrong.AnswerID, Text: q.MostChosenWrong.Text, Count: q.MostChosenWrong.Count}
}
out.Questions = append(out.Questions, row)
}
return out
}