Add per-part true/false breakdown to exam review summary for enhanced clarity
This commit is contained in:
@@ -25,7 +25,61 @@
|
||||
{% else %}
|
||||
<strong>Respondent {{ forloop.counter }}</strong>
|
||||
{% endif %}
|
||||
<div>Answer: <strong>{{ ua.answer }}</strong></div>
|
||||
<div class="mt-1">Answer:
|
||||
<div class="d-flex gap-1 mt-1">
|
||||
{# Show parts A-E as coloured badges indicating correctness #}
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="me-1 small text-muted">A</span>
|
||||
{% if ua.a is None %}
|
||||
<span class="badge bg-light text-muted">N/A</span>
|
||||
{% elif ua.a == question.a_answer %}
|
||||
<span class="badge bg-success">{{ ua.a|yesno:"T,F" }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">{{ ua.a|yesno:"T,F" }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="me-1 small text-muted">B</span>
|
||||
{% if ua.b is None %}
|
||||
<span class="badge bg-light text-muted">N/A</span>
|
||||
{% elif ua.b == question.b_answer %}
|
||||
<span class="badge bg-success">{{ ua.b|yesno:"T,F" }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">{{ ua.b|yesno:"T,F" }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="me-1 small text-muted">C</span>
|
||||
{% if ua.c is None %}
|
||||
<span class="badge bg-light text-muted">N/A</span>
|
||||
{% elif ua.c == question.c_answer %}
|
||||
<span class="badge bg-success">{{ ua.c|yesno:"T,F" }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">{{ ua.c|yesno:"T,F" }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="me-1 small text-muted">D</span>
|
||||
{% if ua.d is None %}
|
||||
<span class="badge bg-light text-muted">N/A</span>
|
||||
{% elif ua.d == question.d_answer %}
|
||||
<span class="badge bg-success">{{ ua.d|yesno:"T,F" }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">{{ ua.d|yesno:"T,F" }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="me-1 small text-muted">E</span>
|
||||
{% if ua.e is None %}
|
||||
<span class="badge bg-light text-muted">N/A</span>
|
||||
{% elif ua.e == question.e_answer %}
|
||||
<span class="badge bg-success">{{ ua.e|yesno:"T,F" }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">{{ ua.e|yesno:"T,F" }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if ua.created_date %}
|
||||
<div class="small text-muted">Answered: {{ ua.created_date }}</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -2,6 +2,23 @@
|
||||
<div id="physics-response-summary-content">
|
||||
{% if exam_response_total %}
|
||||
<div class="small text-muted mb-2">Total responses: {{ exam_response_total }}</div>
|
||||
{# Per-part TF breakdown (A-E) #}
|
||||
{% if per_part_stats %}
|
||||
<div class="row mb-3">
|
||||
{% for part in per_part_stats %}
|
||||
<div class="col-md-2">
|
||||
<div class="card">
|
||||
<div class="card-body p-2 text-center">
|
||||
<div class="small text-muted">{{ part.part|upper }}</div>
|
||||
<div class="h5 mb-0">{{ part.true_count }} / {{ part.total }}</div>
|
||||
<div class="small text-muted">True: {{ part.pct_true }}%</div>
|
||||
<div class="small text-muted">Correct: {{ part.correct_pct }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="row mb-2">
|
||||
<div class="col-auto">Correct:</div>
|
||||
<div class="col">
|
||||
@@ -18,7 +35,7 @@
|
||||
|
||||
<div class="list-group mb-2">
|
||||
{# Render top-level answer breakdown from exam_response_counts and pcts #}
|
||||
{% for key, count in exam_response_counts.items %}
|
||||
{% for key, count in exam_response_items %}
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex w-100 justify-content-between align-items-center">
|
||||
<div>{{ key }}</div>
|
||||
|
||||
+63
-2
@@ -518,6 +518,11 @@ def exam_review_question_summary(request, pk: int, q_index: int):
|
||||
for k in counts.keys():
|
||||
pcts[k] = 0.0
|
||||
|
||||
# Provide an explicit iterable of (key, count) pairs for templates to consume.
|
||||
# This avoids relying on attribute-resolution like `exam_response_counts.items`
|
||||
# in templates which can be ambiguous in Django's template variable lookup.
|
||||
exam_response_items = sorted(counts.items(), key=lambda x: x[1], reverse=True)
|
||||
|
||||
# Compute percent correct using per-answer scoring
|
||||
correct_count = None
|
||||
correct_pct = None
|
||||
@@ -555,6 +560,60 @@ def exam_review_question_summary(request, pk: int, q_index: int):
|
||||
correct_count = None
|
||||
correct_pct = None
|
||||
|
||||
# Per-part (a-e) true/false aggregation. Each physics question has 5 parts
|
||||
# stored as boolean fields on UserAnswer (a,b,c,d,e). Build counts and
|
||||
# percentages for each part so templates can render them as separate TF
|
||||
# sub-questions.
|
||||
per_part_stats = []
|
||||
try:
|
||||
parts = ["a", "b", "c", "d", "e"]
|
||||
# initialize counters
|
||||
for idx, part in enumerate(parts):
|
||||
per_part_stats.append({
|
||||
"part": part,
|
||||
"label": getattr(question, part).strip() if getattr(question, part, None) else f"Part {idx+1}",
|
||||
"true_count": 0,
|
||||
"false_count": 0,
|
||||
"total": 0,
|
||||
"correct_count": 0,
|
||||
"correct_pct": 0.0,
|
||||
"pct_true": 0.0,
|
||||
})
|
||||
|
||||
for ua in ua_qs:
|
||||
for idx, part in enumerate(parts):
|
||||
try:
|
||||
val = getattr(ua, part)
|
||||
except Exception:
|
||||
continue
|
||||
if val is True:
|
||||
per_part_stats[idx]["true_count"] += 1
|
||||
elif val is False:
|
||||
per_part_stats[idx]["false_count"] += 1
|
||||
# count only explicit booleans as total
|
||||
if isinstance(val, bool):
|
||||
per_part_stats[idx]["total"] += 1
|
||||
|
||||
# correct counting
|
||||
try:
|
||||
correct_val = getattr(question, f"{part}_answer")
|
||||
except Exception:
|
||||
correct_val = None
|
||||
if isinstance(val, bool) and isinstance(correct_val, bool):
|
||||
if val == correct_val:
|
||||
per_part_stats[idx]["correct_count"] += 1
|
||||
|
||||
# compute percentages
|
||||
for stats in per_part_stats:
|
||||
if stats["total"]:
|
||||
stats["pct_true"] = round(100.0 * stats["true_count"] / stats["total"], 1)
|
||||
stats["correct_pct"] = round(100.0 * stats["correct_count"] / stats["total"], 1)
|
||||
else:
|
||||
stats["pct_true"] = 0.0
|
||||
stats["correct_pct"] = 0.0
|
||||
except Exception:
|
||||
per_part_stats = []
|
||||
|
||||
# Provide a best-effort correct answer display
|
||||
correct_answer = None
|
||||
try:
|
||||
@@ -571,13 +630,15 @@ def exam_review_question_summary(request, pk: int, q_index: int):
|
||||
"question": question,
|
||||
"q_index": q_index,
|
||||
"app_name": "physics",
|
||||
"exam_response_counts": counts,
|
||||
"exam_response_pcts": pcts,
|
||||
"exam_response_counts": counts,
|
||||
"exam_response_items": exam_response_items,
|
||||
"exam_response_pcts": pcts,
|
||||
"exam_response_total": total_responses,
|
||||
"exam_response_correct_count": correct_count,
|
||||
"exam_response_correct_pct": correct_pct,
|
||||
"exam_response_texts": {},
|
||||
"correct_answer": correct_answer,
|
||||
"per_part_stats": per_part_stats,
|
||||
}
|
||||
|
||||
return render(request, "physics/partials/exam_review_question_summary_fragment.html", context)
|
||||
|
||||
Reference in New Issue
Block a user