From 3e9a96d99fc63e4080187b6a4cbb2641aaa57aa5 Mon Sep 17 00:00:00 2001 From: Ross Date: Sun, 9 Nov 2025 22:09:23 +0000 Subject: [PATCH] Add HTMX-based aggregated response summary for exam questions --- .../exam_review_question_fragment.html | 48 +++------------- ...exam_review_question_summary_fragment.html | 43 ++++++++++++++ sbas/urls.py | 10 ++++ sbas/views.py | 56 +++++++++++++++++++ 4 files changed, 116 insertions(+), 41 deletions(-) create mode 100644 sbas/templates/sbas/partials/exam_review_question_summary_fragment.html diff --git a/sbas/templates/sbas/partials/exam_review_question_fragment.html b/sbas/templates/sbas/partials/exam_review_question_fragment.html index 980cf1a9..5a47abfc 100644 --- a/sbas/templates/sbas/partials/exam_review_question_fragment.html +++ b/sbas/templates/sbas/partials/exam_review_question_fragment.html @@ -109,49 +109,15 @@ View full question details

- {# Aggregated summary: always shown if aggregation context is available #} + {# Aggregated summary: HTMX-loaded automatically on fragment load #}
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 %} +
+
Loading summary…
+
{# Responses partial: load on demand via HTMX. Button fetches partial into #responses-container #} diff --git a/sbas/templates/sbas/partials/exam_review_question_summary_fragment.html b/sbas/templates/sbas/partials/exam_review_question_summary_fragment.html new file mode 100644 index 00000000..a8cff363 --- /dev/null +++ b/sbas/templates/sbas/partials/exam_review_question_summary_fragment.html @@ -0,0 +1,43 @@ +{# Partial: aggregated response summary for a question within an exam. HTMX-loadable. #} +
+ {% 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 %} +
diff --git a/sbas/urls.py b/sbas/urls.py index fc7339bd..adcdf80e 100644 --- a/sbas/urls.py +++ b/sbas/urls.py @@ -122,6 +122,16 @@ urlpatterns = [ views.exam_review_question_responses, name="exam_review_question_responses", ), + path( + "exam//review//summary", + views.exam_review_question_summary, + name="exam_review_question_summary", + ), + path( + "exam//review//responses", + views.exam_review_question_responses, + name="exam_review_question_responses", + ), path("question//toggle_frcr/", views.toggle_frcr_appropriate, name="toggle_frcr"), path("category/merge/", views.merge_category, name="merge_category"), #path( diff --git a/sbas/views.py b/sbas/views.py index c8435239..f4dab079 100644 --- a/sbas/views.py +++ b/sbas/views.py @@ -120,6 +120,62 @@ def exam_review_question_responses(request, pk: int, q_index: int): return render(request, "sbas/partials/exam_review_question_responses_fragment.html", context) +@login_required +def exam_review_question_summary(request, pk: int, q_index: int): + """Return the aggregated response summary partial for a given exam question (HTMX-friendly). + + Computes counts and percentages for choices a..e and returns the summary partial. + """ + exam = get_object_or_404(Exam, pk=pk) + + questions = exam.get_questions() + try: + question = questions[q_index] + except Exception: + raise Http404("Question not found in exam") + + # Aggregate user answers for this question & exam + ua_qs = question.cid_user_answers.filter(exam=exam) + total_responses = ua_qs.count() + + counts = {"a": 0, "b": 0, "c": 0, "d": 0, "e": 0} + for ans in ua_qs.values_list("answer", flat=True): + if ans in counts: + counts[ans] += 1 + else: + # Unexpected answer labels — store them as-is (rare) + counts[ans] = counts.get(ans, 0) + 1 + + 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 + + correct_count = None + correct_pct = None + if hasattr(question, "best_answer") and question.best_answer: + correct_count = ua_qs.filter(answer=question.best_answer).count() + if total_responses: + correct_pct = round(100.0 * correct_count / total_responses, 1) + + context = { + "exam": exam, + "question": question, + "q_index": q_index, + "app_name": "sbas", + "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, + } + + return render(request, "sbas/partials/exam_review_question_summary_fragment.html", context) + + class AuthorOrCheckerRequiredMixin(object): def get_object(self, *args, **kwargs): obj = super().get_object(*args, **kwargs)