From a5be13c2bece509968a6279d82b912c10367ba28 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Fri, 2 Oct 2026 14:44:15 +0200 Subject: [PATCH] fix: include answer_text of self_check questions in the quiz payload --- README.md | 6 +++--- api_postgres_test.go | 14 ++++++++++++-- controllers/api/json.go | 22 ++++++++++++++-------- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index d264fa4..2e53da4 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Deleting a quiz deletes its questions, answers and attempts. Deleting a question - **Rounds and shuffling.** `round_size` tells the client to present the questions in rounds of N; `shuffle_questions` shuffles the question order on every `GET /quizzes/{slug}`. - **Attempts.** A user starts an attempt, answers questions in any order (answering again overwrites the earlier answer and returns fresh feedback) and finishes it. Answers are refused after the time limit; finishing is still allowed and counts the answers given in time. - **Scoring.** A pure function in `classes` (`classes.Score`, `classes.Totals`) with optional partial credit for multiple questions and an optional penalty for wrong answers. See [Configuration](#configuration). -- **No solution leaks.** The quiz payload never contains `is_correct`, explanations or `answer_text`. Feedback reveals the solution of the question just answered; a result reveals the solutions of answered questions, and of every question once the attempt is finished. +- **No solution leaks.** The quiz payload never contains `is_correct` or explanations, and `answer_text` only for `self_check` flashcards, which have nothing to hide. Feedback reveals the solution of the question just answered; a result reveals the solutions of answered questions, and of every question once the attempt is finished. - **Admin backend.** Quizzes, Questions (filtered by quiz, category and type) and Answers (filtered by quiz) under the `golem15.quizzes.access_quizzes` permission, and a read-only Attempts list under `golem15.quizzes.access_attempts`. Polish and English labels. - **Importer.** `quizzes:import` loads a quiz from a JSON file, idempotent by slug. See [CLI commands](#cli-commands). @@ -60,7 +60,7 @@ All routes are mounted under `/_quizzes/api/v1` with the `jwt.auth` middleware o | Method and path | Handler | Does | |-----------------|---------|------| | `GET /quizzes` | `api.Handlers.ListQuizzes` | Published quizzes, by title, with the caller's statistics. Query: `context`. | -| `GET /quizzes/{slug}` | `api.Handlers.ShowQuiz` | A published quiz with its questions and answers, without solutions. Query: `category`. | +| `GET /quizzes/{slug}` | `api.Handlers.ShowQuiz` | A published quiz with its questions and answers, without solutions (a `self_check` question carries its `answer_text`). Query: `category`. | | `POST /quizzes/{slug}/attempts` | `api.Handlers.StartAttempt` | Starts an attempt. Body (optional): `{"category": "..."}`. | | `POST /attempts/{id}/answers` | `api.Handlers.Answer` | Saves or overwrites the answer to one question and returns feedback. | | `POST /attempts/{id}/finish` | `api.Handlers.Finish` | Finishes the attempt and returns the result. | @@ -115,7 +115,7 @@ The messages are translated into the request locale (`golem15.quizzes::lang.api. "categories": ["Documents", "Theory"], "questions": [ {"id": 10, "text": "What is societal resilience?", "type": "self_check", "category": "Theory", - "points": 1, "sort_order": 1, "answers": []}, + "points": 1, "sort_order": 1, "answer_text": "The ability of a society to withstand a crisis.", "answers": []}, {"id": 11, "text": "Which is a strategy document?", "type": "single", "category": "Documents", "points": 1, "sort_order": 2, "answers": [{"id": 30, "text": "National Security Strategy"}, {"id": 31, "text": "Highway Code"}]} ] diff --git a/api_postgres_test.go b/api_postgres_test.go index b062147..ae80ee3 100644 --- a/api_postgres_test.go +++ b/api_postgres_test.go @@ -134,18 +134,28 @@ func TestQuizAPIFlow(t *testing.T) { } } - // The quiz payload never reveals a solution. + // The quiz payload never reveals a solution; only self_check flashcards carry answer_text. code, raw := c.do("GET", apiPrefix+"/quizzes/swiat", token, "") if code != 200 { t.Fatalf("GET /quizzes/swiat = %d", code) } body, _ := json.Marshal(raw) - for _, leak := range []string{"is_correct", "explanation", "answer_text", "Warszawa od 1596"} { + for _, leak := range []string{"is_correct", "explanation", "Warszawa od 1596"} { if strings.Contains(string(body), leak) { t.Errorf("GET /quizzes/swiat leaks %q: %s", leak, body) } } quiz := data(raw) + for _, qi := range quiz["questions"].([]any) { + qm := qi.(map[string]any) + text, has := qm["answer_text"] + if (qm["type"] == "self_check") != has { + t.Errorf("question %v (%v): answer_text present = %v", qm["id"], qm["type"], has) + } + if has && text != "Zdolność społeczeństwa do..." { + t.Errorf("self_check answer_text = %v", text) + } + } if qs := quiz["questions"].([]any); len(qs) != 3 || qs[0].(map[string]any)["id"] != float64(f.single.ID) { t.Errorf("questions = %v, want three in sort order", qs) } diff --git a/controllers/api/json.go b/controllers/api/json.go index d7e7ed9..3a45726 100644 --- a/controllers/api/json.go +++ b/controllers/api/json.go @@ -62,15 +62,18 @@ type quizJSON struct { } // questionJSON is a question as the player sees it before answering: no -// is_correct, explanation or answer_text. +// is_correct or explanation, and no answer_text except for self_check +// flashcards, which have no solution to hide. type questionJSON struct { - ID uint `json:"id"` - Text string `json:"text"` - Type string `json:"type"` - Category *string `json:"category"` - Points int `json:"points"` - SortOrder int `json:"sort_order"` - Answers []answerJSON `json:"answers"` + ID uint `json:"id"` + Text string `json:"text"` + Type string `json:"type"` + Category *string `json:"category"` + Points int `json:"points"` + SortOrder int `json:"sort_order"` + // AnswerText is the model answer of a self_check question only. + AnswerText *string `json:"answer_text,omitempty"` + Answers []answerJSON `json:"answers"` } type answerJSON struct { @@ -110,6 +113,9 @@ func question(q models.Question) questionJSON { SortOrder: q.SortOrder, Answers: make([]answerJSON, 0, len(q.Answers)), } + if q.Type == models.TypeSelfCheck { + out.AnswerText = q.AnswerText + } for _, a := range q.Answers { out.Answers = append(out.Answers, answerJSON{ID: a.ID, Text: a.Text}) }