diff --git a/anatomy/templates/anatomy/partials/exam_review_question_fragment.html b/anatomy/templates/anatomy/partials/exam_review_question_fragment.html index f525b4fa..bc15d712 100644 --- a/anatomy/templates/anatomy/partials/exam_review_question_fragment.html +++ b/anatomy/templates/anatomy/partials/exam_review_question_fragment.html @@ -1,3 +1,91 @@ +
+
+
Anatomy
+
Question
+

{{ question }}

+ +

+ View full question details +

+ +
+
Marked answers
+
    + {% for answer in question.answers.all|dictsortreversed:"status" %} +
  • +
    + {% if answer.proposed %} + (proposed) + {% endif %} + {{ answer.answer }} +
    +
    + {% if answer.status == '2' %} + Correct + {% elif answer.status == '1' %} + Half mark + {% elif answer.status == '0' %} + Incorrect + {% else %} + Unmarked + {% endif %} +
    +
  • + {% endfor %} +
+
+ + {# Aggregated summary placeholder — auto-load via HTMX #} +
+
Response summary
+
+
Loading summary…
+
+
+ + {# Responses: load on demand #} +
+
+
Responses in this exam
+
+ +
+
+
+
Responses are hidden — click “Show responses” to load.
+
+
+ +
+ {% if prev_index is not None %} + ← Previous + {% else %} + + {% endif %} + + {% if next_index is not None %} + Next → + {% else %} + Complete review + {% endif %} +
+ +
+
Anatomy
diff --git a/anatomy/templates/anatomy/partials/exam_review_question_responses_fragment.html b/anatomy/templates/anatomy/partials/exam_review_question_responses_fragment.html new file mode 100644 index 00000000..b65d8a93 --- /dev/null +++ b/anatomy/templates/anatomy/partials/exam_review_question_responses_fragment.html @@ -0,0 +1,35 @@ +{# Responses partial for anatomy question within an exam. HTMX-loadable. #} +
+
Responses in this exam
+
List of candidate answers recorded for this question in the current exam.
+
    + {% for ua in exam_user_answers %} +
  • +
    + {% if ua.user %} + {{ ua.user.get_full_name|default:ua.user.username }} + {% else %} + Anonymous + {% endif %} +
    Answer: {{ ua.answer }}
    + {% if ua.created_date %} +
    Answered: {{ ua.created_date }}
    + {% endif %} +
    +
    + {% if ua.score == '2' %} + Correct + {% elif ua.score == '1' %} + Half mark + {% elif ua.score == '0' %} + Incorrect + {% else %} + Unmarked + {% endif %} +
    +
  • + {% empty %} +
  • No responses recorded for this exam.
  • + {% endfor %} +
+
diff --git a/anatomy/templates/anatomy/partials/exam_review_question_summary_fragment.html b/anatomy/templates/anatomy/partials/exam_review_question_summary_fragment.html new file mode 100644 index 00000000..d6da29dc --- /dev/null +++ b/anatomy/templates/anatomy/partials/exam_review_question_summary_fragment.html @@ -0,0 +1,56 @@ +{# Aggregated response summary partial for anatomy questions (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 %} +
+
+ +
+
+
Correct
+
{{ exam_response_score_counts.2|default:0 }}
+
+
+
Half mark
+
{{ exam_response_score_counts.1|default:0 }}
+
+
+
Incorrect
+
{{ exam_response_score_counts.0|default:0 }}
+
+
+
Unmarked
+
{{ exam_response_score_counts.empty|default:0 }}
+
+
+ +
+
Top submitted answers
+ {% if top_answers %} +
    + {% for a in top_answers %} +
  • +
    {{ a.answer_compare }}
    +
    {{ a.count }}
    +
  • + {% endfor %} +
+ {% else %} +
No submitted answers yet.
+ {% endif %} +
+ {% else %} +
No responses recorded for this question in the exam yet.
+ {% endif %} +
diff --git a/anatomy/urls.py b/anatomy/urls.py index 5871d41a..c71ebd61 100644 --- a/anatomy/urls.py +++ b/anatomy/urls.py @@ -92,6 +92,16 @@ urlpatterns = [ views.UserAnswerDelete.as_view(), name="user_answer_delete", ), + path( + "exam//review//responses", + views.exam_review_question_responses, + name="exam_review_question_responses", + ), + path( + "exam//review//summary", + views.exam_review_question_summary, + name="exam_review_question_summary", + ), ] urlpatterns.extend(generic_view_urls(views.GenericViews)) diff --git a/anatomy/views.py b/anatomy/views.py index 5e8f5c30..13a3a06d 100644 --- a/anatomy/views.py +++ b/anatomy/views.py @@ -80,6 +80,7 @@ from helpers.images import image_as_base64 from django.template.defaulttags import register from generic.mixins import CheckCanEditMixin, SuperuserRequiredMixin +from django.db.models import Count class AuthorOrCheckerRequiredMixin(object): @@ -259,6 +260,82 @@ def mark(request, exam_pk, sk, unmarked_exam_answers_only=True, review=False): n = sk + + +@login_required +def exam_review_question_responses(request, pk: int, q_index: int): + """Return the responses partial for a given anatomy exam question (HTMX-friendly).""" + 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") + + 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": exam, + "question": question, + "q_index": q_index, + "app_name": "anatomy", + "exam_user_answers": exam_user_answers, + } + + return render(request, "anatomy/partials/exam_review_question_responses_fragment.html", context) + + +@login_required +def exam_review_question_summary(request, pk: int, q_index: int): + """Return aggregated response summary partial for anatomy question (HTMX-friendly).""" + 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") + + ua_qs = question.cid_user_answers.filter(exam=exam) + total_responses = ua_qs.count() + + # counts by score values ('2','1','0','') + score_counts = {} + for s, c in ua_qs.values_list("score").annotate(count=Count("id")) if False else []: + pass + + # simpler: compute via values + score_counts = {row["score"] if row["score"] is not None else "": row["count"] for row in ua_qs.values("score").annotate(count=Count("id"))} + + # top submitted answers (by answer_compare) + top_answers = list(ua_qs.values("answer_compare").annotate(count=Count("id")).order_by("-count")[:10]) + + correct_count = None + correct_pct = None + # compute correct based on matching marked answers if available + if total_responses: + # count scores equal to '2' as correct + correct_count = score_counts.get("2", 0) + correct_pct = round(100.0 * correct_count / total_responses, 1) if total_responses else None + + context = { + "exam": exam, + "question": question, + "q_index": q_index, + "app_name": "anatomy", + "exam_response_total": total_responses, + "exam_response_score_counts": score_counts, + "exam_response_correct_count": correct_count, + "exam_response_correct_pct": correct_pct, + "top_answers": top_answers, + } + + return render(request, "anatomy/partials/exam_review_question_summary_fragment.html", context) + question_details = { "total": len(questions), "current": n + 1,