Add HTMX support for exam overview fragment to enhance user experience

This commit is contained in:
Ross
2025-11-10 21:52:46 +00:00
parent c1eff140f8
commit 52c4f27e2b
2 changed files with 104 additions and 0 deletions
@@ -0,0 +1,68 @@
<div class="exam-overview-fragment">
<h3>Overview: {{ exam }}</h3>
<p>{{ answer_count }} / {{ exam_length }} answered</p>
<div class="overview-list">
<ol class="overview-questions">
{% for q, ans in question_answer_tuples %}
<li class="overview-item" data-qn-index="{{ forloop.counter0 }}">
<div style="display:flex;align-items:center;gap:0.75rem;">
<button class="btn btn-sm btn-outline-secondary goto-from-overview" data-qn="{{ forloop.counter0 }}">{{ forloop.counter }}</button>
<div style="flex:1; padding-left:0.5rem;">{{ q.stem|truncatechars:140|safe }}</div>
<div style="min-width:6rem; text-align:right;">
{% if ans %}
{% if ans.a or ans.b or ans.c or ans.d or ans.e %}
<span class="badge bg-success">Answered</span>
{% else %}
<span class="badge bg-secondary">Unanswered</span>
{% endif %}
{% else %}
<span class="badge bg-secondary">Unanswered</span>
{% endif %}
</div>
</div>
</li>
{% endfor %}
</ol>
</div>
<div style="margin-top:1rem;">
<button id="overview-back-to-question" class="btn btn-primary">Back to current question</button>
</div>
<script>
(function(){
// When a question button is clicked, trigger the hidden goto button in the
// main fragment so the server navigates to that question via the normal flow.
document.querySelectorAll('.goto-from-overview').forEach(function(btn){
btn.addEventListener('click', function(e){
var q = e.currentTarget.dataset.qn;
// Find the form in the currently loaded fragment (it may be the overview itself)
var form = document.querySelector('#question-fragment form.post-form');
if (!form) return;
var goto = form.querySelector('button[name="goto"]') || form.querySelector('#goto-button');
if (!goto) return;
// Set the value and submit
if (goto.tagName.toLowerCase() === 'input' || goto.tagName.toLowerCase() === 'button') {
goto.value = q;
// submit via HTMX by clicking the hidden goto button
goto.click();
}
});
});
// Back button simply reloads the current question fragment by triggering a
// click on the menu item for the current question (if available) or reloads
// via HTMX load of the fragment.
document.getElementById('overview-back-to-question').addEventListener('click', function(){
var menuCurrent = document.querySelector('#menu-list .current-question');
if (menuCurrent) { menuCurrent.click(); return; }
// fallback: force HTMX to reload the current fragment
var frag = document.getElementById('question-fragment');
if (frag && frag.getAttribute('hx-get')) {
htmx.trigger(frag, 'load');
}
});
})();
</script>
</div>
+36
View File
@@ -282,6 +282,42 @@ def exam_take(request, pk: int, sk: int, cid: str | None = None, passcode: str |
return exam_take_fragment(request, pk=pk, sk=pos - 1, cid=cid, passcode=passcode)
return redirect(take_url, sk=pos - 1, **kwargs)
elif "finish" in request.POST:
# For HTMX clients, return an overview fragment so the page can
# update in-place instead of performing a full-page redirect.
if is_htmx:
# Build the same context used by exam_take_overview
questions = exam.get_questions()
if cid is not None:
answers = UserAnswer.objects.filter(cid=cid, exam=exam)
else:
answers = UserAnswer.objects.filter(user=request.user, exam=exam)
answer_question_map = {ans.question: ans for ans in answers}
question_answer_tuples = []
answer_count = 0
for q in questions:
if q in answer_question_map:
question_answer_tuples.append((q, answer_question_map[q]))
answer_count += 1
else:
question_answer_tuples.append((q, None))
cid_user_exam = exam.get_or_create_cid_user_exam(cid=cid, user_user=request.user)
return render(
request,
"physics/partials/exam_take_overview_fragment.html",
{
"exam": exam,
"cid": cid,
"question_answer_tuples": question_answer_tuples,
"answer_count": answer_count,
"exam_length": len(questions),
"passcode": passcode,
"cid_user_exam": cid_user_exam,
},
)
return redirect(finish_url, **kwargs)
elif "save" in request.POST:
# Save action should not trigger a full-page load for HTMX clients.