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)