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 %}
+
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