From 092c4a1be66b90b3135ec0ce18ab837b6b08ab3b Mon Sep 17 00:00:00 2001 From: Ross Date: Sun, 9 Nov 2025 22:06:56 +0000 Subject: [PATCH] Add aggregated response summary and user answer filtering in exam review --- generic/views.py | 47 ++++++++++++++++ .../exam_review_question_fragment.html | 47 +++++++++++++++- ...am_review_question_responses_fragment.html | 55 +++++++++---------- sbas/views.py | 8 +++ 4 files changed, 128 insertions(+), 29 deletions(-) diff --git a/generic/views.py b/generic/views.py index a7b1839c..c8985536 100644 --- a/generic/views.py +++ b/generic/views.py @@ -1216,6 +1216,53 @@ class ExamViews(View, LoginRequiredMixin): "can_edit": self.check_user_edit_access(request.user, exam_id=pk), } + # Compute aggregated response summary for this question within the current exam. + # Add counts and percentages to the context so fragments (e.g. SBAS) can display them. + try: + if hasattr(question, "cid_user_answers"): + ua_qs = question.cid_user_answers.filter(exam=exam) + total_responses = ua_qs.count() + counts = {} + for ans in ua_qs.values_list("answer", flat=True): + counts[ans] = counts.get(ans, 0) + 1 + + # Compute percentages per answer key + pcts = {} + if total_responses: + for k, v in counts.items(): + pcts[k] = round(100.0 * v / total_responses, 1) + else: + for k in counts.keys(): + pcts[k] = 0.0 + + # If the question exposes a best_answer, compute correct count + 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 + + context.update( + { + "exam_response_counts": counts, + "exam_response_pcts": pcts, + "exam_response_total": total_responses, + "exam_response_correct_count": correct_count, + "exam_response_correct_pct": correct_pct, + } + ) + except Exception: + # Best-effort: don't fail the review page if aggregation errors occur + pass + # Choose an app-specific fragment if present, otherwise fall back to # the generic fragment. Expose the chosen fragment name in the # context so the full-page render can include it; return the fragment diff --git a/sbas/templates/sbas/partials/exam_review_question_fragment.html b/sbas/templates/sbas/partials/exam_review_question_fragment.html index ef09f971..980cf1a9 100644 --- a/sbas/templates/sbas/partials/exam_review_question_fragment.html +++ b/sbas/templates/sbas/partials/exam_review_question_fragment.html @@ -109,6 +109,51 @@ View full question details

+ {# Aggregated summary: always shown if aggregation context is available #} +
+
Response summary
+ {% if exam_response_total %} +
Total responses: {{ exam_response_total }}
+
+
Correct:
+
+ {% if exam_response_correct_count is not None %} + {{ exam_response_correct_count }} + {% if exam_response_correct_pct is not None %} + ({{ exam_response_correct_pct }}%) + {% endif %} + {% else %} + N/A + {% endif %} +
+
+
+
+
A
+
{{ exam_response_counts.a|default:0 }}{{ exam_response_pcts.a|default:0 }}%
+
+
+
B
+
{{ exam_response_counts.b|default:0 }}{{ exam_response_pcts.b|default:0 }}%
+
+
+
C
+
{{ exam_response_counts.c|default:0 }}{{ exam_response_pcts.c|default:0 }}%
+
+
+
D
+
{{ exam_response_counts.d|default:0 }}{{ exam_response_pcts.d|default:0 }}%
+
+
+
E
+
{{ exam_response_counts.e|default:0 }}{{ exam_response_pcts.e|default:0 }}%
+
+
+ {% else %} +
No responses recorded for this question in the exam yet.
+ {% endif %} +
+ {# Responses partial: load on demand via HTMX. Button fetches partial into #responses-container #}
@@ -116,7 +161,7 @@ + hx-swap="innerHTML">Show responses
diff --git a/sbas/templates/sbas/partials/exam_review_question_responses_fragment.html b/sbas/templates/sbas/partials/exam_review_question_responses_fragment.html index 9dfce6fd..da48d151 100644 --- a/sbas/templates/sbas/partials/exam_review_question_responses_fragment.html +++ b/sbas/templates/sbas/partials/exam_review_question_responses_fragment.html @@ -3,31 +3,30 @@
Responses in this exam
List of candidate answers recorded for this question in the current exam.
-
+ {% if exam_user_answers is defined %} + diff --git a/sbas/views.py b/sbas/views.py index 0dcc022e..c8435239 100644 --- a/sbas/views.py +++ b/sbas/views.py @@ -109,6 +109,14 @@ def exam_review_question_responses(request, pk: int, q_index: int): "app_name": "sbas", } + # Prefilter user answers for this exam to avoid template-side filtering + try: + exam_user_answers = question.cid_user_answers.filter(exam=exam).select_related("user") + except Exception: + exam_user_answers = question.cid_user_answers.all() + + context["exam_user_answers"] = exam_user_answers + return render(request, "sbas/partials/exam_review_question_responses_fragment.html", context)