Enhance exam take functionality to support HTMX for dynamic question loading and navigation
This commit is contained in:
@@ -209,3 +209,40 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block js %}
|
||||||
|
<script>
|
||||||
|
/* HTMX swap helpers: preserve scroll and container height to avoid page jumping
|
||||||
|
Listens for htmx:beforeSwap and htmx:afterSwap on the question fragment container.
|
||||||
|
*/
|
||||||
|
(function(){
|
||||||
|
function getTarget(evt){
|
||||||
|
return (evt.detail && (evt.detail.target || evt.detail.elt)) || evt.target;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('htmx:beforeSwap', function(evt){
|
||||||
|
try{
|
||||||
|
var target = getTarget(evt);
|
||||||
|
if(!target || target.id !== 'question-fragment') return;
|
||||||
|
// save scroll position
|
||||||
|
target.__savedScrollY = window.scrollY || window.pageYOffset || 0;
|
||||||
|
// preserve current height to avoid layout shift while fragment is swapping
|
||||||
|
target.style.minHeight = target.offsetHeight + 'px';
|
||||||
|
}catch(e){ console && console.error && console.error(e); }
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('htmx:afterSwap', function(evt){
|
||||||
|
try{
|
||||||
|
var target = getTarget(evt);
|
||||||
|
if(!target || target.id !== 'question-fragment') return;
|
||||||
|
var y = target.__savedScrollY;
|
||||||
|
if(typeof y === 'number') window.scrollTo(0, y);
|
||||||
|
// remove the minHeight after a short delay to allow content to layout
|
||||||
|
setTimeout(function(){ target.style.minHeight = ''; }, 60);
|
||||||
|
// notify fragment-loaded so fragment init code can listen if required
|
||||||
|
target.dispatchEvent(new CustomEvent('fragment:loaded', { bubbles: true }));
|
||||||
|
}catch(e){ console && console.error && console.error(e); }
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<div class="exam-question-fragment">
|
<div class="exam-question-fragment">
|
||||||
<form method="POST" class="post-form">
|
<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">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form.non_field_errors }}
|
{{ form.non_field_errors }}
|
||||||
|
|
||||||
@@ -95,7 +97,7 @@
|
|||||||
if (el == li_el.find('input').get(0).checked) {
|
if (el == li_el.find('input').get(0).checked) {
|
||||||
li_el.addClass('answer-correct');
|
li_el.addClass('answer-correct');
|
||||||
} else {
|
} else {
|
||||||
li_el.addClass('answer-incorrect');
|
li_el.addClass('answer-incorrect');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+13
-1
@@ -266,16 +266,28 @@ def exam_take(request, pk: int, sk: int, cid: str | None = None, passcode: str |
|
|||||||
take_url = "physics:exam_take_user"
|
take_url = "physics:exam_take_user"
|
||||||
finish_url = "physics:exam_take_overview_user"
|
finish_url = "physics:exam_take_overview_user"
|
||||||
|
|
||||||
|
# Support HTMX: if this is an HX request, return the fragment HTML for the
|
||||||
|
# destination question instead of issuing a full-page redirect. This lets
|
||||||
|
# the client swap only the question fragment.
|
||||||
|
is_htmx = request.headers.get("HX-Request") == "true" or request.META.get("HTTP_HX_REQUEST")
|
||||||
|
|
||||||
if "next" in request.POST:
|
if "next" in request.POST:
|
||||||
if not next:
|
if not next:
|
||||||
return HttpResponseBadRequest("Invalid request")
|
return HttpResponseBadRequest("Invalid request")
|
||||||
|
if is_htmx:
|
||||||
|
return exam_take_fragment(request, pk=pk, sk=pos + 1, cid=cid, passcode=passcode)
|
||||||
return redirect(take_url, sk=pos + 1, **kwargs)
|
return redirect(take_url, sk=pos + 1, **kwargs)
|
||||||
elif "previous" in request.POST:
|
elif "previous" in request.POST:
|
||||||
|
if is_htmx:
|
||||||
|
return exam_take_fragment(request, pk=pk, sk=pos - 1, cid=cid, passcode=passcode)
|
||||||
return redirect(take_url, sk=pos - 1, **kwargs)
|
return redirect(take_url, sk=pos - 1, **kwargs)
|
||||||
elif "finish" in request.POST:
|
elif "finish" in request.POST:
|
||||||
return redirect(finish_url, **kwargs)
|
return redirect(finish_url, **kwargs)
|
||||||
elif "goto" in request.POST:
|
elif "goto" in request.POST:
|
||||||
return redirect(take_url, sk=int(request.POST.get("goto")), **kwargs)
|
dest = int(request.POST.get("goto"))
|
||||||
|
if is_htmx:
|
||||||
|
return exam_take_fragment(request, pk=pk, sk=dest, cid=cid, passcode=passcode)
|
||||||
|
return redirect(take_url, sk=dest, **kwargs)
|
||||||
else:
|
else:
|
||||||
form = UserAnswerForm(instance=answer)
|
form = UserAnswerForm(instance=answer)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user