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,
|
views.generic_exam_json_edit,
|
||||||
name="generic_exam_json_edit",
|
name="generic_exam_json_edit",
|
||||||
),
|
),
|
||||||
|
path("bulk_delete_questions/", views.bulk_delete_questions, name="bulk_delete_questions"),
|
||||||
path(
|
path(
|
||||||
"examination-autocomplete",
|
"examination-autocomplete",
|
||||||
views.ExaminationAutocomplete.as_view(
|
views.ExaminationAutocomplete.as_view(
|
||||||
|
|||||||
@@ -149,6 +149,65 @@ from django.db.models import Prefetch
|
|||||||
from loguru import logger
|
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:
|
class RedirectMixin:
|
||||||
def get_success_url(self) -> str:
|
def get_success_url(self) -> str:
|
||||||
if "redirect" in self.request.GET:
|
if "redirect" in self.request.GET:
|
||||||
|
|||||||
@@ -20,11 +20,45 @@
|
|||||||
</div>
|
</div>
|
||||||
{% render_table table %}
|
{% 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"
|
<button id="button-select-add-exam"
|
||||||
data-exam_json_edit_url="{% url 'generic:generic_exam_json_edit' %}"
|
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-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-type={{app_name}} data-csrf="{{ csrf_token}}">Add selected questions to
|
||||||
exam</button>
|
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>
|
<div id="exam-options"></div>
|
||||||
|
|
||||||
{% endblock %}
|
{% 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