Skip to content

Fix 500 on student_quiz_report_v2 when a subject is fully skipped#80

Merged
pritamps merged 1 commit into
mainfrom
fix/null-accuracy-in-report-templates
May 20, 2026
Merged

Fix 500 on student_quiz_report_v2 when a subject is fully skipped#80
pritamps merged 1 commit into
mainfrom
fix/null-accuracy-in-report-templates

Conversation

@pritamps

Copy link
Copy Markdown
Contributor

Summary

Production hit a 500 on https://reports.avantifellows.org/reports/student_quiz_report/PunjabStudents_69d5060a9be5a82f7e85209e/204716. CloudWatch trace:

File "templates/student_quiz_report_v2.html", line 186
    <span class="font-semibold text-{{ color }}-600">{{ "%.1f"|format(subject.accuracy) }}%</span>
TypeError: must be real number, not NoneType

The DynamoDB item for that student has subject_performance[1].accuracy = NULL for Maths — the student skipped all 25 questions, so accuracy = correct / (correct + wrong) = 0/0 is mathematically undefined and the upstream pipeline persists it as DynamoDB NULL rather than 0. Jinja's "%.Nf"|format(...) then explodes.

Fix (two layers)

  • render_v2_report() in app/routers/student_quiz_reports.py: coerce None → 0 for percentage and accuracy on both overall_performance and each entry of subject_performance before passing the payload to the template. This is the primary fix — any consumer of the v2 payload now sees clean numerics.
  • student_quiz_report_v2.html and student_quiz_report_v2_print.html: every "%.Nf"|format(report.overall_performance.{percentage,accuracy}) and "%.Nf"|format(subject.{percentage,accuracy}) call now reads ... or 0 as a defense-in-depth fallback. The template can't crash on these fields even if a future code path skips the renderer sanitization.

Scope note: only the v2 + v2_print templates have "%.Nf"|format(...) on these fields, so those are the only files that can crash this way. The v1 (student_quiz_report.html) and v3 (student_quiz_report_v3.html) templates render the same values without format(...) and so cannot 500 on None.

Drive-by

app/routers/student_quiz_reports.py line 656 uses json.load(...) but json was never imported (introduced March 2025) — flake8 caught it when committing this change. Added import json at the top. Latent NameError on the v3 codepath if that line is ever hit.

Follow-up (separate ticket worth filing)

The v2 producer / ETL should emit 0.0 (or an explicit not_attempted: true flag) instead of DynamoDB NULL for accuracy when no questions were attempted. Existing rows with accuracy: NULL would also benefit from a one-shot backfill. I scoped this PR to a hotfix so the page stops 500-ing today; the data-quality cleanup is its own piece of work.

Test plan

  • Local: hit the v2 template with a constructed report where subject_performance[*].accuracy = None and confirm it renders 0.0% instead of 500-ing.
  • After deploy to staging: open the live URL /reports/student_quiz_report/PunjabStudents_69d5060a9be5a82f7e85209e/204716 and confirm the page renders. Maths row should show 0.0% accuracy.
  • Open a known-good report (any student with all subjects attempted) and confirm the numbers are unchanged.
  • PDF path: hit the same URL with ?format=pdf and confirm the print template also renders.

🤖 Generated with Claude Code

The v2 report templates pipe `percentage` and `accuracy` through
`"%.Nf"|format(...)`, which raises TypeError on None and surfaces as a
Lambda 500. Upstream stores these as DynamoDB NULL when no questions
were attempted in a subject (e.g. a student skipped all 25 Maths
questions: 0 correct / 0 wrong is mathematically undefined, so the
producer wrote NULL rather than 0).

Two-layer fix:
- render_v2_report() now coerces None -> 0 for `percentage` and
  `accuracy` on both `overall_performance` and each entry in
  `subject_performance`, so any consumer of the v2 payload sees clean
  numerics.
- v2 and v2_print templates additionally guard each `%.Nf|format(...)`
  call with `or 0` as a defense-in-depth fallback.

Also imports `json` at the top of routers/student_quiz_reports.py —
it was used at line 656 (chapter_to_link_map = json.load(...)) but
never imported, a latent NameError on the v3 codepath that flake8
flagged when committing this change. One-line drive-by fix.

Follow-up worth filing separately: the v2 producer / ETL should emit
0.0 (or an explicit not-attempted flag) instead of NULL for these
fields, and any existing rows with NULL accuracy should be backfilled.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@pritamps pritamps merged commit 386cb72 into main May 20, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant