Refactor exam take functionality to support HTMX for dynamic question loading and navigation

This commit is contained in:
Ross
2025-11-10 21:30:45 +00:00
parent 6eec40c43b
commit 41fc7050ad
4 changed files with 209 additions and 116 deletions
+7 -116
View File
@@ -33,75 +33,12 @@
</div>
<form method="POST" class="post-form">
{% csrf_token %}
{{ form.non_field_errors }}
<div class="form-contents">
<ol class="physics-answer-list">
<li data-ans="a">
<div class="fieldWrapper flex-container">
{{ form.a.errors }}
<label for="{{ form.a.id_for_label }}" class="flex-8 question-text">{{ question.a|safe }}</label>
<span class="flex-1">{{ form.a }}<span class="postinput"></span></span>
</div>
{% if exam.publish_results and question.a_feedback %}
<span class="feedback">Feedback: {{ question.a_feedback }}</span>
{% endif %}
</li>
<li data-ans="b">
<div class="fieldWrapper flex-container">
{{ form.b.errors }}
<label for="{{ form.b.id_for_label }}" class="flex-8 question-text">{{ question.b|safe }}</label>
<span class="flex-1">{{ form.b }}<span class="postinput"></span></span>
</div>
</li>
<li data-ans="c">
<div class="fieldWrapper flex-container">
{{ form.c.errors }}
<label for="{{ form.c.id_for_label }}" class="flex-8 question-text">{{ question.c|safe }}</label>
<span class="flex-1">{{ form.c }}<span class="postinput"></span></span>
</div>
</li>
<li data-ans="d">
<div class="fieldWrapper flex-container">
{{ form.d.errors }}
<label for="{{ form.d.id_for_label }}" class="flex-8 question-text">{{ question.d|safe }}</label>
<span class="flex-1">{{ form.d }}<span class="postinput"></span></span>
</div>
</li>
<li data-ans="e">
<div class="fieldWrapper flex-container">
{{ form.e.errors }}
<label for="{{ form.e.id_for_label }}" class="flex-8 question-text">{{ question.e|safe }}</label>
<span class="flex-1">{{ form.e }}<span class="postinput"></span></span>
</div>
</li>
</ol>
</div>
<div class="mt-2">
{% if previous > -1 %}
<button type="submit" name="previous" class="save btn btn-default" title="Click to save your answer(s) and go to the previous question">Previous</button>
{% endif %}
{% if next %}
<button type="submit" name="next" class="save btn btn-default" title="Click to save your answer(s) and go to the next question">Next</button>
{% else %}
{% if not exam.publish_results %}
<button type="submit" name="save" class="save btn btn-default" title="Click to save your current answer(s)">Save</button>
{% endif %}
{% endif %}
<br />
<button type="submit" id="overview-button" name="finish" class="save btn btn-default" title="Click to go to the overview page (your answers will be saved).">Overview</button>
<button type="submit" id="goto-button" value="test" name="goto" class="hide">goto</button>
</div>
</form>
<div id="question-fragment"
hx-trigger="load"
hx-swap="innerHTML"
hx-get="{% if cid %}{% url 'physics:exam_take_fragment' pk=exam.pk sk=pos cid=cid passcode=passcode %}{% else %}{% url 'physics:exam_take_fragment_user' pk=exam.pk sk=pos %}{% endif %}">
<!-- HTMX will load the question form + controls here -->
</div>
</div>
<h4>Questions</h4>
@@ -111,53 +48,7 @@
{% endblock %}
{% block js %}
<script>
$(document).ready(() => {
/* beautify ignore:start */
{% if not exam.publish_results and not cid_user_exam.completed %}
let time_limit = '{{ exam.time_limit }}';
if (time_limit != 'None') {
let end_time = new Date({{ cid_user_exam.start_time|date:"U" }} * 1000 + parseInt('{{ exam.time_limit }}') * 1000);
initializeClock('clockdiv', end_time);
}
{% elif exam.publish_results %}
$('ol.physics-answer-list input').each((n, el) => { el.disabled = true; });
{{ question.get_answers_js }}.forEach((el, n) => {
const li_el = $(`ol.physics-answer-list li:eq(${n})`);
if (el) {
li_el.addClass('answer-true');
} else {
li_el.addClass('answer-false');
}
if (el == li_el.find('input').get(0).checked) {
li_el.addClass('answer-correct');
} else {
li_el.addClass('answer-incorrect');
}
});
{% elif cid_user_exam.completed %}
$('ol.physics-answer-list input').each((n, el) => { el.disabled = true; });
{% endif %}
for (let i = 0; i < {{ exam_length }}; i++) {
const qbutton = $(`<button class="question-menu-item" name="goto-${i}" data-qn="${i}">${i+1}</button>`);
if (i == {{ pos }}) { qbutton.addClass('current-question'); }
$('#menu-list').append(qbutton);
}
$('button.question-menu-item').on('click', (e) => {
document.getElementById('goto-button').value = e.currentTarget.dataset.qn;
$('#goto-button').click();
});
/* beautify ignore:end */
});
</script>
{% endblock %}
{# HTMX fragment will load question-specific JS; nothing required here #}
{% block css %}
<style type="text/css">
@@ -0,0 +1,120 @@
<div class="exam-question-fragment">
<form method="POST" class="post-form">
{% csrf_token %}
{{ form.non_field_errors }}
<div class="form-contents">
<ol class="physics-answer-list">
<li data-ans="a">
<div class="fieldWrapper flex-container">
{{ form.a.errors }}
<label for="{{ form.a.id_for_label }}" class="flex-8 question-text">{{ question.a|safe }}</label>
<span class="flex-1">{{ form.a }}<span class="postinput"></span></span>
</div>
{% if exam.publish_results and question.a_feedback %}
<span class="feedback">Feedback: {{ question.a_feedback }}</span>
{% endif %}
</li>
<li data-ans="b">
<div class="fieldWrapper flex-container">
{{ form.b.errors }}
<label for="{{ form.b.id_for_label }}" class="flex-8 question-text">{{ question.b|safe }}</label>
<span class="flex-1">{{ form.b }}<span class="postinput"></span></span>
</div>
</li>
<li data-ans="c">
<div class="fieldWrapper flex-container">
{{ form.c.errors }}
<label for="{{ form.c.id_for_label }}" class="flex-8 question-text">{{ question.c|safe }}</label>
<span class="flex-1">{{ form.c }}<span class="postinput"></span></span>
</div>
</li>
<li data-ans="d">
<div class="fieldWrapper flex-container">
{{ form.d.errors }}
<label for="{{ form.d.id_for_label }}" class="flex-8 question-text">{{ question.d|safe }}</label>
<span class="flex-1">{{ form.d }}<span class="postinput"></span></span>
</div>
</li>
<li data-ans="e">
<div class="fieldWrapper flex-container">
{{ form.e.errors }}
<label for="{{ form.e.id_for_label }}" class="flex-8 question-text">{{ question.e|safe }}</label>
<span class="flex-1">{{ form.e }}<span class="postinput"></span></span>
</div>
</li>
</ol>
</div>
<div class="mt-2">
{% if previous > -1 %}
<button type="submit" name="previous" class="save btn btn-default" title="Click to save your answer(s) and go to the previous question">Previous</button>
{% endif %}
{% if next %}
<button type="submit" name="next" class="save btn btn-default" title="Click to save your answer(s) and go to the next question">Next</button>
{% else %}
{% if not exam.publish_results %}
<button type="submit" name="save" class="save btn btn-default" title="Click to save your current answer(s)">Save</button>
{% endif %}
{% endif %}
<br />
<button type="submit" id="overview-button" name="finish" class="save btn btn-default" title="Click to go to the overview page (your answers will be saved).">Overview</button>
<button type="submit" id="goto-button" value="test" name="goto" class="hide">goto</button>
</div>
</form>
<script>
(function () {
/* run after fragment is loaded */
{% if not exam.publish_results and not cid_user_exam.completed %}
let time_limit = '{{ exam.time_limit }}';
if (time_limit != 'None') {
let end_time = new Date({{ cid_user_exam.start_time|date:"U" }} * 1000 + parseInt('{{ exam.time_limit }}') * 1000);
if (typeof initializeClock === 'function') {
initializeClock('clockdiv', end_time);
}
}
{% elif exam.publish_results %}
$('ol.physics-answer-list input').each((n, el) => { el.disabled = true; });
{{ question.get_answers_js }}.forEach((el, n) => {
const li_el = $(`ol.physics-answer-list li:eq(${n})`);
if (el) {
li_el.addClass('answer-true');
} else {
li_el.addClass('answer-false');
}
if (el == li_el.find('input').get(0).checked) {
li_el.addClass('answer-correct');
} else {
li_el.addClass('answer-incorrect');
}
});
{% elif cid_user_exam.completed %}
$('ol.physics-answer-list input').each((n, el) => { el.disabled = true; });
{% endif %}
// build the question menu locally so it is available after fragment swaps
$('#menu-list').empty();
for (let i = 0; i < {{ exam_length }}; i++) {
const qbutton = $(`<button class="question-menu-item" name="goto-${i}" data-qn="${i}">${i+1}</button>`);
if (i == {{ pos }}) { qbutton.addClass('current-question'); }
$('#menu-list').append(qbutton);
}
$('button.question-menu-item').on('click', (e) => {
document.getElementById('goto-button').value = e.currentTarget.dataset.qn;
$('#goto-button').click();
});
})();
</script>
</div>