From 34ef5106f42ad71bf729143a3314bfa4e2f0a6ae Mon Sep 17 00:00:00 2001 From: Ross Date: Tue, 11 Nov 2025 11:53:35 +0000 Subject: [PATCH] Add new endpoints and templates for displaying marked answers in the improved marking UI --- anatomy/templates/anatomy/mark2.html | 83 +++++++++++++------ .../partials/mark2_exam_marked_fragment.html | 37 +++++++++ .../mark2_question_marked_fragment.html | 38 +++++++++ anatomy/urls.py | 2 + anatomy/views.py | 64 ++++++++++++++ 5 files changed, 197 insertions(+), 27 deletions(-) create mode 100644 anatomy/templates/anatomy/partials/mark2_exam_marked_fragment.html create mode 100644 anatomy/templates/anatomy/partials/mark2_question_marked_fragment.html diff --git a/anatomy/templates/anatomy/mark2.html b/anatomy/templates/anatomy/mark2.html index 82e2be11..877a2bf6 100644 --- a/anatomy/templates/anatomy/mark2.html +++ b/anatomy/templates/anatomy/mark2.html @@ -23,7 +23,7 @@
{% if question.answer_help %} - + {% endif %}
@@ -39,7 +39,7 @@
{% csrf_token %}

Click each answer to toggle through marks awarded (as per colour). This UI writes marks immediately.

-
Key: 2 Marks, 1 Mark, 0 Marks
+
{% if question_details.current > 1 %} Previous @@ -51,26 +51,28 @@ {% endif %}
+
+ + +
-
-
-
Unmarked
-
    - {% include 'anatomy/partials/mark2_unmarked_list_fragment.html' %} -
-
-
-
Marked
-
    - {% include 'anatomy/partials/mark2_marked_list_fragment.html' %} -
-
-
+
Answers
+
    + {# Render currently-stored marked answers first, then unmarked submissions #} + {% include 'anatomy/partials/mark2_marked_list_fragment.html' %} + {% include 'anatomy/partials/mark2_unmarked_list_fragment.html' %} +
+
+
+
+
Key: 2 Marks, 1 Mark, 0 Marks
+
+ {{ form.as_p }}
@@ -88,13 +90,25 @@ {% endblock %} @@ -138,13 +152,19 @@ // set class preserving 'answer' base el.className = 'answer ' + newState; - // move DOM node to the appropriate list for immediate feedback + // mark/unmark list item visually (no moving between lists in single-column mode) const li = el.closest('li'); if (li) { + // clear any previous per-state classes + li.classList.remove('marked-correct','marked-half-correct','marked-incorrect','marked-item'); if (newState === 'not-marked') { - document.getElementById('unmarked-list').appendChild(li); - } else { - document.getElementById('marked-list').appendChild(li); + // nothing + } else if (newState === 'correct') { + li.classList.add('marked-item','marked-correct'); + } else if (newState === 'half-correct') { + li.classList.add('marked-item','marked-half-correct'); + } else if (newState === 'incorrect') { + li.classList.add('marked-item','marked-incorrect'); } } @@ -155,7 +175,7 @@ function updateMarkedAnswersField() { const marked = { 'correct': [], 'half-correct': [], 'incorrect': [] }; - document.querySelectorAll('#marked-list span.answer').forEach(function(el) { + document.querySelectorAll('#answer-list span.answer').forEach(function(el) { if (el.classList.contains('correct')) marked.correct.push((el.title || el.textContent).trim()); else if (el.classList.contains('half-correct')) marked['half-correct'].push((el.title || el.textContent).trim()); else if (el.classList.contains('incorrect')) marked.incorrect.push((el.title || el.textContent).trim()); @@ -211,7 +231,7 @@ } function setMarkForSpan(el, numeric) { - // numeric: 2 -> correct, 1 -> half-correct, 0 -> incorrect, null -> not-marked + // numeric: 2 -> correct, 1 -> half-correct, 0 -> incorrect, null -> not-marked const li = el.closest('li'); let state = 'not-marked'; if (numeric === 2) state = 'correct'; @@ -219,8 +239,17 @@ else if (numeric === 0) state = 'incorrect'; el.className = 'answer ' + state; if (li) { - if (state === 'not-marked') document.getElementById('unmarked-list').appendChild(li); - else document.getElementById('marked-list').appendChild(li); + // clear previous per-state classes + li.classList.remove('marked-correct','marked-half-correct','marked-incorrect','marked-item'); + if (state === 'not-marked') { + // nothing + } else if (state === 'correct') { + li.classList.add('marked-item','marked-correct'); + } else if (state === 'half-correct') { + li.classList.add('marked-item','marked-half-correct'); + } else if (state === 'incorrect') { + li.classList.add('marked-item','marked-incorrect'); + } } updateMarkedAnswersField(); } diff --git a/anatomy/templates/anatomy/partials/mark2_exam_marked_fragment.html b/anatomy/templates/anatomy/partials/mark2_exam_marked_fragment.html new file mode 100644 index 00000000..23d1512f --- /dev/null +++ b/anatomy/templates/anatomy/partials/mark2_exam_marked_fragment.html @@ -0,0 +1,37 @@ +{% comment %}List of distinct answers submitted within this exam for the question, with counts and mark badges{% endcomment %} +
+
+
+
Answers submitted in this exam
+
+
+ + +
+
+
+ {% if not items %} +
No answers submitted in this exam.
+ {% else %} +
    + {% for it in items %} +
  • +
    {{ it.answer }}
    +
    + {% if it.mark == 'correct' %} + 2 + {% elif it.mark == 'half-correct' %} + 1 + {% elif it.mark == 'incorrect' %} + 0 + {% else %} + - + {% endif %} + {{ it.count }} +
    +
  • + {% endfor %} +
+ {% endif %} +
+
diff --git a/anatomy/templates/anatomy/partials/mark2_question_marked_fragment.html b/anatomy/templates/anatomy/partials/mark2_question_marked_fragment.html new file mode 100644 index 00000000..b7669f2a --- /dev/null +++ b/anatomy/templates/anatomy/partials/mark2_question_marked_fragment.html @@ -0,0 +1,38 @@ +{% comment %}All stored Answer choices for this question grouped by status{% endcomment %} +
+
+
All stored answers for this question
+
+
+
2 marks
+
    + {% for answer in correct_answers %} +
  • {{ answer.answer }}
  • + {% empty %} +
  • None
  • + {% endfor %} +
+
+
+
1 mark
+
    + {% for answer in half_mark_answers %} +
  • {{ answer.answer }}
  • + {% empty %} +
  • None
  • + {% endfor %} +
+
+
+
0 marks
+
    + {% for answer in incorrect_answers %} +
  • {{ answer.answer }}
  • + {% empty %} +
  • None
  • + {% endfor %} +
+
+
+
+
diff --git a/anatomy/urls.py b/anatomy/urls.py index c933e2d0..a662c6d3 100644 --- a/anatomy/urls.py +++ b/anatomy/urls.py @@ -105,6 +105,8 @@ urlpatterns = [ # New improved marking UI (mark2) and small toggle endpoint used by JS/HTMX path("exam///mark2", views.mark2, name="mark2"), path("exam/mark2/toggle", views.mark2_toggle_answer, name="mark2_toggle_answer"), + path("exam///mark2/exam_marked", views.mark2_exam_marked, name="mark2_exam_marked"), + path("question//mark2/question_marked", views.mark2_question_marked, name="mark2_question_marked"), ] urlpatterns.extend(generic_view_urls(views.GenericViews)) diff --git a/anatomy/views.py b/anatomy/views.py index cd4f121c..1f1fbbf9 100644 --- a/anatomy/views.py +++ b/anatomy/views.py @@ -836,6 +836,70 @@ def mark2_toggle_answer(request): return JsonResponse({"status": "ok"}) +@login_required +def mark2_exam_marked(request, exam_pk, sk): + """Return a partial listing of answers submitted for this question within the given exam. + + Shows distinct submitted answers with counts and any existing mark/status. + """ + exam = get_object_or_404(Exam, pk=exam_pk) + questions = exam.get_questions() + try: + question = questions[sk] + except Exception: + raise Http404("Question not found in exam") + + ua_qs = question.cid_user_answers.filter(exam=exam) + + # Allow client to request ordering by 'count' (default) or by 'mark' (badge) + order = request.GET.get('order', 'count') + + # Group distinct submitted answers and count occurrences + grouped = ua_qs.values('answer_compare').annotate(count=Count('id')) + + items = [] + for row in grouped: + ans = row['answer_compare'] + cnt = row['count'] + a = question.answers.filter(answer_compare__iexact=ans).first() + mark = None + if a is not None: + if a.status == Answer.MarkOptions.CORRECT: + mark = 'correct' + elif a.status == Answer.MarkOptions.HALF_MARK: + mark = 'half-correct' + elif a.status == Answer.MarkOptions.INCORRECT: + mark = 'incorrect' + items.append({'answer': ans, 'count': cnt, 'mark': mark}) + + # Sort items according to requested ordering + if order == 'mark': + weight = {'correct': 3, 'half-correct': 2, 'incorrect': 1, None: 0} + items.sort(key=lambda x: (weight.get(x.get('mark')), x.get('count', 0)), reverse=True) + else: + items.sort(key=lambda x: x.get('count', 0), reverse=True) + + context = {'exam': exam, 'question': question, 'items': items} + return render(request, 'anatomy/partials/mark2_exam_marked_fragment.html', context) + + +@login_required +def mark2_question_marked(request, pk): + """Return a partial listing of all stored Answer choices for this question (grouped by status).""" + question = get_object_or_404(AnatomyQuestion, pk=pk) + correct_answers = question.answers.filter(status=Answer.MarkOptions.CORRECT) + half_mark_answers = question.answers.filter(status=Answer.MarkOptions.HALF_MARK) + incorrect_answers = question.answers.filter(status=Answer.MarkOptions.INCORRECT) + + context = { + 'question': question, + 'correct_answers': correct_answers, + 'half_mark_answers': half_mark_answers, + 'incorrect_answers': incorrect_answers, + } + return render(request, 'anatomy/partials/mark2_question_marked_fragment.html', context) + + #@login_required #def exam_scores_refresh(request, pk):