- 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
192 lines
6.5 KiB
Go
192 lines
6.5 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/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"`
|
|
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"`
|
|
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,
|
|
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,
|
|
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"`
|
|
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
|
|
}
|