Refactor exam question fragment to move flag button inside the fragment for HTMX updates and enhance local state management for answered and flagged questions.

This commit is contained in:
Ross
2026-01-05 14:30:55 +00:00
parent 8c796c3048
commit db51ef23be
3 changed files with 42 additions and 1 deletions
+22 -1
View File
@@ -18,7 +18,7 @@
[<span id="question-number">{{ pos|add:1 }}</span>/<span id="exam-length">{{ exam_length }}</span>]
</h2>
<div class="ms-3">
{% include 'physics/partials/exam_flag_button.html' %}
<!-- flag button moved into the question fragment so it updates with HTMX swaps -->
</div>
</div>
@@ -69,6 +69,27 @@
}catch(e){ console && console.error && console.error(e); }
};
</script>
<script>
// Listen for flag button swaps (HTMX) and update the local menu state
document.addEventListener('htmx:afterSwap', function(evt){
try{
var target = (evt.detail && (evt.detail.target || evt.detail.elt)) || evt.target;
if(!target) return;
// If the flag button was swapped, update flagged array for current pos
var container = (target.id === 'flag-button-container') ? target : target.querySelector && target.querySelector('#flag-button-container');
if(!container) return;
var btn = container.querySelector('button');
if(!btn) return;
var flaggedNow = btn.classList.contains('btn-warning');
var pos = (typeof window.currentQuestionPos === 'number') ? window.currentQuestionPos : NaN;
if(!isNaN(pos)){
window.examQuestionFlagged = window.examQuestionFlagged || new Array({{ exam_length }}).fill(false);
window.examQuestionFlagged[pos] = !!flaggedNow;
if(typeof window.updateQuestionMenu === 'function') window.updateQuestionMenu();
}
}catch(e){ console && console.error && console.error(e); }
});
</script>
{% include "physics/exam_take_help.html" %}
@@ -1,4 +1,6 @@
<div class="exam-question-fragment">
<!-- Flag button lives inside the fragment so it updates together with other question controls -->
{% include 'physics/partials/exam_flag_button.html' %}
<form method="POST" class="post-form"
hx-post="{% if cid %}{% url 'physics:exam_take' pk=exam.pk sk=pos cid=cid passcode=passcode %}{% else %}{% url 'physics:exam_take_user' pk=exam.pk sk=pos %}{% endif %}"
hx-target="#question-fragment" hx-swap="innerHTML">
@@ -134,6 +136,14 @@
// build the question menu locally so it is available after fragment swaps
$('#menu-list').empty();
const serverAnswered = {{ answered_json|default:'null'|safe }};
const serverFlagged = {{ flagged_json|default:'null'|safe }};
if ((!window.examQuestionAnswered || !Array.isArray(window.examQuestionAnswered)) && Array.isArray(serverAnswered)) {
window.examQuestionAnswered = serverAnswered;
}
if ((!window.examQuestionFlagged || !Array.isArray(window.examQuestionFlagged)) && Array.isArray(serverFlagged)) {
window.examQuestionFlagged = serverFlagged;
}
const answeredArr = (window.examQuestionAnswered && Array.isArray(window.examQuestionAnswered)) ? window.examQuestionAnswered : null;
const flaggedArr = (window.examQuestionFlagged && Array.isArray(window.examQuestionFlagged)) ? window.examQuestionFlagged : null;
for (let i = 0; i < {{ exam_length }}; i++) {
+10
View File
@@ -485,6 +485,16 @@ def exam_take_fragment(request, pk: int, sk: int, cid: str | None = None, passco
"saved_answer": saved_answer,
"answer": answer,
"flagged": flagged,
"answered_json": json.dumps([
True if (q.cid_user_answers.filter(cid=cid, exam=exam).exists() if cid is not None else q.cid_user_answers.filter(user=request.user, exam=exam).exists()) else False
for q in questions
]),
"flagged_json": json.dumps([
(Flag.objects.filter(content_type=ContentType.objects.get_for_model(q), object_id=q.pk).filter(cid_user__cid=cid).exists()
if cid is not None
else Flag.objects.filter(content_type=ContentType.objects.get_for_model(q), object_id=q.pk).filter(user=request.user).exists())
for q in questions
]),
"passcode": passcode,
"cid_user_exam": cid_user_exam,
},