From 796ac3fb8443739c9bd750fb4711b57ea4798540 Mon Sep 17 00:00:00 2001 From: Ross Date: Tue, 11 Nov 2025 13:01:51 +0000 Subject: [PATCH] Implement edit answer functionality with HTMX support; add new endpoint and template for editing stored answers in the marking UI. --- .../partials/mark2_edit_answer_fragment.html | 35 ++++++++ .../partials/mark2_exam_marked_fragment.html | 59 +++++++++++-- anatomy/urls.py | 1 + anatomy/views.py | 87 ++++++++++++++++++- 4 files changed, 174 insertions(+), 8 deletions(-) create mode 100644 anatomy/templates/anatomy/partials/mark2_edit_answer_fragment.html diff --git a/anatomy/templates/anatomy/partials/mark2_edit_answer_fragment.html b/anatomy/templates/anatomy/partials/mark2_edit_answer_fragment.html new file mode 100644 index 00000000..57d4ae13 --- /dev/null +++ b/anatomy/templates/anatomy/partials/mark2_edit_answer_fragment.html @@ -0,0 +1,35 @@ +
+
+
Edit stored answer
+
+ {% csrf_token %} + + + {% if exam_pk %} + + {% endif %} + {% if sk is not None %} + + {% endif %} + +
+ +
{{ original }}
+
+ +
+ + +
+ +
+ + {% if exam_pk and sk is not None %} + + {% else %} + + {% endif %} +
+
+
+
diff --git a/anatomy/templates/anatomy/partials/mark2_exam_marked_fragment.html b/anatomy/templates/anatomy/partials/mark2_exam_marked_fragment.html index 93cfdbed..f543262d 100644 --- a/anatomy/templates/anatomy/partials/mark2_exam_marked_fragment.html +++ b/anatomy/templates/anatomy/partials/mark2_exam_marked_fragment.html @@ -1,13 +1,21 @@ {% 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 %} @@ -16,18 +24,35 @@
    {% for it in items %}
  • -
    {{ it.answer }}
    -
    +
    {{ it.answer }}
    +
    + {# Stored mark badge #} {% if it.mark == 'correct' %} - 2 + 2 {% elif it.mark == 'half-correct' %} - 1 + 1 {% elif it.mark == 'incorrect' %} - 0 + 0 {% else %} - - + - {% endif %} + + {# Submission count badge #} {{ it.count }} + +
    + {% csrf_token %} + + + + + + + + +
    + +
  • {% endfor %} @@ -35,3 +60,23 @@ {% endif %}
+ + diff --git a/anatomy/urls.py b/anatomy/urls.py index a662c6d3..3b4d2d59 100644 --- a/anatomy/urls.py +++ b/anatomy/urls.py @@ -107,6 +107,7 @@ urlpatterns = [ 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"), + path("exam/mark2/edit_answer", views.mark2_edit_answer, name="mark2_edit_answer"), ] urlpatterns.extend(generic_view_urls(views.GenericViews)) diff --git a/anatomy/views.py b/anatomy/views.py index 1f1fbbf9..0861ef29 100644 --- a/anatomy/views.py +++ b/anatomy/views.py @@ -818,6 +818,24 @@ def mark2_toggle_answer(request): "incorrect": Answer.MarkOptions.INCORRECT, } + # Support clearing a stored answer (delete) via 'clear' + exam_pk = request.POST.get('exam_pk') + sk = request.POST.get('sk') + + if newmark == 'clear': + a = Answer.objects.filter(answer_compare__iexact=answer_text, question_id=question.pk).first() + if a is not None: + # permission: ensure user can edit this answer + if not a.can_edit(request.user): + return JsonResponse({"status": "error", "message": "forbidden"}, status=403) + a.delete() + if request.htmx and exam_pk and sk is not None: + try: + return mark2_exam_marked(request, int(exam_pk), int(sk)) + except Exception: + return JsonResponse({"status": "ok"}) + return JsonResponse({"status": "ok"}) + if newmark not in mark_map: return JsonResponse({"status": "error", "message": "invalid mark"}, status=400) @@ -833,6 +851,12 @@ def mark2_toggle_answer(request): a.status = status_val a.save() + if request.htmx and exam_pk and sk is not None: + try: + return mark2_exam_marked(request, int(exam_pk), int(sk)) + except Exception: + return JsonResponse({"status": "ok"}) + return JsonResponse({"status": "ok"}) @@ -879,10 +903,71 @@ def mark2_exam_marked(request, exam_pk, sk): else: items.sort(key=lambda x: x.get('count', 0), reverse=True) - context = {'exam': exam, 'question': question, 'items': items} + context = {'exam': exam, 'question': question, 'items': items, 'sk': sk} return render(request, 'anatomy/partials/mark2_exam_marked_fragment.html', context) +@login_required +def mark2_edit_answer(request): + """HTMX-friendly endpoint to edit/create a stored Answer for a question. + + GET: returns a small form fragment. + POST: updates/creates the Answer and, if exam_pk/sk provided, returns the + refreshed exam-marked fragment so the UI updates in-place. + """ + if request.method == 'GET' and request.htmx: + question_pk = request.GET.get('question_pk') + original = request.GET.get('original', '') + exam_pk = request.GET.get('exam_pk') + sk = request.GET.get('sk') + question = get_object_or_404(AnatomyQuestion, pk=question_pk) + # Try to find an existing Answer by compare + existing = question.answers.filter(answer_compare__iexact=original).first() + context = { + 'question': question, + 'original': original, + 'existing': existing, + 'exam_pk': exam_pk, + 'sk': sk, + } + return render(request, 'anatomy/partials/mark2_edit_answer_fragment.html', context) + + if request.method == 'POST' and request.htmx: + question_pk = request.POST.get('question_pk') + original = request.POST.get('original', '') + new_text = request.POST.get('new_answer', '').strip() + exam_pk = request.POST.get('exam_pk') + sk = request.POST.get('sk') + + if not question_pk or not new_text: + return HttpResponse('Missing parameters', status=400) + + question = get_object_or_404(AnatomyQuestion, pk=question_pk) + + a = question.answers.filter(answer_compare__iexact=original).first() + if a is None: + a = Answer() + a.question = question + a.answer = new_text + else: + a.answer = new_text + + a.save() + + # If we have exam context, refresh the exam-marked fragment for that question + if exam_pk and sk is not None: + # delegate to mark2_exam_marked to re-render the card + try: + return mark2_exam_marked(request, int(exam_pk), int(sk)) + except Exception: + return HttpResponse('OK') + + return HttpResponse('OK') + + # Fallback: forbidden for non-HTMX or other methods + raise PermissionDenied() + + @login_required def mark2_question_marked(request, pk): """Return a partial listing of all stored Answer choices for this question (grouped by status)."""