From 0a9aeca3bc28e958441da0b076b13e00cb57ddbe Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Wed, 9 May 2018 11:13:00 +0200 Subject: [PATCH] Bulk deletion for subevents --- .../pretixcontrol/subevents/delete.html | 2 +- .../pretixcontrol/subevents/delete_bulk.html | 41 ++++ .../pretixcontrol/subevents/index.html | 184 ++++++++++-------- src/pretix/control/urls.py | 1 + src/pretix/control/views/subevents.py | 62 +++++- src/pretix/static/pretixcontrol/js/ui/main.js | 30 ++- src/tests/control/test_events.py | 43 ++++ 7 files changed, 280 insertions(+), 83 deletions(-) create mode 100644 src/pretix/control/templates/pretixcontrol/subevents/delete_bulk.html diff --git a/src/pretix/control/templates/pretixcontrol/subevents/delete.html b/src/pretix/control/templates/pretixcontrol/subevents/delete.html index e6c9398c46..9db73eb1ed 100644 --- a/src/pretix/control/templates/pretixcontrol/subevents/delete.html +++ b/src/pretix/control/templates/pretixcontrol/subevents/delete.html @@ -8,7 +8,7 @@ {% csrf_token %}

{% blocktrans %}Are you sure you want to delete the date {{ subevent }}?{% endblocktrans %}

- + {% trans "Cancel" %} +
+ +{% endblock %} diff --git a/src/pretix/control/templates/pretixcontrol/subevents/index.html b/src/pretix/control/templates/pretixcontrol/subevents/index.html index 5ba66e6fce..a029219827 100644 --- a/src/pretix/control/templates/pretixcontrol/subevents/index.html +++ b/src/pretix/control/templates/pretixcontrol/subevents/index.html @@ -45,89 +45,113 @@ class="btn btn-default"> {% trans "Create many new dates" context "subevent" %}

-
- - - - - - - - - - - - {% for s in subevents %} + + {% csrf_token %} +
+
- {% trans "Name" %} - - {% trans "Begin" %} - - - - {% trans "Paid tickets per quota" %} - - - - {% trans "Status" %} - - -
+ - - - - + + + + + + - {% endfor %} - -
- - {{ s.name }} - {{ s.get_date_from_display }} - {% for q in s.first_quotas|slice:":3" %} - {% include "pretixcontrol/fragment_quota_box_paid.html" with quota=q %} - {% endfor %} - {% if s.first_quotas|length > 3 %} - - ··· - + + {% if "can_change_event_settings" in request.eventpermset %} + {% endif %} - - - {% if not s.active %} - {% trans "Disabled" %} - {% elif s.presale_has_ended %} - {% trans "Presale over" %} - {% elif not s.presale_is_running %} - {% trans "Presale not started" %} - {% else %} - {% trans "On sale" %} - {% endif %} - - - - - + {% trans "Name" %} + + {% trans "Begin" %} + + + + {% trans "Paid tickets per quota" %} + + + + {% trans "Status" %} + + +
-
+ + + {% for s in subevents %} + + + {% if "can_change_event_settings" in request.eventpermset %} + + {% endif %} + + + + {{ s.name }} + + {{ s.get_date_from_display }} + + {% for q in s.first_quotas|slice:":3" %} + {% include "pretixcontrol/fragment_quota_box_paid.html" with quota=q %} + {% endfor %} + {% if s.first_quotas|length > 3 %} + + ··· + + {% endif %} + + + {% if not s.active %} + {% trans "Disabled" %} + {% elif s.presale_has_ended %} + {% trans "Presale over" %} + {% elif not s.presale_is_running %} + {% trans "Presale not started" %} + {% else %} + {% trans "On sale" %} + {% endif %} + + + +
+ + +
+ + + + {% endfor %} + + + + {% if "can_change_event_settings" in request.eventpermset %} + + + + {% endif %} + {% include "pretixcontrol/pagination.html" %} {% endif %} {% endblock %} diff --git a/src/pretix/control/urls.py b/src/pretix/control/urls.py index 63da063aed..856089644b 100644 --- a/src/pretix/control/urls.py +++ b/src/pretix/control/urls.py @@ -106,6 +106,7 @@ urlpatterns = [ name='event.subevent.delete'), url(r'^subevents/add$', subevents.SubEventCreate.as_view(), name='event.subevents.add'), url(r'^subevents/bulk_add$', subevents.SubEventBulkCreate.as_view(), name='event.subevents.bulk'), + url(r'^subevents/bulk_action$', subevents.SubEventBulkAction.as_view(), name='event.subevents.bulkaction'), url(r'^items/$', item.ItemList.as_view(), name='event.items'), url(r'^items/add$', item.ItemCreate.as_view(), name='event.items.add'), url(r'^items/(?P\d+)/$', item.ItemUpdateGeneral.as_view(), name='event.item'), diff --git a/src/pretix/control/views/subevents.py b/src/pretix/control/views/subevents.py index bddda51969..a7924049e2 100644 --- a/src/pretix/control/views/subevents.py +++ b/src/pretix/control/views/subevents.py @@ -9,10 +9,11 @@ from django.db.models import F, IntegerField, OuterRef, Prefetch, Subquery, Sum from django.db.models.functions import Coalesce from django.forms import inlineformset_factory from django.http import Http404, HttpResponseRedirect -from django.shortcuts import redirect +from django.shortcuts import redirect, render from django.utils.functional import cached_property from django.utils.timezone import make_aware from django.utils.translation import pgettext_lazy, ugettext_lazy as _ +from django.views import View from django.views.generic import CreateView, DeleteView, ListView, UpdateView from pretix.base.models.checkin import CheckinList @@ -469,6 +470,65 @@ class SubEventCreate(SubEventEditorMixin, EventPermissionRequiredMixin, CreateVi return formlist +class SubEventBulkAction(EventPermissionRequiredMixin, View): + permission = 'can_change_settings' + + @cached_property + def objects(self): + return self.request.event.subevents.filter( + id__in=self.request.POST.getlist('subevent') + ) + + @transaction.atomic + def post(self, request, *args, **kwargs): + if request.POST.get('action') == 'disable': + for obj in self.objects: + obj.log_action( + 'pretix.subevent.changed', user=self.request.user, data={ + 'active': False + } + ) + obj.active = False + obj.save(update_fields=['active']) + messages.success(request, pgettext_lazy('subevent', 'The selected dates have been disabled.')) + elif request.POST.get('action') == 'enable': + for obj in self.objects: + obj.log_action( + 'pretix.subevent.changed', user=self.request.user, data={ + 'active': True + } + ) + obj.active = True + obj.save(update_fields=['active']) + messages.success(request, pgettext_lazy('subevent', 'The selected dates have been disabled.')) + elif request.POST.get('action') == 'delete': + return render(request, 'pretixcontrol/subevents/delete_bulk.html', { + 'allowed': self.objects.filter(orderposition__isnull=True), + 'forbidden': self.objects.filter(orderposition__isnull=False), + }) + elif request.POST.get('action') == 'delete_confirm': + for obj in self.objects: + if obj.allow_delete(): + obj.log_action('pretix.subevent.deleted', user=self.request.user) + obj.delete() + else: + obj.log_action( + 'pretix.subevent.changed', user=self.request.user, data={ + 'active': False + } + ) + obj.active = False + obj.save(update_fields=['active']) + messages.success(request, pgettext_lazy('subevent', 'The selected dates have been deleted or disabled.')) + return redirect(self.get_success_url()) + + def get_success_url(self) -> str: + return reverse('control:event.subevents', kwargs={ + 'organizer': self.request.event.organizer.slug, + 'event': self.request.event.slug, + }) + + class SubEventBulkCreate(SubEventEditorMixin, EventPermissionRequiredMixin, CreateView): model = SubEvent template_name = 'pretixcontrol/subevents/bulk.html' diff --git a/src/pretix/static/pretixcontrol/js/ui/main.js b/src/pretix/static/pretixcontrol/js/ui/main.js index e27ee8c2dd..f0b01924d7 100644 --- a/src/pretix/static/pretixcontrol/js/ui/main.js +++ b/src/pretix/static/pretixcontrol/js/ui/main.js @@ -482,7 +482,35 @@ $(function () { $box.find(".propagated-settings-form").removeClass("blurred"); ev.preventDefault(); return true; - }) + }); + + $("input[data-toggle-table]").each(function (ev) { + var $toggle = $(this); + + var update = function () { + var all_true = true; + var all_false = true; + $toggle.closest("table").find("td:first-child input[type=checkbox]").each(function () { + if ($(this).prop("checked")) { + all_false = false; + } else { + all_true = false; + } + }); + if (all_true) { + $toggle.prop("checked", true).prop("indeterminate", false); + } else if (all_false) { + $toggle.prop("checked", false).prop("indeterminate", false); + } else { + $toggle.prop("checked", false).prop("indeterminate", true); + } + }; + + $(this).closest("table").find("td:first-child input[type=checkbox]").change(update); + $(this).change(function (ev) { + $(this).closest("table").find("td:first-child input[type=checkbox]").prop("checked", $(this).prop("checked")); + }); + }); $("#ajaxerr").on("click", ".ajaxerr-close", ajaxErrDialog.hide); moment.locale($("body").attr("data-datetimelocale")); diff --git a/src/tests/control/test_events.py b/src/tests/control/test_events.py index 7047c4cfef..91d439f0fc 100644 --- a/src/tests/control/test_events.py +++ b/src/tests/control/test_events.py @@ -1173,6 +1173,49 @@ class SubEventsTest(SoupTest): assert ses[1].date_from.isoformat() == "2018-04-12T11:29:31+00:00" assert ses[-1].date_from.isoformat() == "2019-03-28T12:29:31+00:00" + def test_delete_bulk(self): + self.subevent2.active = True + self.subevent2.save() + o = Order.objects.create( + code='FOO', event=self.event1, email='dummy@dummy.test', + status=Order.STATUS_PENDING, + datetime=now(), expires=now() + datetime.timedelta(days=10), + total=14, payment_provider='banktransfer', locale='en' + ) + OrderPosition.objects.create( + order=o, + item=self.ticket, + subevent=self.subevent1, + price=Decimal("14"), + ) + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_action', { + 'subevent': [str(self.subevent1.pk), str(self.subevent2.pk)], + 'action': 'delete_confirm' + }, follow=True) + assert doc.select(".alert-success") + assert not self.event1.subevents.filter(pk=self.subevent2.pk).exists() + assert self.event1.subevents.get(pk=self.subevent1.pk).active is False + + def test_disable_bulk(self): + self.subevent2.active = True + self.subevent2.save() + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_action', { + 'subevent': str(self.subevent2.pk), + 'action': 'disable' + }, follow=True) + assert doc.select(".alert-success") + assert self.event1.subevents.get(pk=self.subevent2.pk).active is False + + def test_enable_bulk(self): + self.subevent2.active = False + self.subevent2.save() + doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_action', { + 'subevent': str(self.subevent2.pk), + 'action': 'enable' + }, follow=True) + assert doc.select(".alert-success") + assert self.event1.subevents.get(pk=self.subevent2.pk).active is True + class EventDeletionTest(SoupTest): def setUp(self):