Reading Journey: quiz answer review never shows correct answers/explanations (no backend endpoint ships the answer key) #663

Open
opened 2026-07-17 16:19:01 +00:00 by spikerj · 3 comments
Owner

Found while enabling strictTemplates (#662). The reading-journey quiz review UI reads correctAnswerIndex/explanation from QuestionForTakingDto, but that DTO intentionally excludes answers ("excluded for security" per backend comment) and no controller endpoint serves the existing QuizForReviewDto. Result: the correct-answer highlight and explanations have always rendered as undefined — the review feature is visually non-functional.

Frontend is now honestly typed (type-guard helpers correctAnswerIndexOf()/explanationOf() in reading-journey.component.ts), so wiring this up needs:

  1. Backend: a post-completion review endpoint returning QuizForReviewDto (DTO already exists, unused)
  2. Frontend: fetch review data after submission and pass it to the review template (the guards will pick it up)
Found while enabling strictTemplates (#662). The reading-journey quiz review UI reads `correctAnswerIndex`/`explanation` from `QuestionForTakingDto`, but that DTO intentionally excludes answers ("excluded for security" per backend comment) and no controller endpoint serves the existing `QuizForReviewDto`. Result: the correct-answer highlight and explanations have always rendered as undefined — the review feature is visually non-functional. Frontend is now honestly typed (type-guard helpers `correctAnswerIndexOf()`/`explanationOf()` in reading-journey.component.ts), so wiring this up needs: 1. Backend: a post-completion review endpoint returning `QuizForReviewDto` (DTO already exists, unused) 2. Frontend: fetch review data after submission and pass it to the review template (the guards will pick it up)
Author
Owner

Backend half up as PR #419 (mergeable). Claim verified: QuizForReviewDto/ToReviewDto() existed with the full answer key but had zero controller callers.

New GET api/quiz/{id}/review: ownership via the same GetQuizQuery-by-username pattern as GetQuiz; 403 until the caller has a completed attempt (new IQuizAttemptService.HasCompletedAttempt) so the taking-DTO's answer exclusion can't be bypassed mid-quiz; 200 → ToReviewDto(). 4 controller tests incl. an explicit no-answer-leak assertion on the 403 path.

Remaining (this ticket stays open): the spikersoft-angular half — fetch /review after submission and feed the existing correctAnswerIndexOf()/explanationOf() guards in reading-journey.component.ts.

**Backend half up as PR [#419](https://git.spikersoft.com/spikerj/spikersoft-backend/pulls/419)** (mergeable). Claim verified: `QuizForReviewDto`/`ToReviewDto()` existed with the full answer key but had zero controller callers. New `GET api/quiz/{id}/review`: ownership via the same `GetQuizQuery`-by-username pattern as `GetQuiz`; **403 until the caller has a completed attempt** (new `IQuizAttemptService.HasCompletedAttempt`) so the taking-DTO's answer exclusion can't be bypassed mid-quiz; 200 → `ToReviewDto()`. 4 controller tests incl. an explicit no-answer-leak assertion on the 403 path. **Remaining (this ticket stays open):** the spikersoft-angular half — fetch `/review` after submission and feed the existing `correctAnswerIndexOf()`/`explanationOf()` guards in `reading-journey.component.ts`.
Author
Owner

Sweep note: backend PR #419 merged the post-completion review endpoint with the answer key, but I could not find frontend code consuming it in reading-journey — the user-facing 'never shows correct answers' symptom may persist. Leaving open pending the UI half (or evidence it exists).

Sweep note: backend PR #419 merged the post-completion review endpoint with the answer key, but I could not find frontend code consuming it in reading-journey — the user-facing 'never shows correct answers' symptom may persist. Leaving open pending the UI half (or evidence it exists).
Author
Owner

Audited against origin/master in both repos — the backend half shipped, the Angular half never did. Staying open. The ticket title's premise ("no backend endpoint ships the answer key") is now stale, so updating the notes:

Backend: DONE. spikersoft-backend PR #419 (d06150b6, merge 9f335874).

  • QuizController.cs:85[HttpGet("{id}/review")] GetQuizReview, returning QuizForReviewDto.
  • Correctly gated: :110 checks _quizAttemptService.HasCompletedAttempt(id, username) and returns 403 otherwise, so the taking DTO's answer-exclusion can't be bypassed mid-quiz. Ownership still resolved through GetQuizQuery (404 on miss).
  • DTOs split as intended: QuizDto.cs:118 keeps CorrectAnswerIndex/Explanation excluded from the taking shape; :145-146 adds them to the review shape.

Angular: NOT DONE — nothing calls the endpoint. git grep -nE "/review|getQuizReview|QuizForReview" origin/master -- '*.ts' over spikersoft-angular returns no quiz hits at all (only an art-studio review link and an unrelated geography.service.ts:316 facts-review POST).

The consequence is a UI that silently renders the empty state forever:

  • reading-journey.component.ts:724-726correctAnswerIndexOf() is return "correctAnswerIndex" in question ? question.correctAnswerIndex : null;, and :728-730 explanationOf() is the same shape. Since the client only ever fetches the taking DTO, that in check is always false and both helpers always return null.
  • So reading-journey.component.html:428 [class.correct-answer]="$index === correctAnswerIndexOf(question)" never matches any option, and :277 keeps rendering readingQuiz.explanationPending"Explanation will be available after quiz completion" (en.json:1752) — which is now a false promise: completion happens, the explanation still never arrives.

Remaining work: add a getQuizReview(id) call to the quiz service, fetch the review DTO once the attempt is complete, and feed it to the review pane so correctAnswerIndexOf/explanationOf receive a QuizQuestion rather than a QuestionForTakingDto. Then drop or re-word explanationPending.

Note the identical dead-helper pattern exists at aptituderator.component.ts:699 — worth fixing in the same pass, or filing separately if aptituderator has no review endpoint of its own.

Audited against `origin/master` in both repos — **the backend half shipped, the Angular half never did.** Staying open. The ticket title's premise ("no backend endpoint ships the answer key") is now stale, so updating the notes: **Backend: DONE.** spikersoft-backend PR #419 (`d06150b6`, merge `9f335874`). - `QuizController.cs:85` — `[HttpGet("{id}/review")] GetQuizReview`, returning `QuizForReviewDto`. - Correctly gated: `:110` checks `_quizAttemptService.HasCompletedAttempt(id, username)` and returns **403** otherwise, so the taking DTO's answer-exclusion can't be bypassed mid-quiz. Ownership still resolved through `GetQuizQuery` (404 on miss). - DTOs split as intended: `QuizDto.cs:118` keeps `CorrectAnswerIndex`/`Explanation` excluded from the taking shape; `:145-146` adds them to the review shape. **Angular: NOT DONE — nothing calls the endpoint.** `git grep -nE "/review|getQuizReview|QuizForReview" origin/master -- '*.ts'` over spikersoft-angular returns **no quiz hits at all** (only an art-studio review *link* and an unrelated `geography.service.ts:316` facts-review POST). The consequence is a UI that silently renders the empty state forever: - `reading-journey.component.ts:724-726` — `correctAnswerIndexOf()` is `return "correctAnswerIndex" in question ? question.correctAnswerIndex : null;`, and `:728-730` `explanationOf()` is the same shape. Since the client only ever fetches the *taking* DTO, that `in` check is always false and both helpers always return `null`. - So `reading-journey.component.html:428` `[class.correct-answer]="$index === correctAnswerIndexOf(question)"` never matches any option, and `:277` keeps rendering `readingQuiz.explanationPending` — *"Explanation will be available after quiz completion"* (`en.json:1752`) — which is now a false promise: completion happens, the explanation still never arrives. **Remaining work:** add a `getQuizReview(id)` call to the quiz service, fetch the review DTO once the attempt is complete, and feed it to the review pane so `correctAnswerIndexOf`/`explanationOf` receive a `QuizQuestion` rather than a `QuestionForTakingDto`. Then drop or re-word `explanationPending`. Note the identical dead-helper pattern exists at `aptituderator.component.ts:699` — worth fixing in the same pass, or filing separately if aptituderator has no review endpoint of its own.
Sign in to join this conversation.