Add HTMX support for exam overview fragment to enhance user experience
This commit is contained in:
@@ -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>
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user