Implement suggestion feature for incorrect answers; add HTMX support for dynamic candidate rendering and inline editing.

This commit is contained in:
Ross
2025-11-11 13:21:32 +00:00
parent a158a6f928
commit 6e7135823a
2 changed files with 182 additions and 1 deletions
+139 -1
View File
@@ -1332,20 +1332,158 @@ class AnatomyQuestionView(LoginRequiredMixin, SingleTableMixin, FilterView):
@user_is_author_or_anatomy_checker
def question_suggest_incorrect_answers(request, pk):
question = get_object_or_404(AnatomyQuestion, pk=pk)
# helper: build candidate suggestions (n-grams + prefixes)
def build_candidates(question_obj):
from django.db.models import Count
import re
def tokenize(s: str):
return re.findall(r"\w+", (s or "").lower())
candidates_qs = (
question_obj.cid_user_answers.values("answer_compare")
.annotate(count=Count("id"))
.order_by("-count")
)
existing = set([s for s in question_obj.answer_suggest_incorrect if s])
marked_texts = [m.lower() for m in question_obj.get_marked_answers()]
ngram_map = {}
ngram_weight = {}
prefix_map = {}
prefix_weight = {}
for row in candidates_qs:
ans = (row.get("answer_compare") or "").strip()
cnt = int(row.get("count") or 0)
if not ans:
continue
tokens = tokenize(ans)
max_n = min(4, len(tokens))
for n in range(1, max_n + 1):
for i in range(len(tokens) - n + 1):
ngram = " ".join(tokens[i : i + n])
ngram_map.setdefault(ngram, set()).add(ans)
ngram_weight[ngram] = ngram_weight.get(ngram, 0) + cnt
for tok in tokens:
tlen = len(tok)
for plen in range(3, min(6, tlen) + 1):
pref = tok[:plen]
prefix_map.setdefault(pref, set()).add(ans)
prefix_weight[pref] = prefix_weight.get(pref, 0) + cnt
raw_candidates = []
for ngram, ans_set in ngram_map.items():
if ngram in existing:
continue
excluded = False
for mt in marked_texts:
if ngram in mt:
excluded = True
break
if excluded:
continue
coverage = len(ans_set)
if coverage < 1:
continue
raw_candidates.append(
{
"text": ngram,
"coverage": coverage,
"count": ngram_weight.get(ngram, 0),
"words": len(ngram.split()),
"type": "ngram",
}
)
for pref, ans_set in prefix_map.items():
if pref in existing:
continue
excluded = False
for mt in marked_texts:
if pref in mt:
excluded = True
break
if excluded:
continue
coverage = len(ans_set)
if coverage < 1:
continue
raw_candidates.append(
{
"text": pref,
"coverage": coverage,
"count": prefix_weight.get(pref, 0),
"words": 1,
"type": "prefix",
}
)
raw_candidates.sort(key=lambda x: (x["words"], -x["coverage"], -x["count"]))
selected = []
selected_texts = []
for c in raw_candidates:
t = c["text"]
skip = False
for s in selected_texts:
if s in t:
skip = True
break
if not skip:
selected.append({"answer": t, "count": c["count"], "type": c.get("type", "ngram")})
selected_texts.append(t)
return selected
# Support HTMX partial rendering with suggestion candidates and inline add
if request.method == "POST":
# Support single-candidate add via HTMX (button posts 'suggestion')
if request.htmx and request.POST.get("suggestion"):
suggestion = request.POST.get("suggestion").strip()
if suggestion:
existing = [s for s in question.answer_suggest_incorrect if s]
if suggestion not in existing:
existing.append(suggestion)
question.answer_suggest_incorrect = existing
question.save()
# Recompute candidates and render 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)
# Otherwise treat as the full form submission
form = SuggestIncorrectAnswerForm(request.POST, instance=question)
if form.is_valid():
obj = form.save()
obj.save()
# If HTMX requested, return the fragment so the UI can update in-place
if request.htmx:
# Recompute candidates and render 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)
return render(request, "anatomy/question_detail.html#suggest-incorrect-form", {"question": question})
return HttpResponse("Invalid")
else:
form = SuggestIncorrectAnswerForm(instance=question)
candidates = build_candidates(question)
context = {"form": form, "question": question, "candidates": candidates}
return render(request, "anatomy/suggest_incorrect.html", {"form": form, "question": question})
if request.htmx:
return render(request, "anatomy/partials/suggest_incorrect_fragment.html", context)
return render(request, "anatomy/suggest_incorrect.html", context)
return HttpResponse("Error", status=400)