Add bulk delete functionality for questions with confirmation feedback
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
<div class="alert alert-info">Deleted {{ deleted }} items.</div>
|
||||
{% if errors %}
|
||||
<div class="alert alert-warning">Errors: {{ errors }}</div>
|
||||
{% endif %}
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 %}
|
||||
Reference in New Issue
Block a user