Add flagging functionality for exam questions with a reusable Flag model and toggle feature

This commit is contained in:
Ross
2026-01-05 13:47:45 +00:00
parent e4cfb1f782
commit bf344f20e1
6 changed files with 246 additions and 47 deletions
@@ -0,0 +1,17 @@
{% comment %}A small flag/unflag button partial. Re-rendered by HTMX when toggled.{% endcomment %}
<div id="flag-button-container">
{% load static %}
{% if flagged %}
<form method="POST" hx-post="{% if cid %}{% url 'physics:exam_toggle_flag' pk=exam.pk sk=pos cid=cid passcode=passcode %}{% else %}{% url 'physics:exam_toggle_flag_user' pk=exam.pk sk=pos %}{% endif %}" hx-target="#flag-button-container" hx-swap="outerHTML">
{% csrf_token %}
<input type="hidden" name="set" value="false" />
<button type="submit" class="btn btn-warning btn-sm" title="Unflag question for review">Unflag <i class="bi bi-flag-fill"></i></button>
</form>
{% else %}
<form method="POST" hx-post="{% if cid %}{% url 'physics:exam_toggle_flag' pk=exam.pk sk=pos cid=cid passcode=passcode %}{% else %}{% url 'physics:exam_toggle_flag_user' pk=exam.pk sk=pos %}{% endif %}" hx-target="#flag-button-container" hx-swap="outerHTML">
{% csrf_token %}
<input type="hidden" name="set" value="true" />
<button type="submit" class="btn btn-outline-warning btn-sm" title="Flag question for review">Flag <i class="bi bi-flag"></i></button>
</form>
{% endif %}
</div>
@@ -55,6 +55,9 @@
</div>
<div class="mt-2">
<div id="flag-button-container" class="mb-2">
{% include 'physics/partials/exam_flag_button.html' %}
</div>
{% if previous > -1 %}
<button type="submit" name="previous" class="save btn btn-secondary" title="Click to save your answer(s) and go to the previous question">Previous</button>
{% endif %}
@@ -294,59 +297,59 @@
})();
</script>
<!-- Save / Skip Modal -->
<div class="modal fade" id="saveSkipModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Confirm</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p id="saveSkipModalMessage">Are you sure?</p>
</div>
<div class="modal-footer">
<button type="button" id="saveSkipModalSave" class="btn btn-primary">Save</button>
<button type="button" id="saveSkipModalSkip" class="btn btn-secondary">Skip</button>
<button type="button" id="saveSkipModalCancel" class="btn btn-link" data-bs-dismiss="modal">Cancel</button>
</div>
<div class="modal fade" id="saveSkipModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Confirm</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p id="saveSkipModalMessage">Are you sure?</p>
</div>
<div class="modal-footer">
<button type="button" id="saveSkipModalSave" class="btn btn-primary">Save</button>
<button type="button" id="saveSkipModalSkip" class="btn btn-secondary">Skip</button>
<button type="button" id="saveSkipModalCancel" class="btn btn-link" data-bs-dismiss="modal">Cancel</button>
</div>
</div>
</div>
</div>
<script>
<script>
// Modal helper: returns a Promise resolved with 'save'|'skip'|'cancel'
function openSaveSkipModal(message){
return new Promise(function(resolve, reject){
try{
let modalEl = document.getElementById('saveSkipModal');
if(!modalEl) return resolve('cancel');
modalEl.querySelector('#saveSkipModalMessage').textContent = message || '';
const bsModal = new bootstrap.Modal(modalEl, { backdrop: 'static' });
function openSaveSkipModal(message){
return new Promise(function(resolve, reject){
try{
let modalEl = document.getElementById('saveSkipModal');
if(!modalEl) return resolve('cancel');
modalEl.querySelector('#saveSkipModalMessage').textContent = message || '';
const bsModal = new bootstrap.Modal(modalEl, { backdrop: 'static' });
function cleanup(){
saveBtn.removeEventListener('click', onSave);
skipBtn.removeEventListener('click', onSkip);
cancelBtn.removeEventListener('click', onCancel);
modalEl.removeEventListener('hidden.bs.modal', onHidden);
}
function cleanup(){
saveBtn.removeEventListener('click', onSave);
skipBtn.removeEventListener('click', onSkip);
cancelBtn.removeEventListener('click', onCancel);
modalEl.removeEventListener('hidden.bs.modal', onHidden);
}
function onSave(){ cleanup(); bsModal.hide(); resolve('save'); }
function onSkip(){ cleanup(); bsModal.hide(); resolve('skip'); }
function onCancel(){ cleanup(); bsModal.hide(); resolve('cancel'); }
function onHidden(){ cleanup(); resolve('cancel'); }
function onSave(){ cleanup(); bsModal.hide(); resolve('save'); }
function onSkip(){ cleanup(); bsModal.hide(); resolve('skip'); }
function onCancel(){ cleanup(); bsModal.hide(); resolve('cancel'); }
function onHidden(){ cleanup(); resolve('cancel'); }
const saveBtn = modalEl.querySelector('#saveSkipModalSave');
const skipBtn = modalEl.querySelector('#saveSkipModalSkip');
const cancelBtn = modalEl.querySelector('#saveSkipModalCancel');
const saveBtn = modalEl.querySelector('#saveSkipModalSave');
const skipBtn = modalEl.querySelector('#saveSkipModalSkip');
const cancelBtn = modalEl.querySelector('#saveSkipModalCancel');
saveBtn.addEventListener('click', onSave);
skipBtn.addEventListener('click', onSkip);
cancelBtn.addEventListener('click', onCancel);
modalEl.addEventListener('hidden.bs.modal', onHidden);
saveBtn.addEventListener('click', onSave);
skipBtn.addEventListener('click', onSkip);
cancelBtn.addEventListener('click', onCancel);
modalEl.addEventListener('hidden.bs.modal', onHidden);
bsModal.show();
}catch(e){ reject(e); }
});
}
</script>
bsModal.show();
}catch(e){ reject(e); }
});
}
</script>
</div>