From 17d3b4206abfdd36499ea18a04e1a8b204e71d57 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 16 Feb 2026 09:46:57 +0000 Subject: [PATCH] Implement primary answer management with HTMX support, including form for selecting or creating answers and display templates --- anatomy/forms.py | 17 +++++- .../partials/primary_answer_display.html | 25 ++++++++ .../partials/primary_answer_empty.html | 4 ++ .../anatomy/partials/primary_answer_form.html | 58 +++++++++++++++++++ .../templates/anatomy/question_detail.html | 6 +- anatomy/urls.py | 1 + anatomy/views.py | 57 +++++++++++++++++- 7 files changed, 165 insertions(+), 3 deletions(-) create mode 100644 anatomy/templates/anatomy/partials/primary_answer_display.html create mode 100644 anatomy/templates/anatomy/partials/primary_answer_empty.html create mode 100644 anatomy/templates/anatomy/partials/primary_answer_form.html diff --git a/anatomy/forms.py b/anatomy/forms.py index 7cc962ad..c7795d87 100644 --- a/anatomy/forms.py +++ b/anatomy/forms.py @@ -290,4 +290,19 @@ class ExamGroupsForm(ExamGroupsFormMixin): class SuggestIncorrectAnswerForm(ModelForm): class Meta: model = AnatomyQuestion - fields = ["answer_suggest_incorrect"] \ No newline at end of file + fields = ["answer_suggest_incorrect"] + + +class PrimaryAnswerForm(Form): + existing_answer = ChoiceField(required=False, choices=[], label="Use existing answer") + new_answer = CharField(required=False, widget=Textarea(attrs={"rows":3}), label="Or create new primary answer") + + def __init__(self, *args, **kwargs): + question = kwargs.pop("question", None) + super().__init__(*args, **kwargs) + choices = [("", "-- choose existing --")] + if question is not None: + # Only include existing non-proposed CORRECT answers, ordered by answer text + for a in question.answers.filter(proposed=False, status=Answer.MarkOptions.CORRECT).order_by('answer'): + choices.append((str(a.pk), a.answer)) + self.fields["existing_answer"].choices = choices \ No newline at end of file diff --git a/anatomy/templates/anatomy/partials/primary_answer_display.html b/anatomy/templates/anatomy/partials/primary_answer_display.html new file mode 100644 index 00000000..06767f8e --- /dev/null +++ b/anatomy/templates/anatomy/partials/primary_answer_display.html @@ -0,0 +1,25 @@ +
+ {{ question.get_primary_answer }} + + + +
diff --git a/anatomy/templates/anatomy/partials/primary_answer_empty.html b/anatomy/templates/anatomy/partials/primary_answer_empty.html new file mode 100644 index 00000000..4b8bbd3f --- /dev/null +++ b/anatomy/templates/anatomy/partials/primary_answer_empty.html @@ -0,0 +1,4 @@ +
+ No primary answer set + +
diff --git a/anatomy/templates/anatomy/partials/primary_answer_form.html b/anatomy/templates/anatomy/partials/primary_answer_form.html new file mode 100644 index 00000000..37c7e9a5 --- /dev/null +++ b/anatomy/templates/anatomy/partials/primary_answer_form.html @@ -0,0 +1,58 @@ +{% load crispy_forms_tags %} +
+
+ {% csrf_token %} +
+ + +
+ +
+ + + + +
+ +
+ + +
+
+
diff --git a/anatomy/templates/anatomy/question_detail.html b/anatomy/templates/anatomy/question_detail.html index 8c0ec686..4c6bcd91 100644 --- a/anatomy/templates/anatomy/question_detail.html +++ b/anatomy/templates/anatomy/question_detail.html @@ -36,7 +36,11 @@
Primary answer
-
{{ question.get_primary_answer }}
+ {% if question.primary_answer %} + {% include 'anatomy/partials/primary_answer_display.html' %} + {% else %} +
+ {% endif %}
Answers
diff --git a/anatomy/urls.py b/anatomy/urls.py index 793ff6d5..22cf7c1f 100644 --- a/anatomy/urls.py +++ b/anatomy/urls.py @@ -114,6 +114,7 @@ urlpatterns = [ 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"), + path("question//primary_answer/", views.primary_answer_htmx, name="primary_answer_htmx"), ] urlpatterns.extend(generic_view_urls(views.GenericViews)) diff --git a/anatomy/views.py b/anatomy/views.py index 388dd9b9..ba752c58 100644 --- a/anatomy/views.py +++ b/anatomy/views.py @@ -46,6 +46,7 @@ from .forms import ( AnatomyQuestionForm, ExamForm, SuggestIncorrectAnswerForm, + PrimaryAnswerForm, ) from .models import ( AnatomyQuestion, @@ -1655,4 +1656,58 @@ def question_add_exam(request, question_id: int): return HttpResponse(mark_safe(html)) - raise PermissionDenied() \ No newline at end of file + raise PermissionDenied() + + +@login_required +def primary_answer_htmx(request, pk: int): + """HTMX endpoint to render or update a question's primary answer. + + GET: render the form for selecting/creating primary answer. + POST: create/select answer and set `question.primary_answer`, then + return the display fragment. + """ + if not request.htmx: + raise PermissionDenied() + + question = get_object_or_404(AnatomyQuestion, pk=pk) + + if not question.can_edit(request.user): + return HttpResponse("Invalid permissions", status=403) + + if request.method == "POST": + form = PrimaryAnswerForm(request.POST, question=question) + if form.is_valid(): + new_text = form.cleaned_data.get("new_answer", "").strip() + existing = form.cleaned_data.get("existing_answer", "") + + if new_text: + # create a new Answer and mark it CORRECT + ans = Answer(question=question, answer=new_text, proposed=False, status=Answer.MarkOptions.CORRECT) + ans.save() + question.primary_answer = ans + question.save(update_fields=["primary_answer"]) + elif existing: + try: + aid = int(existing) + ans = Answer.objects.get(pk=aid, question=question) + question.primary_answer = ans + question.save(update_fields=["primary_answer"]) + except (ValueError, Answer.DoesNotExist): + return HttpResponse("Invalid answer choice", status=400) + + # return display fragment + return render(request, "anatomy/partials/primary_answer_display.html", {"question": question}) + else: + return HttpResponse("Invalid form", status=400) + + # Handle cancel request to restore original state + if request.method == "GET" and request.GET.get("cancel"): + if question.primary_answer: + return render(request, "anatomy/partials/primary_answer_display.html", {"question": question}) + else: + return render(request, "anatomy/partials/primary_answer_empty.html", {"question": question}) + + # GET - render form + form = PrimaryAnswerForm(question=question) + return render(request, "anatomy/partials/primary_answer_form.html", {"form": form, "question": question}) \ No newline at end of file