Add suggestion management for incorrect answers; implement collapsed view and HTMX support for dynamic updates

This commit is contained in:
Ross
2025-11-11 13:36:55 +00:00
parent 6e7135823a
commit e4bab4a509
5 changed files with 76 additions and 25 deletions
@@ -0,0 +1,16 @@
<div id="incorrect-answers" class="small text-muted">
<strong>Answer suggest incorrect:</strong>
<div class="mt-2">
{% if question.answer_suggest_incorrect %}
{% for s in question.answer_suggest_incorrect %}
<span class="badge bg-secondary me-1 mb-1">{{ s }}</span>
{% endfor %}
{% else %}
<span class="small text-muted">No suggestions</span>
{% endif %}
</div>
<div class="mt-1">
<button id="answer-suggest-incorrect-button" class="btn btn-sm btn-outline-secondary" hx-get="{% url 'anatomy:question_suggest_incorrect_answers' question.pk %}" hx-target="#incorrect-answers" hx-swap="innerHTML">Edit suggestions</button>
</div>
</div>
@@ -1,20 +1,25 @@
<div class="card mt-2"> <div class="card mt-2">
<div class="card-body"> <div class="card-body">
<h6 class="card-title">Suggest incorrect answers</h6> <h6 class="card-title">Suggest incorrect answers</h6>
<form hx-post="{% url 'anatomy:question_suggest_incorrect_answers' question.pk %}" hx-target="closest .card" hx-swap="outerHTML"> <form hx-post="{% url 'anatomy:question_suggest_incorrect_answers' question.pk %}" hx-target="#incorrect-answers" hx-swap="outerHTML">
{% csrf_token %} {% csrf_token %}
<div class="mb-2"> <div class="mb-2">
<label class="form-label">Current suggestions (one per line)</label> <label class="form-label">Current suggestions</label>
{{ form.answer_suggest_incorrect }} <div class="mb-2">
{% for s in question.answer_suggest_incorrect %}
<button type="button" class="btn btn-sm btn-outline-secondary me-1 mb-1" hx-post="{% url 'anatomy:question_suggest_incorrect_answers' question.pk %}" hx-vals='{"remove": "{{ s|escapejs }}"}' hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}' hx-target="closest .card" hx-swap="outerHTML">{{ s }} <span class="text-danger ms-1">&times;</span></button>
{% empty %}
<div class="small text-muted">No suggestions</div>
{% endfor %}
</div>
<div class="d-flex gap-2 align-items-center">
<input id="new-suggest" name="suggestion" class="form-control form-control-sm" placeholder="Add suggestion" />
<button type="button" class="btn btn-sm btn-primary" hx-post="{% url 'anatomy:question_suggest_incorrect_answers' question.pk %}" hx-include="#new-suggest" hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}' hx-target="closest .card" hx-swap="outerHTML">Add</button>
</div>
</div> </div>
<div class="d-flex gap-2 justify-content-end"> <div class="d-flex gap-2 justify-content-end">
<button type="submit" class="btn btn-primary btn-sm">Save</button> <button type="submit" name="close" value="1" class="btn btn-primary btn-sm">Close</button>
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="(function(e){
var wrapper = document.getElementById('incorrect-answers');
if(!wrapper) { var c=e.target.closest('.card'); if(c) c.remove(); return; }
var t = wrapper.querySelector('template.incorrect-answers-original');
if(t) { wrapper.innerHTML = t.innerHTML; }
})(event)">Cancel</button>
</div> </div>
</form> </form>
@@ -28,7 +33,7 @@
<div class="flex-grow-1"><pre class="mb-0">{{ c.answer }}</pre></div> <div class="flex-grow-1"><pre class="mb-0">{{ c.answer }}</pre></div>
<div class="text-end"> <div class="text-end">
<span class="badge bg-light text-dark me-2">{{ c.count }}</span> <span class="badge bg-light text-dark me-2">{{ c.count }}</span>
<button class="btn btn-sm btn-outline-primary" hx-post="{% url 'anatomy:question_suggest_incorrect_answers' question.pk %}" hx-vals='{"suggestion": "{{ c.answer|escapejs }}"}' hx-target="closest .card" hx-swap="outerHTML">Add</button> <button class="btn btn-sm btn-outline-primary" hx-post="{% url 'anatomy:question_suggest_incorrect_answers' question.pk %}" hx-vals='{"suggestion": "{{ c.answer|escapejs }}", "context": "fragment"}' hx-target="closest .card" hx-swap="outerHTML">Add</button>
</div> </div>
</li> </li>
{% endfor %} {% endfor %}
@@ -37,7 +42,7 @@
</div> </div>
</div> </div>
<template class="incorrect-answers-original" hidden> <template class="incorrect-answers-original" hidden>
<strong>Answer suggest incorrect:</strong> {{ question.answer_suggest_incorrect }} <strong>Answer suggest incorrect:</strong> {{ question.answer_suggest_incorrect }}
<div class="mt-1"><button id="answer-suggest-incorrect-button" class="btn btn-sm btn-outline-secondary" hx-get="{% url 'anatomy:question_suggest_incorrect_answers' question.pk %}" hx-target="#incorrect-answers" hx-swap="innerHTML">Edit suggestions</button></div> <div class="mt-1"><button id="answer-suggest-incorrect-button" class="btn btn-sm btn-outline-secondary" hx-get="{% url 'anatomy:question_suggest_incorrect_answers' question.pk %}" hx-target="#incorrect-answers" hx-swap="innerHTML">Edit suggestions</button></div>
</template> </template>
@@ -76,12 +76,7 @@
{% endif %} {% endif %}
<div class="mb-2"> <div class="mb-2">
{% partialdef suggest-incorrect-form inline %} {% include 'anatomy/partials/suggest_incorrect_collapsed.html' %}
<div id="incorrect-answers" class="small text-muted">
<strong>Answer suggest incorrect:</strong> {{ question.answer_suggest_incorrect }}
<div class="mt-1"><button id="answer-suggest-incorrect-button" class="btn btn-sm btn-outline-secondary" hx-get="{% url 'anatomy:question_suggest_incorrect_answers' question.pk %}" hx-target="#incorrect-answers" hx-swap="innerHTML">Edit suggestions</button></div>
</div>
{% endpartialdef %}
</div> </div>
<div class="mb-2"> <div class="mb-2">
+5
View File
@@ -38,6 +38,11 @@ urlpatterns = [
views.question_suggest_incorrect_answers, views.question_suggest_incorrect_answers,
name="question_suggest_incorrect_answers", name="question_suggest_incorrect_answers",
), ),
path(
"question/<int:pk>/suggest_incorrect_collapsed",
views.question_suggest_incorrect_collapsed,
name="question_suggest_incorrect_collapsed",
),
path("question/<int:pk>/answer/", views.answer_question, name="answer_question"), path("question/<int:pk>/answer/", views.answer_question, name="answer_question"),
path( path(
+34 -4
View File
@@ -1442,7 +1442,25 @@ def question_suggest_incorrect_answers(request, pk):
# Support HTMX partial rendering with suggestion candidates and inline add # Support HTMX partial rendering with suggestion candidates and inline add
if request.method == "POST": if request.method == "POST":
# Support single-candidate add via HTMX (button posts 'suggestion') # Support remove action from the edit fragment
if request.htmx and request.POST.get("remove"):
to_remove = request.POST.get("remove").strip()
if to_remove:
existing = [s for s in question.answer_suggest_incorrect if s]
if to_remove in existing:
existing = [s for s in existing if s != to_remove]
question.answer_suggest_incorrect = existing
question.save()
# Recompute candidates and render the edit fragment
form = SuggestIncorrectAnswerForm(instance=question)
candidates = build_candidates(question)
context = {"form": form, "question": question, "candidates": candidates}
return render(request, "anatomy/partials/suggest_incorrect_fragment.html", context)
# Support single-candidate add via HTMX (button posts 'suggestion').
# Edits should only be performed via the edit fragment; when a
# suggestion is posted we re-render the edit fragment so the
# user remains in edit mode and sees the updated candidate list.
if request.htmx and request.POST.get("suggestion"): if request.htmx and request.POST.get("suggestion"):
suggestion = request.POST.get("suggestion").strip() suggestion = request.POST.get("suggestion").strip()
if suggestion: if suggestion:
@@ -1451,7 +1469,7 @@ def question_suggest_incorrect_answers(request, pk):
existing.append(suggestion) existing.append(suggestion)
question.answer_suggest_incorrect = existing question.answer_suggest_incorrect = existing
question.save() question.save()
# Recompute candidates and render fragment # Recompute candidates and render the edit fragment
form = SuggestIncorrectAnswerForm(instance=question) form = SuggestIncorrectAnswerForm(instance=question)
candidates = build_candidates(question) candidates = build_candidates(question)
context = {"form": form, "question": question, "candidates": candidates} context = {"form": form, "question": question, "candidates": candidates}
@@ -1464,9 +1482,13 @@ def question_suggest_incorrect_answers(request, pk):
obj = form.save() obj = form.save()
obj.save() obj.save()
# If HTMX requested, return the fragment so the UI can update in-place # If HTMX requested, return the fragment or collapsed partial
if request.htmx: if request.htmx:
# Recompute candidates and render fragment # If the close button was used, return the collapsed small partial
if request.POST.get("close"):
return render(request, "anatomy/partials/suggest_incorrect_collapsed.html", {"question": question})
# Otherwise re-render the edit fragment so the user stays in edit mode
form = SuggestIncorrectAnswerForm(instance=question) form = SuggestIncorrectAnswerForm(instance=question)
candidates = build_candidates(question) candidates = build_candidates(question)
context = {"form": form, "question": question, "candidates": candidates} context = {"form": form, "question": question, "candidates": candidates}
@@ -1487,6 +1509,14 @@ def question_suggest_incorrect_answers(request, pk):
return HttpResponse("Error", status=400) return HttpResponse("Error", status=400)
@user_is_author_or_anatomy_checker
def question_suggest_incorrect_collapsed(request, pk):
"""Return the small collapsed 'Edit suggestions' box as a partial."""
question = get_object_or_404(AnatomyQuestion, pk=pk)
context = {"question": question}
return render(request, "anatomy/partials/suggest_incorrect_collapsed.html", context)
@user_is_author_or_anatomy_checker @user_is_author_or_anatomy_checker
def question_save_annotation(request, pk): def question_save_annotation(request, pk):
if request.method == "POST": if request.method == "POST":