diff --git a/generic/templates/generic/exam_index.html b/generic/templates/generic/exam_index.html index 6e33966f..0c4ad510 100644 --- a/generic/templates/generic/exam_index.html +++ b/generic/templates/generic/exam_index.html @@ -1,26 +1,33 @@ {% extends app_name|add:'/base.html' %} {% block content %} -

{{filter.qs|length}} exams found.

- {% for exam in filter.qs %} -
-

- - {% if exam.exam_mode %} - Exam: - {% else %} - Packet: - {% endif %} - {{ exam }}

- Authors: {{exam.get_authors}} - {% if exam.exam_mode %} - {% if app_name in "rapids longs anatomy" %} - {% if request.user.is_staff %}Mark answers{% endif %} - {% endif %} - {% if request.user.is_staff %}Scores{% endif %} - {% endif %} +
+
+ +
- {% endfor %} +
+
+ {% for k, v in request.GET.items %} + + {% endfor %} + {% csrf_token %} +
+ + +
+
+
+ {% for k, v in request.GET.items %} + + {% endfor %} + {% csrf_token %} + +
+
+
+ + {% include 'generic/partials/_exam_list.html' with filter=filter app_name=app_name %} {% include "generic/partials/filter_bar.html" with filter=filter app_name=app_name collapse_id="bottom-filter-body" %} {% endblock %} @@ -37,3 +44,23 @@ {% endblock css %} +{% block extra_js %} + +{% endblock %} + diff --git a/generic/templates/generic/partials/_exam_list.html b/generic/templates/generic/partials/_exam_list.html new file mode 100644 index 00000000..d405fa1e --- /dev/null +++ b/generic/templates/generic/partials/_exam_list.html @@ -0,0 +1,26 @@ +{# Partial: list of exams used by HTMX bulk actions #} +
+

{{ filter.qs|length }} exams found.

+ + {% for exam in filter.qs %} +
+
+
+ +
+
+
+ {% if exam.exam_mode %}Exam:{% else %}Packet:{% endif %} {{ exam }} +
+ Authors: {{ exam.get_authors }} + {% if exam.exam_mode %} + {% if app_name in "rapids longs anatomy" %} + {% if request.user.is_staff %}Mark answers{% endif %} + {% endif %} + {% if request.user.is_staff %}Scores{% endif %} + {% endif %} +
+
+
+ {% endfor %} +
diff --git a/generic/urls.py b/generic/urls.py index d3258535..1b2ac1dd 100755 --- a/generic/urls.py +++ b/generic/urls.py @@ -333,6 +333,16 @@ def generic_view_urls(generic_views: GenericViewBase): def generic_exam_urls(generic_exam_view: GenericExamViews): urlpatterns = [ + path( + "exam/bulk_update/", + generic_exam_view.exam_bulk_update, + name="exam_bulk_update", + ), + path( + "exam/bulk_delete/", + generic_exam_view.exam_bulk_delete, + name="exam_bulk_delete", + ), path("", generic_exam_view.index, name="index"), path("exam//", generic_exam_view.exam_overview, name="exam_overview"), path( diff --git a/generic/views.py b/generic/views.py index 027ab42c..8ed1217f 100644 --- a/generic/views.py +++ b/generic/views.py @@ -988,6 +988,117 @@ class ExamViews(View, LoginRequiredMixin): ) ) + @method_decorator(login_required) + def exam_bulk_update(self, request): + """HTMX endpoint to bulk-update exams (currently supports updating the date). + + Expects POST with 'exam_ids' (multiple) and optional 'date' (YYYY-MM-DD). + Returns the updated exam list partial. + """ + if request.method != "POST": + return HttpResponse(status=405) + + exam_ids = request.POST.getlist("exam_ids") + date_str = request.POST.get("date", "") + + # Parse date if provided + new_date = None + if date_str: + try: + from datetime import date as _date + + new_date = _date.fromisoformat(date_str) + except Exception: + new_date = None + + # Find the queryset for the current user as index() does + if ( + request.user.is_superuser + or ( + self.app_name == "rapids" + and request.user.groups.filter(name="rapid_checker").exists() + ) + or ( + self.app_name == "anatomy" + and request.user.groups.filter(name="anatomy_checker").exists() + ) + or ( + self.app_name == "longs" + and request.user.groups.filter(name="long_checker").exists() + ) + ): + exams_qs = self.Exam.objects.all() + filter = self.ExtraExamFilter(request.GET, queryset=exams_qs) + else: + exams_qs = self.Exam.objects.filter(author__id=request.user.id) | self.Exam.objects.filter(open_access=True) + filter = self.BasicExamFilter(request.GET, queryset=exams_qs) + + # Apply changes + for eid in exam_ids: + try: + exam = self.Exam.objects.get(pk=eid) + except self.Exam.DoesNotExist: + continue + + if not self.check_user_edit_access(request.user, exam_id=exam.pk): + # Skip exams the user cannot edit + continue + + if new_date is not None: + exam.date = new_date + exam.save() + + return render(request, "generic/partials/_exam_list.html", {"filter": filter, "app_name": self.app_name}) + + @method_decorator(login_required) + def exam_bulk_delete(self, request): + """HTMX endpoint to bulk-delete selected exams. Expects POST 'exam_ids'. + + Returns the updated exam list partial. + """ + if request.method != "POST": + return HttpResponse(status=405) + + exam_ids = request.POST.getlist("exam_ids") + + if ( + request.user.is_superuser + or ( + self.app_name == "rapids" + and request.user.groups.filter(name="rapid_checker").exists() + ) + or ( + self.app_name == "anatomy" + and request.user.groups.filter(name="anatomy_checker").exists() + ) + or ( + self.app_name == "longs" + and request.user.groups.filter(name="long_checker").exists() + ) + ): + exams_qs = self.Exam.objects.all() + filter = self.ExtraExamFilter(request.GET, queryset=exams_qs) + else: + exams_qs = self.Exam.objects.filter(author__id=request.user.id) | self.Exam.objects.filter(open_access=True) + filter = self.BasicExamFilter(request.GET, queryset=exams_qs) + + for eid in exam_ids: + try: + exam = self.Exam.objects.get(pk=eid) + except self.Exam.DoesNotExist: + continue + + if not self.check_user_edit_access(request.user, exam_id=exam.pk): + continue + + try: + exam.delete() + except Exception: + # ignore individual delete failures + continue + + return render(request, "generic/partials/_exam_list.html", {"filter": filter, "app_name": self.app_name}) + @method_decorator(login_required) def exam_toggle_results_published(self, request, pk): if request.method == "POST":