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
This commit is contained in:
@@ -145,16 +145,23 @@ type updateQuizBody struct {
|
||||
PassingScore *int `json:"passing_score"`
|
||||
RoundSize *int `json:"round_size"`
|
||||
ShuffleQuestions *bool `json:"shuffle_questions"`
|
||||
Grader *string `json:"grader"`
|
||||
}
|
||||
|
||||
func (b updateQuizBody) input() classes.QuizInput {
|
||||
return classes.QuizInput{
|
||||
Title: b.Title, Description: b.Description, Context: b.Context, Mode: b.Mode, Navigation: b.Navigation,
|
||||
MaxAttempts: b.MaxAttempts, TimeLimitSeconds: b.TimeLimitSeconds, PassingScore: b.PassingScore,
|
||||
RoundSize: b.RoundSize, ShuffleQuestions: b.ShuffleQuestions,
|
||||
RoundSize: b.RoundSize, ShuffleQuestions: b.ShuffleQuestions, Grader: b.Grader,
|
||||
}
|
||||
}
|
||||
|
||||
// graderProblems checks a sent grader against the codes the application's
|
||||
// grader registry knows (manual and every registered grader).
|
||||
func (h *Handlers) graderProblems(in classes.QuizInput) []string {
|
||||
return classes.CheckGrader(in.Grader, classes.ForApp(h.App, nil).Graders.Codes())
|
||||
}
|
||||
|
||||
// answerQuiz counts and answers a quiz a write returned, or maps err.
|
||||
func answerQuiz(w http.ResponseWriter, r *http.Request, db *gorm.DB, status int, quiz *models.Quiz, err error) {
|
||||
if err != nil {
|
||||
@@ -174,7 +181,7 @@ func answerQuiz(w http.ResponseWriter, r *http.Request, db *gorm.DB, status int,
|
||||
// database is touched.
|
||||
func (h *Handlers) ManageCreateQuiz(w http.ResponseWriter, r *http.Request) {
|
||||
var in classes.QuizInput
|
||||
if !readManaged(w, r, &in) || refuse(w, classes.CheckQuizInput(in, true)) {
|
||||
if !readManaged(w, r, &in) || refuse(w, classes.CheckQuizInput(in, true)) || refuse(w, h.graderProblems(in)) {
|
||||
return
|
||||
}
|
||||
db, ok := h.manageDB(w)
|
||||
@@ -189,7 +196,8 @@ func (h *Handlers) ManageCreateQuiz(w http.ResponseWriter, r *http.Request) {
|
||||
// fields change.
|
||||
func (h *Handlers) ManageUpdateQuiz(w http.ResponseWriter, r *http.Request) {
|
||||
var body updateQuizBody
|
||||
if !readManaged(w, r, &body) || refuse(w, classes.CheckQuizInput(body.input(), false)) {
|
||||
if !readManaged(w, r, &body) || refuse(w, classes.CheckQuizInput(body.input(), false)) ||
|
||||
refuse(w, h.graderProblems(body.input())) {
|
||||
return
|
||||
}
|
||||
db, ok := h.manageDB(w)
|
||||
|
||||
@@ -7,6 +7,7 @@ package api
|
||||
|
||||
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"
|
||||
)
|
||||
@@ -24,6 +25,7 @@ type managedQuizJSON struct {
|
||||
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"`
|
||||
@@ -45,6 +47,8 @@ type managedQuestionJSON struct {
|
||||
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"`
|
||||
}
|
||||
|
||||
@@ -70,6 +74,7 @@ func managedQuiz(q models.Quiz, questions, attempts int) managedQuizJSON {
|
||||
PassingScore: q.PassingScore,
|
||||
ShuffleQuestions: q.ShuffleQuestions,
|
||||
RoundSize: q.RoundSize,
|
||||
Grader: quizGrader(&q),
|
||||
IsPublished: q.IsPublished,
|
||||
QuestionCount: questions,
|
||||
AttemptsCount: attempts,
|
||||
@@ -105,6 +110,8 @@ func managedQuestion(q models.Question) managedQuestionJSON {
|
||||
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 {
|
||||
@@ -116,9 +123,10 @@ func managedQuestion(q models.Question) managedQuestionJSON {
|
||||
}
|
||||
|
||||
type statsJSON struct {
|
||||
Quiz statsQuizJSON `json:"quiz"`
|
||||
Students []studentStatJSON `json:"students"`
|
||||
Questions []questionStatJSON `json:"questions"`
|
||||
Quiz statsQuizJSON `json:"quiz"`
|
||||
Unreviewed int `json:"unreviewed"`
|
||||
Students []studentStatJSON `json:"students"`
|
||||
Questions []questionStatJSON `json:"questions"`
|
||||
}
|
||||
|
||||
type statsQuizJSON struct {
|
||||
@@ -150,6 +158,8 @@ type questionStatJSON struct {
|
||||
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 {
|
||||
@@ -164,8 +174,9 @@ func quizStats(s *classes.Stats) statsJSON {
|
||||
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)),
|
||||
Unreviewed: s.Unreviewed,
|
||||
Students: make([]studentStatJSON, 0, len(s.Students)),
|
||||
Questions: make([]questionStatJSON, 0, len(s.Questions)),
|
||||
}
|
||||
for _, st := range s.Students {
|
||||
row := studentStatJSON{
|
||||
@@ -181,6 +192,7 @@ func quizStats(s *classes.Stats) statsJSON {
|
||||
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}
|
||||
@@ -189,3 +201,12 @@ func quizStats(s *classes.Stats) statsJSON {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -57,6 +57,11 @@ func TestManageWritesRefuseBadInputBeforeDatabase(t *testing.T) {
|
||||
"update slug": {h.ManageUpdateQuiz, "PATCH", "/", `{"slug":"b"}`, "slug"},
|
||||
"update is_published": {h.ManageUpdateQuiz, "PATCH", "/", `{"is_published":true}`, "is_published"},
|
||||
"update passing 101": {h.ManageUpdateQuiz, "PATCH", "/", `{"passing_score":101}`, "passing_score"},
|
||||
"create unknown grader": {h.ManageCreateQuiz, "POST", "/", `{"title":"A","grader":"zzz"}`, "grader must be one of manual"},
|
||||
"create bad grader": {h.ManageCreateQuiz, "POST", "/", `{"title":"A","grader":"Bad Grader"}`, "grader must be lowercase"},
|
||||
"update unknown grader": {h.ManageUpdateQuiz, "PATCH", "/", `{"grader":"zzz"}`, "grader must be one of manual"},
|
||||
"question open answers": {h.ManageCreateQuestion, "POST", "/", `{"text":"Q","type":"open","answers":[{"text":"a"}]}`, "an open question has no answers"},
|
||||
"question rubric single": {h.ManageCreateQuestion, "POST", "/", `{"text":"Q","type":"single","rubric":"R","answers":[{"text":"a","is_correct":true},{"text":"b"}]}`, "rubric applies to open questions only"},
|
||||
"delete without confirm": {h.ManageDeleteQuiz, "DELETE", "/", "", "confirm must repeat the quiz slug"},
|
||||
"delete wrong confirm": {h.ManageDeleteQuiz, "DELETE", "/?confirm=y", "", "confirm must repeat the quiz slug"},
|
||||
} {
|
||||
|
||||
Reference in New Issue
Block a user