Implement bulk archive functionality for exams; add HTMX endpoint and update UI controls for improved exam management

This commit is contained in:
Ross
2025-11-11 20:52:44 +00:00
parent dc23dffaf9
commit e9d0e402be
3 changed files with 172 additions and 3 deletions
+115 -3
View File
@@ -3,10 +3,14 @@
{% block content %}
<div id="exam-list-wrapper">
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<button type="button" class="btn btn-sm btn-outline-secondary" id="bulk-toggle-btn">Bulk edit</button>
</div>
<div>
<button type="button" class="btn btn-sm btn-outline-secondary" id="bulk-toggle-btn">Bulk edit</button>
</div>
<div class="d-flex align-items-center">
<div class="me-2">
<button id="toggle-archive-btn" type="button" class="btn btn-sm btn-outline-info">Show archived</button>
</div>
<div id="bulk-controls" class="d-none">
<div>
<button type="button" class="btn btn-sm btn-outline-primary" id="select-all-btn">Select all</button>
@@ -31,6 +35,16 @@
{% csrf_token %}
<button class="btn btn-sm btn-danger" type="submit" onclick="return confirm('Delete selected exams? This action is irreversible.')">Delete selected</button>
</form>
<form id="bulk-archive-form" class="mt-2" hx-post="{% url app_name|add:':exam_bulk_archive' %}" hx-target="#exam-list-container" hx-include="#exam-list-container input[name='exam_ids']" hx-indicator="#bulk-action-indicator">
{% for k, v in request.GET.items %}
<input type="hidden" name="{{ k }}" value="{{ v }}" />
{% endfor %}
{% csrf_token %}
<div class="btn-group" role="group">
<button name="archive" value="true" class="btn btn-sm btn-warning" type="submit" onclick="return confirm('Archive selected exams?')">Archive selected</button>
<button name="archive" value="false" class="btn btn-sm btn-outline-warning" type="submit" onclick="return confirm('Unarchive selected exams?')">Unarchive selected</button>
</div>
</form>
<!-- Bulk action indicator shown while HTMX requests are running -->
<span id="bulk-action-indicator" class="ms-2" aria-hidden="true">
@@ -67,6 +81,8 @@
/* Defensive: hide bulk-controls unless bulk-mode is enabled */
#exam-list-wrapper:not(.bulk-mode) #bulk-controls { display: none !important; }
/* (bulk-only top-level controls removed) */
/* Bulk action indicator - hidden by default, shown while HTMX request is active */
#bulk-action-indicator { display: none; }
#bulk-action-indicator.htmx-request { display: inline-block; }
@@ -106,6 +122,58 @@
});
}
// Top-level archive/unarchive buttons: make visible and usable without hunting inside bulk panel
const topArchiveBtn = document.getElementById('top-archive-btn');
const topUnarchiveBtn = document.getElementById('top-unarchive-btn');
function ensureBulkModeVisible(){
if(!examListWrapper.classList.contains('bulk-mode')){
examListWrapper.classList.add('bulk-mode');
bulkControls.classList.remove('d-none');
}
}
function submitArchiveAction(isArchive){
// find the bulk-archive form already in the DOM
const form = document.getElementById('bulk-archive-form');
if(!form){
alert('Bulk archive form not found.');
return;
}
// if no checkboxes selected, open bulk-mode and focus user to select
const checked = Array.from(document.querySelectorAll('#exam-list-container input.exam-select-checkbox:checked'));
if(checked.length === 0){
ensureBulkModeVisible();
// scroll to controls so user can pick
bulkControls.scrollIntoView({behavior:'smooth', block:'center'});
// highlight controls briefly
bulkControls.classList.add('border', 'border-3', 'border-primary');
setTimeout(()=> bulkControls.classList.remove('border','border-3','border-primary'), 2000);
return;
}
// set the archive input value on the appropriate button and submit
const btn = form.querySelector('button[name="archive"][value="' + (isArchive ? 'true' : 'false') + '"]');
if(btn){
btn.click();
} else {
// fallback: add hidden input and submit
const hidden = document.createElement('input');
hidden.type = 'hidden';
hidden.name = 'archive';
hidden.value = isArchive ? 'true' : 'false';
form.appendChild(hidden);
form.requestSubmit();
}
}
if(topArchiveBtn){
topArchiveBtn.addEventListener('click', function(){ submitArchiveAction(true); });
}
if(topUnarchiveBtn){
topUnarchiveBtn.addEventListener('click', function(){ submitArchiveAction(false); });
}
// Make clicking an exam row toggle its checkbox when bulk-mode is enabled.
document.addEventListener('click', function(e){
if(!examListWrapper.classList.contains('bulk-mode')) return;
@@ -123,5 +191,49 @@
});
});
</script>
<script>
// Archive toggle button: toggle ?archive=1 while preserving other query params
(function(){
function getQueryParams(){
return new URLSearchParams(window.location.search);
}
function updateButton(){
const btn = document.getElementById('toggle-archive-btn');
if(!btn) return;
const params = getQueryParams();
if(params.get('archive') && params.get('archive') !== '0'){
btn.textContent = 'Hide archived';
btn.classList.remove('btn-outline-info');
btn.classList.add('btn-outline-secondary');
} else {
btn.textContent = 'Show archived';
btn.classList.remove('btn-outline-secondary');
btn.classList.add('btn-outline-info');
}
}
// handle toggle button click
document.addEventListener('click', function(e){
if(e.target && e.target.id === 'toggle-archive-btn'){
const params = getQueryParams();
if(params.get('archive') && params.get('archive') !== '0'){
params.delete('archive');
} else {
params.set('archive','1');
}
const q = params.toString();
const newUrl = window.location.pathname + (q ? ('?' + q) : '');
window.location.href = newUrl;
}
});
// init on load
document.addEventListener('DOMContentLoaded', updateButton);
// also update button when HTMX swaps content
document.addEventListener('htmx:afterSwap', updateButton);
document.addEventListener('htmx:afterOnLoad', updateButton);
})();
</script>
{% endblock %}