Add bulk delete functionality for questions with confirmation feedback

This commit is contained in:
Ross
2025-10-20 12:32:59 +01:00
parent 1085b9e6a2
commit 980bb199cf
4 changed files with 101 additions and 3 deletions
@@ -0,0 +1,4 @@
<div class="alert alert-info">Deleted {{ deleted }} items.</div>
{% if errors %}
<div class="alert alert-warning">Errors: {{ errors }}</div>
{% endif %}
+1
View File
@@ -34,6 +34,7 @@ urlpatterns = [
views.generic_exam_json_edit,
name="generic_exam_json_edit",
),
path("bulk_delete_questions/", views.bulk_delete_questions, name="bulk_delete_questions"),
path(
"examination-autocomplete",
views.ExaminationAutocomplete.as_view(
+59
View File
@@ -149,6 +149,65 @@ from django.db.models import Prefetch
from loguru import logger
@login_required
@user_passes_test(lambda u: u.is_superuser)
def bulk_delete_questions(request):
"""Generic bulk delete for question-like models.
Expects POST with 'app' (app_name) and 'selected' (list or comma-separated ids).
Returns a small HTML fragment summarising deletions (suitable for HTMX).
"""
if request.method != "POST":
return HttpResponse(status=405)
app = request.POST.get("app")
selected_list = request.POST.getlist("selected")
selected_raw = request.POST.get("selected")
ids = set()
if selected_list:
for s in selected_list:
try:
ids.add(int(s))
except Exception:
continue
elif selected_raw:
for part in selected_raw.split(','):
try:
ids.add(int(part))
except Exception:
continue
# Map app name to model class
APP_MODEL_MAP = {
"sbas": SbasQuestion,
"physics": PhysicsQuestion,
"anatomy": AnatomyQuestion,
"rapids": RapidQuestion,
"rapid": RapidQuestion,
"shorts": ShortsQuestion,
"longs": LongQuestion,
"long": LongQuestion,
}
Model = APP_MODEL_MAP.get(app)
if Model is None:
return JsonResponse({"ok": False, "error": f"Unknown app: {app}"}, status=400)
deleted = 0
errors = []
try:
qs = Model.objects.filter(pk__in=list(ids))
deleted = qs.count()
qs.delete()
except Exception as e:
logger.exception("bulk_delete failed for app=%s ids=%s", app, ids)
return JsonResponse({"ok": False, "error": str(e)}, status=500)
# Render a small fragment
return render(request, "generic/partials/bulk_delete_result.html", {"deleted": deleted, "errors": errors})
class RedirectMixin:
def get_success_url(self) -> str:
if "redirect" in self.request.GET:
+37 -3
View File
@@ -20,11 +20,45 @@
</div>
{% render_table table %}
<form id="bulk-delete-form" method="post" action="{% url app_name|add:':bulk_delete_questions' %}" hx-post="{% url app_name|add:':bulk_delete_questions' %}" hx-target="#bulk-delete-result" hx-swap="innerHTML">
{% csrf_token %}
<input type="hidden" name="app" value="{{ app_name }}">
<input type="hidden" name="selected" id="bulk-selected-input" value="">
<button id="button-bulk-delete" class="btn btn-danger btn-sm" type="submit">Delete selected</button>
<span id="bulk-delete-result" class="ms-2"></span>
</form>
<button id="button-select-add-exam"
data-exam_json_edit_url="{% url 'generic:generic_exam_json_edit' %}"
data-exam_list_url="{% url 'api-1:'|add:app_name|add:'_user_exams' %}"
data-type={{app_name}} data-csrf="{{ csrf_token}}">Add selected questions to
data-exam_json_edit_url="{% url 'generic:generic_exam_json_edit' %}"
data-exam_list_url="{% url 'api-1:'|add:app_name|add:'_user_exams' %}"
data-type={{app_name}} data-csrf="{{ csrf_token}}">Add selected questions to
exam</button>
<button id="button-select-remove-questions"
data-question_bulk_delete_url="{% url app_name|add:':bulk_delete_questions' %}"
data-csrf="{{ csrf_token}}">Delete selected questions</button>
<div id="exam-options"></div>
{% endblock %}
{% block scripts %}
<script>
// When the user submits the bulk-delete form, gather selected checkboxes from the table
document.getElementById('bulk-delete-form').addEventListener('submit', function(e){
const selected = [];
document.querySelectorAll('input[type=checkbox][name="selection"]:checked, input[type=checkbox][name="selection[]"]:checked, input[type=checkbox][name="selection\[\]"]:checked').forEach(function(cb){
// many tables render checkbox inputs with value attribute as pk
if(cb.value) selected.push(cb.value);
else if(cb.dataset && cb.dataset.pk) selected.push(cb.dataset.pk);
});
// fallback: django-tables2 renders checkboxes with accessor name 'selection' and value
if(selected.length === 0){
// try to find inputs inside table
document.querySelectorAll('table input[type=checkbox]').forEach(function(cb){
if(cb.checked && cb.value) selected.push(cb.value);
});
}
document.getElementById('bulk-selected-input').value = selected.join(',');
// allow HTMX to submit
});
</script>
{% endblock %}