diff --git a/physics/templates/physics/partials/exam_review_question_responses_fragment.html b/physics/templates/physics/partials/exam_review_question_responses_fragment.html index 2deee9f7..adeba053 100644 --- a/physics/templates/physics/partials/exam_review_question_responses_fragment.html +++ b/physics/templates/physics/partials/exam_review_question_responses_fragment.html @@ -25,7 +25,61 @@ {% else %} Respondent {{ forloop.counter }} {% endif %} -
Answer: {{ ua.answer }}
+
Answer: +
+ {# Show parts A-E as coloured badges indicating correctness #} +
+ A + {% if ua.a is None %} + N/A + {% elif ua.a == question.a_answer %} + {{ ua.a|yesno:"T,F" }} + {% else %} + {{ ua.a|yesno:"T,F" }} + {% endif %} +
+
+ B + {% if ua.b is None %} + N/A + {% elif ua.b == question.b_answer %} + {{ ua.b|yesno:"T,F" }} + {% else %} + {{ ua.b|yesno:"T,F" }} + {% endif %} +
+
+ C + {% if ua.c is None %} + N/A + {% elif ua.c == question.c_answer %} + {{ ua.c|yesno:"T,F" }} + {% else %} + {{ ua.c|yesno:"T,F" }} + {% endif %} +
+
+ D + {% if ua.d is None %} + N/A + {% elif ua.d == question.d_answer %} + {{ ua.d|yesno:"T,F" }} + {% else %} + {{ ua.d|yesno:"T,F" }} + {% endif %} +
+
+ E + {% if ua.e is None %} + N/A + {% elif ua.e == question.e_answer %} + {{ ua.e|yesno:"T,F" }} + {% else %} + {{ ua.e|yesno:"T,F" }} + {% endif %} +
+
+
{% if ua.created_date %}
Answered: {{ ua.created_date }}
{% endif %} diff --git a/physics/templates/physics/partials/exam_review_question_summary_fragment.html b/physics/templates/physics/partials/exam_review_question_summary_fragment.html index 3580c841..c1b5d854 100644 --- a/physics/templates/physics/partials/exam_review_question_summary_fragment.html +++ b/physics/templates/physics/partials/exam_review_question_summary_fragment.html @@ -2,6 +2,23 @@
{% if exam_response_total %}
Total responses: {{ exam_response_total }}
+ {# Per-part TF breakdown (A-E) #} + {% if per_part_stats %} +
+ {% for part in per_part_stats %} +
+
+
+
{{ part.part|upper }}
+
{{ part.true_count }} / {{ part.total }}
+
True: {{ part.pct_true }}%
+
Correct: {{ part.correct_pct }}%
+
+
+
+ {% endfor %} +
+ {% endif %}
Correct:
@@ -18,7 +35,7 @@
{# 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 %}
{{ key }}
diff --git a/physics/views.py b/physics/views.py index e24c911e..65b3679f 100644 --- a/physics/views.py +++ b/physics/views.py @@ -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)