Files
sm-quizzes-plugin/controllers/api/manage_json.go
Jakub Zych 9688b4d552 feat: carry open questions and the grader through import and the quiz tools
- import document, manage routes and MCP question tools take rubric and
  max_length; open questions refuse answers, max_length is 1..20000 and
  both apply to open questions only
- quizzes take a grader; the manage routes refuse a code the registry
  does not know, the import accepts any well-formed code
- quiz stats report unreviewed open answers and their average points
- import_quiz warns that replace deletes the grades and reviews
2026-11-09 18:36:20 +01:00

213 lines
7.2 KiB
Go

package api
// The owner shapes of the manage routes. They carry the solutions
// (is_correct, explanations, answer_text) and are never used by a student
// route; the student shapes live in json.go. Built field by field, so a new
// column never leaks.
import (
"git.golem15.com/golem15/sm-quizzes-plugin/classes"
"git.golem15.com/golem15/sm-quizzes-plugin/graders"
"git.golem15.com/golem15/sm-quizzes-plugin/models"
"git.golem15.com/golem15/summercms/modules/wire"
)
type managedQuizJSON struct {
ID uint `json:"id"`
Slug string `json:"slug"`
Title string `json:"title"`
Description *string `json:"description"`
Context *string `json:"context"`
Mode string `json:"mode"`
Navigation string `json:"navigation"`
MaxAttempts *int `json:"max_attempts"`
TimeLimitSeconds *int `json:"time_limit_seconds"`
PassingScore int `json:"passing_score"`
ShuffleQuestions bool `json:"shuffle_questions"`
RoundSize *int `json:"round_size"`
Grader string `json:"grader"`
IsPublished bool `json:"is_published"`
QuestionCount int `json:"question_count"`
AttemptsCount int `json:"attempts_count"`
CreatedAt wire.Time `json:"created_at"`
UpdatedAt wire.Time `json:"updated_at"`
}
type managedQuizDetailJSON struct {
managedQuizJSON
Questions []managedQuestionJSON `json:"questions"`
}
type managedQuestionJSON struct {
ID uint `json:"id"`
Text string `json:"text"`
Type string `json:"type"`
Category *string `json:"category"`
AnswerText *string `json:"answer_text"`
Explanation *string `json:"explanation"`
Points int `json:"points"`
SortOrder int `json:"sort_order"`
Rubric *string `json:"rubric"`
MaxLength *int `json:"max_length"`
Answers []managedAnswerJSON `json:"answers"`
}
type managedAnswerJSON struct {
ID uint `json:"id"`
Text string `json:"text"`
IsCorrect bool `json:"is_correct"`
Explanation *string `json:"explanation"`
SortOrder int `json:"sort_order"`
}
func managedQuiz(q models.Quiz, questions, attempts int) managedQuizJSON {
return managedQuizJSON{
ID: q.ID,
Slug: q.Slug,
Title: q.Title,
Description: q.Description,
Context: q.Context,
Mode: quizMode(&q),
Navigation: quizNavigation(&q),
MaxAttempts: q.MaxAttempts,
TimeLimitSeconds: q.TimeLimitSeconds,
PassingScore: q.PassingScore,
ShuffleQuestions: q.ShuffleQuestions,
RoundSize: q.RoundSize,
Grader: quizGrader(&q),
IsPublished: q.IsPublished,
QuestionCount: questions,
AttemptsCount: attempts,
CreatedAt: wireTime(q.CreatedAt),
UpdatedAt: wireTime(q.UpdatedAt),
}
}
func managedQuizRow(row classes.ManagedQuizRow) managedQuizJSON {
return managedQuiz(row.Quiz, row.QuestionCount, row.AttemptsCount)
}
// managedQuizDetail is a quiz loaded by classes.ManagedQuiz with its
// questions; attempts is its completed-attempt count.
func managedQuizDetail(q models.Quiz, attempts int) managedQuizDetailJSON {
out := managedQuizDetailJSON{
managedQuizJSON: managedQuiz(q, len(q.Questions), attempts),
Questions: make([]managedQuestionJSON, 0, len(q.Questions)),
}
for _, question := range q.Questions {
out.Questions = append(out.Questions, managedQuestion(question))
}
return out
}
func managedQuestion(q models.Question) managedQuestionJSON {
out := managedQuestionJSON{
ID: q.ID,
Text: q.Text,
Type: q.Type,
Category: q.Category,
AnswerText: q.AnswerText,
Explanation: q.Explanation,
Points: q.Points,
SortOrder: q.SortOrder,
Rubric: q.Rubric,
MaxLength: q.MaxLength,
Answers: make([]managedAnswerJSON, 0, len(q.Answers)),
}
for _, a := range q.Answers {
out.Answers = append(out.Answers, managedAnswerJSON{
ID: a.ID, Text: a.Text, IsCorrect: a.IsCorrect, Explanation: a.Explanation, SortOrder: a.SortOrder,
})
}
return out
}
type statsJSON struct {
Quiz statsQuizJSON `json:"quiz"`
Unreviewed int `json:"unreviewed"`
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"`
Unreviewed int `json:"unreviewed"`
AveragePoints *float64 `json:"average_points"`
}
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,
},
Unreviewed: s.Unreviewed,
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,
Unreviewed: q.Unreviewed, AveragePoints: q.AveragePoints,
}
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
}
// quizGrader is the quiz's grader code, manual for a quiz read before the
// column had a value.
func quizGrader(q *models.Quiz) string {
if q.Grader == "" {
return graders.Manual
}
return q.Grader
}