Refactor exam review templates and views to improve response summary display and handle unanswered responses
This commit is contained in:
@@ -6,9 +6,6 @@
|
||||
<div id="review-root">
|
||||
{# The #review-content element is replaced by HTMX when navigating between questions. #}
|
||||
<div id="review-content">
|
||||
{# The view selects an app-specific fragment if present and passes its name
|
||||
in `fragment_template`. Include that fragment here so the full-page
|
||||
render and the HTMX partials both use the same template. #}
|
||||
{% include fragment_template %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<div class="mb-2"><span class="badge bg-dark">SBAs</span></div>
|
||||
<h5 class="card-title">Question</h5>
|
||||
<p class="lead">{{ question|safe }}</p>
|
||||
|
||||
|
||||
@@ -19,7 +19,12 @@
|
||||
{# Choice A #}
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex w-100 justify-content-between align-items-center">
|
||||
<div>A</div>
|
||||
<div>
|
||||
<div class="fw-bold">A</div>
|
||||
{% if exam_response_texts.a %}
|
||||
<div class="small text-muted">{{ exam_response_texts.a|safe }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<span class="me-2">{{ exam_response_counts.a|default:0 }}</span>
|
||||
<small class="text-muted">{{ exam_response_pcts.a|default:0 }}%</small>
|
||||
@@ -35,7 +40,12 @@
|
||||
{# Choice B #}
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex w-100 justify-content-between align-items-center">
|
||||
<div>B</div>
|
||||
<div>
|
||||
<div class="fw-bold">B</div>
|
||||
{% if exam_response_texts.b %}
|
||||
<div class="small text-muted">{{ exam_response_texts.b|safe }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<span class="me-2">{{ exam_response_counts.b|default:0 }}</span>
|
||||
<small class="text-muted">{{ exam_response_pcts.b|default:0 }}%</small>
|
||||
@@ -51,7 +61,12 @@
|
||||
{# Choice C #}
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex w-100 justify-content-between align-items-center">
|
||||
<div>C</div>
|
||||
<div>
|
||||
<div class="fw-bold">C</div>
|
||||
{% if exam_response_texts.c %}
|
||||
<div class="small text-muted">{{ exam_response_texts.c|safe }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<span class="me-2">{{ exam_response_counts.c|default:0 }}</span>
|
||||
<small class="text-muted">{{ exam_response_pcts.c|default:0 }}%</small>
|
||||
@@ -67,7 +82,12 @@
|
||||
{# Choice D #}
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex w-100 justify-content-between align-items-center">
|
||||
<div>D</div>
|
||||
<div>
|
||||
<div class="fw-bold">D</div>
|
||||
{% if exam_response_texts.d %}
|
||||
<div class="small text-muted">{{ exam_response_texts.d|safe }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<span class="me-2">{{ exam_response_counts.d|default:0 }}</span>
|
||||
<small class="text-muted">{{ exam_response_pcts.d|default:0 }}%</small>
|
||||
@@ -83,7 +103,12 @@
|
||||
{# Choice E #}
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex w-100 justify-content-between align-items-center">
|
||||
<div>E</div>
|
||||
<div>
|
||||
<div class="fw-bold">E</div>
|
||||
{% if exam_response_texts.e %}
|
||||
<div class="small text-muted">{{ exam_response_texts.e|safe }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<span class="me-2">{{ exam_response_counts.e|default:0 }}</span>
|
||||
<small class="text-muted">{{ exam_response_pcts.e|default:0 }}%</small>
|
||||
@@ -96,6 +121,23 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{# Unanswered / other responses (blank, None, or unexpected labels) #}
|
||||
{% if exam_response_counts.unanswered|default:0 %}
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex w-100 justify-content-between align-items-center">
|
||||
<div>No answer / Other</div>
|
||||
<div class="text-end">
|
||||
<span class="me-2">{{ exam_response_counts.unanswered|default:0 }}</span>
|
||||
<small class="text-muted">{{ exam_response_pcts.unanswered|default:0 }}%</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress mt-2" style="height:12px;">
|
||||
<div class="progress-bar bg-secondary" role="progressbar"
|
||||
style="width: {{ exam_response_pcts.unanswered|default:0 }}%;"
|
||||
aria-valuenow="{{ exam_response_pcts.unanswered|default:0 }}" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="small text-muted">No responses recorded for this question in the exam yet.</div>
|
||||
{% endif %}
|
||||
|
||||
+24
-2
@@ -138,13 +138,24 @@ def exam_review_question_summary(request, pk: int, q_index: int):
|
||||
ua_qs = question.cid_user_answers.filter(exam=exam)
|
||||
total_responses = ua_qs.count()
|
||||
|
||||
# Track counts for the five canonical choices and aggregate any
|
||||
# empty / unexpected answers into an 'unanswered' bucket so the
|
||||
# template can show them explicitly and percentages use the same
|
||||
# denominator (total_responses).
|
||||
counts = {"a": 0, "b": 0, "c": 0, "d": 0, "e": 0}
|
||||
other_count = 0
|
||||
for ans in ua_qs.values_list("answer", flat=True):
|
||||
# Normalize None/empty strings to be considered 'unanswered'
|
||||
if not ans:
|
||||
other_count += 1
|
||||
continue
|
||||
if ans in counts:
|
||||
counts[ans] += 1
|
||||
else:
|
||||
# Unexpected answer labels — store them as-is (rare)
|
||||
counts[ans] = counts.get(ans, 0) + 1
|
||||
# Unexpected answer labels — treat as 'other/unanswered'
|
||||
other_count += 1
|
||||
# Expose 'unanswered' so templates can report it
|
||||
counts['unanswered'] = other_count
|
||||
|
||||
pcts = {}
|
||||
if total_responses:
|
||||
@@ -161,6 +172,16 @@ def exam_review_question_summary(request, pk: int, q_index: int):
|
||||
if total_responses:
|
||||
correct_pct = round(100.0 * correct_count / total_responses, 1)
|
||||
|
||||
# Provide the actual answer text for each canonical choice so the
|
||||
# summary template can display the full answer alongside the letter.
|
||||
exam_response_texts = {
|
||||
"a": (getattr(question, "a_answer", None) or "") ,
|
||||
"b": (getattr(question, "b_answer", None) or "") ,
|
||||
"c": (getattr(question, "c_answer", None) or "") ,
|
||||
"d": (getattr(question, "d_answer", None) or "") ,
|
||||
"e": (getattr(question, "e_answer", None) or "") ,
|
||||
}
|
||||
|
||||
context = {
|
||||
"exam": exam,
|
||||
"question": question,
|
||||
@@ -171,6 +192,7 @@ def exam_review_question_summary(request, pk: int, q_index: int):
|
||||
"exam_response_total": total_responses,
|
||||
"exam_response_correct_count": correct_count,
|
||||
"exam_response_correct_pct": correct_pct,
|
||||
"exam_response_texts": exam_response_texts,
|
||||
}
|
||||
|
||||
return render(request, "sbas/partials/exam_review_question_summary_fragment.html", context)
|
||||
|
||||
Reference in New Issue
Block a user