From 980bb199cf89dfc00438caece0b1cc39415d5aeb Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 20 Oct 2025 12:32:59 +0100 Subject: [PATCH] Add bulk delete functionality for questions with confirmation feedback --- .../generic/partials/bulk_delete_result.html | 4 ++ generic/urls.py | 1 + generic/views.py | 59 +++++++++++++++++++ templates/question_table_view.html | 40 ++++++++++++- 4 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 generic/templates/generic/partials/bulk_delete_result.html diff --git a/generic/templates/generic/partials/bulk_delete_result.html b/generic/templates/generic/partials/bulk_delete_result.html new file mode 100644 index 00000000..9ff82ee7 --- /dev/null +++ b/generic/templates/generic/partials/bulk_delete_result.html @@ -0,0 +1,4 @@ +
Deleted {{ deleted }} items.
+{% if errors %} +
Errors: {{ errors }}
+{% endif %} diff --git a/generic/urls.py b/generic/urls.py index 5aa73aa8..708c3164 100755 --- a/generic/urls.py +++ b/generic/urls.py @@ -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( diff --git a/generic/views.py b/generic/views.py index c193ad87..78506bc4 100644 --- a/generic/views.py +++ b/generic/views.py @@ -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: diff --git a/templates/question_table_view.html b/templates/question_table_view.html index 132d50f0..6f45d0b4 100644 --- a/templates/question_table_view.html +++ b/templates/question_table_view.html @@ -20,11 +20,45 @@ {% render_table table %} +
+ {% csrf_token %} + + + + +
+ +
+{% endblock %} + +{% block scripts %} + {% endblock %} \ No newline at end of file