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 %}
+
+
+
+{% endblock %}
+
+{% block scripts %}
+
{% endblock %}
\ No newline at end of file