From 744f2c760041e7702d1082f3e0bf8348b19e2ecb Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 10 Nov 2025 12:37:13 +0000 Subject: [PATCH] Add primary answer display to exam review question fragment and ensure safe rendering of question text --- .../exam_review_question_fragment.html | 3 + .../templates/generic/exam_review_start.html | 2 +- generic/views.py | 96 +++++++++++++++---- 3 files changed, 82 insertions(+), 19 deletions(-) diff --git a/anatomy/templates/anatomy/partials/exam_review_question_fragment.html b/anatomy/templates/anatomy/partials/exam_review_question_fragment.html index 62bb401d..6e8cc5b9 100644 --- a/anatomy/templates/anatomy/partials/exam_review_question_fragment.html +++ b/anatomy/templates/anatomy/partials/exam_review_question_fragment.html @@ -4,6 +4,9 @@
+

+ Answer: {{ question.get_primary_answer }} +

View full question details diff --git a/generic/templates/generic/exam_review_start.html b/generic/templates/generic/exam_review_start.html index 19d4fdb9..efff5964 100644 --- a/generic/templates/generic/exam_review_start.html +++ b/generic/templates/generic/exam_review_start.html @@ -17,7 +17,7 @@

-
Question {{ qs.q_index|add:1 }}: {{ qs.question }}
+
Question {{ qs.q_index|add:1 }}: {{ qs.question |safe }}
{% if qs.correct_pct is not None %}{{ qs.correct_pct }}% correct{% elif qs.answered_pct is not None %}{{ qs.answered_pct }}% answered{% else %}No data{% endif %}
diff --git a/generic/views.py b/generic/views.py index 1fe96f2b..ecefae02 100644 --- a/generic/views.py +++ b/generic/views.py @@ -1214,15 +1214,44 @@ class ExamViews(View, LoginRequiredMixin): else: pcts = {k: 0.0 for k in counts.keys()} - # Compute percent correct where a best_answer exists + # Compute percent correct using per-answer scoring when available. + # Different apps use different scoring scales (e.g. anatomy/rapids=2, longs=8, + # physics/shorts=5, sbas=1). We determine the per-question maximum based on + # the app and count user answers whose get_answer_score() equals that max. correct_pct = None correct_count = None - if hasattr(q, "best_answer") and getattr(q, "best_answer", None): - try: - correct_count = ua_qs.filter(answer=q.best_answer).count() + try: + # Determine per-question maximum score for this app + if self.app_name in ("rapids", "anatomy"): + per_question_max = 2 + elif self.app_name == "longs": + per_question_max = 8 + elif self.app_name in ("physics", "shorts"): + per_question_max = 5 + else: + per_question_max = 1 + + # If we have responses, iterate and count fully-correct answers using + # the model's get_answer_score() which encapsulates per-app logic. + if total_responses: + cnt = 0 + for ua in ua_qs: + try: + score = ua.get_answer_score() + except Exception: + # If get_answer_score fails for any answer, skip it + continue + if score == per_question_max: + cnt += 1 + correct_count = cnt correct_pct = round(100.0 * correct_count / total_responses, 1) if total_responses else 0.0 - except Exception: - correct_pct = None + else: + correct_count = 0 + correct_pct = 0.0 + except Exception: + # Best-effort: fall back to None if something unexpected happens + correct_pct = None + correct_count = None # Percent answered relative to candidate pool (if known) answered_pct = None @@ -1343,20 +1372,51 @@ class ExamViews(View, LoginRequiredMixin): for k in counts.keys(): pcts[k] = 0.0 - # If the question exposes a best_answer, compute correct count + # Compute correct count using per-answer scoring when available. + # Different apps use different scoring scales (e.g. anatomy/rapids=2, + # longs=8, physics/shorts=5, sbas=1). Determine per-question max + # and count answers where get_answer_score() equals that max. correct_count = None - if hasattr(question, "best_answer") and question.best_answer: - correct_count = ua_qs.filter(answer=question.best_answer).count() - else: - correct_count = None - - # percent correct correct_pct = None - if correct_count is not None and total_responses: - try: - correct_pct = round(100.0 * correct_count / total_responses, 1) - except Exception: - correct_pct = None + try: + if self.app_name in ("rapids", "anatomy"): + per_question_max = 2 + elif self.app_name == "longs": + per_question_max = 8 + elif self.app_name in ("physics", "shorts"): + per_question_max = 5 + else: + per_question_max = 1 + + if total_responses: + cnt = 0 + for ua in ua_qs: + try: + score = ua.get_answer_score() + except Exception: + continue + + # Handle tuple/list scores (physics) by summing parts + if isinstance(score, (list, tuple)): + try: + ssum = sum([s for s in score if isinstance(s, (int, float))]) + except Exception: + continue + if ssum == per_question_max: + cnt += 1 + else: + # Numeric scores only; skip 'unmarked' / None + if isinstance(score, (int, float)) and score == per_question_max: + cnt += 1 + + correct_count = cnt + correct_pct = round(100.0 * correct_count / total_responses, 1) if total_responses else 0.0 + else: + correct_count = 0 + correct_pct = 0.0 + except Exception: + correct_count = None + correct_pct = None context.update( {