diff --git a/src/pretix/control/templates/pretixcontrol/waitinglist/delete_bulk.html b/src/pretix/control/templates/pretixcontrol/waitinglist/delete_bulk.html new file mode 100644 index 0000000000..2e6edcee79 --- /dev/null +++ b/src/pretix/control/templates/pretixcontrol/waitinglist/delete_bulk.html @@ -0,0 +1,40 @@ +{% extends "pretixcontrol/event/base.html" %} +{% load i18n %} +{% load bootstrap3 %} +{% block title %}{% trans "Delete entries" %}{% endblock %} +{% block content %} +

{% trans "Delete entries" %}

+
+ {% csrf_token %} + {% if allowed %} +

{% blocktrans %}Are you sure you want to delete the following entries?{% endblocktrans %}

+ + {% endif %} + {% if forbidden %} +

{% blocktrans trimmed %}The following entries can't be deleted as they already have a voucher attached.{% endblocktrans %}

+ + {% endif %} +
+ + {% trans "Cancel" %} + + +
+
+{% endblock %} diff --git a/src/pretix/control/templates/pretixcontrol/waitinglist/index.html b/src/pretix/control/templates/pretixcontrol/waitinglist/index.html index 615cf33456..b94d339c8e 100644 --- a/src/pretix/control/templates/pretixcontrol/waitinglist/index.html +++ b/src/pretix/control/templates/pretixcontrol/waitinglist/index.html @@ -126,12 +126,20 @@ {% trans "Download list" %}

-
+ {% csrf_token %} + + +
+ {% if request.event.settings.waiting_list_names_asked %} {% endif %} @@ -148,10 +156,27 @@ + {% if "can_change_orders" in request.eventpermset and page_obj.paginator.num_pages > 1 %} + + + + + {% endif %} {% for e in entries %} + {% if request.event.settings.waiting_list_names_asked %} {% endif %} @@ -233,6 +258,13 @@
+ {% if "can_change_orders" in request.eventpermset %} + + {% endif %} + {% trans "Name" %}{% trans "Voucher" %}
+ {% if "can_change_orders" in request.eventpermset %} + + {% endif %} + {{ e.name|default:"" }}
+ {% if "can_change_orders" in request.eventpermset %} +
+ +
+ {% endif %}
{% include "pretixcontrol/pagination.html" %} {% endblock %} diff --git a/src/pretix/control/urls.py b/src/pretix/control/urls.py index f4e4769fdc..58c8563ba2 100644 --- a/src/pretix/control/urls.py +++ b/src/pretix/control/urls.py @@ -358,6 +358,7 @@ urlpatterns = [ re_path(r'^shredder/download/(?P[^/]+)/$', shredder.ShredDownloadView.as_view(), name='event.shredder.download'), re_path(r'^shredder/shred', shredder.ShredDoView.as_view(), name='event.shredder.shred'), re_path(r'^waitinglist/$', waitinglist.WaitingListView.as_view(), name='event.orders.waitinglist'), + re_path(r'^waitinglist/action$', waitinglist.WaitingListActionView.as_view(), name='event.orders.waitinglist.action'), re_path(r'^waitinglist/auto_assign$', waitinglist.AutoAssign.as_view(), name='event.orders.waitinglist.auto'), re_path(r'^waitinglist/(?P\d+)/delete$', waitinglist.EntryDelete.as_view(), name='event.orders.waitinglist.delete'), diff --git a/src/pretix/control/views/waitinglist.py b/src/pretix/control/views/waitinglist.py index 666a2a6a90..97e1a89e88 100644 --- a/src/pretix/control/views/waitinglist.py +++ b/src/pretix/control/views/waitinglist.py @@ -40,8 +40,9 @@ from django.db import transaction from django.db.models import F, Max, Min, Q, Sum from django.db.models.functions import Coalesce from django.http import Http404, HttpResponse, HttpResponseRedirect -from django.shortcuts import redirect +from django.shortcuts import redirect, render from django.urls import reverse +from django.utils.functional import cached_property from django.utils.http import is_safe_url from django.utils.timezone import now from django.utils.translation import gettext_lazy as _, pgettext @@ -79,16 +80,86 @@ class AutoAssign(EventPermissionRequiredMixin, AsyncAction, View): self.request.POST.get('subevent')) -class WaitingListView(EventPermissionRequiredMixin, PaginationMixin, ListView): +class WaitingListQuerySetMixin: + + @cached_property + def request_data(self): + if self.request.method == "POST": + return self.request.POST + return self.request.GET + + def get_queryset(self): + qs = WaitingListEntry.objects.filter( + event=self.request.event + ).select_related('item', 'variation', 'voucher').prefetch_related( + 'item__quotas', 'variation__quotas' + ) + + s = self.request_data.get("status", "") + if s == 's': + qs = qs.filter(voucher__isnull=False) + elif s == 'a': + pass + elif s == 'r': + qs = qs.filter( + voucher__isnull=False, + voucher__redeemed__gte=F('voucher__max_usages'), + ) + elif s == 'v': + qs = qs.filter( + voucher__isnull=False, + voucher__redeemed__lt=F('voucher__max_usages'), + ).filter(Q(voucher__valid_until__isnull=True) | Q(voucher__valid_until__gt=now())) + elif s == 'e': + qs = qs.filter( + voucher__isnull=False, + voucher__redeemed__lt=F('voucher__max_usages'), + voucher__valid_until__isnull=False, + voucher__valid_until__lte=now() + ) + else: + qs = qs.filter(voucher__isnull=True) + + if self.request_data.get("item", "") != "": + i = self.request_data.get("item", "") + qs = qs.filter(item_id=i) + + if self.request_data.get("subevent", "") != "": + s = self.request_data.get("subevent", "") + qs = qs.filter(subevent_id=s) + + if 'entry' in self.request_data and '__ALL' not in self.request_data: + qs = qs.filter( + id__in=self.request_data.getlist('entry') + ) + + return qs + + +class WaitingListActionView(EventPermissionRequiredMixin, WaitingListQuerySetMixin, View): model = WaitingListEntry - context_object_name = 'entries' - template_name = 'pretixcontrol/waitinglist/index.html' - permission = 'can_view_orders' + permission = 'can_change_orders' + + def _redirect_back(self): + if "next" in self.request.GET and is_safe_url(self.request.GET.get("next"), allowed_hosts=None): + return redirect(self.request.GET.get("next")) + return redirect(reverse('control:event.orders.waitinglist', kwargs={ + 'event': self.request.event.slug, + 'organizer': self.request.event.organizer.slug + })) def post(self, request, *args, **kwargs): - if not request.user.has_event_permission(request.organizer, request.event, 'can_change_orders', - request=request): - messages.error(request, _('You do not have permission to do this')) + if request.POST.get('action') == 'delete': + return render(request, 'pretixcontrol/waitinglist/delete_bulk.html', { + 'allowed': self.get_queryset().filter(voucher__isnull=True), + 'forbidden': self.get_queryset().filter(voucher__isnull=False), + }) + elif request.POST.get('action') == 'delete_confirm': + for obj in self.get_queryset(): + if not obj.voucher_id: + obj.log_action('pretix.event.orders.waitinglist.deleted', user=self.request.user) + obj.delete() + messages.success(request, _('The selected entries have been deleted.')) return self._redirect_back() if 'assign' in request.POST: @@ -135,55 +206,12 @@ class WaitingListView(EventPermissionRequiredMixin, PaginationMixin, ListView): return self._redirect_back() return self._redirect_back() - def _redirect_back(self): - if "next" in self.request.GET and is_safe_url(self.request.GET.get("next"), allowed_hosts=None): - return redirect(self.request.GET.get("next")) - return redirect(reverse('control:event.orders.waitinglist', kwargs={ - 'event': self.request.event.slug, - 'organizer': self.request.event.organizer.slug - })) - def get_queryset(self): - qs = WaitingListEntry.objects.filter( - event=self.request.event - ).select_related('item', 'variation', 'voucher').prefetch_related( - 'item__quotas', 'variation__quotas' - ) - - s = self.request.GET.get("status", "") - if s == 's': - qs = qs.filter(voucher__isnull=False) - elif s == 'a': - pass - elif s == 'r': - qs = qs.filter( - voucher__isnull=False, - voucher__redeemed__gte=F('voucher__max_usages'), - ) - elif s == 'v': - qs = qs.filter( - voucher__isnull=False, - voucher__redeemed__lt=F('voucher__max_usages'), - ).filter(Q(voucher__valid_until__isnull=True) | Q(voucher__valid_until__gt=now())) - elif s == 'e': - qs = qs.filter( - voucher__isnull=False, - voucher__redeemed__lt=F('voucher__max_usages'), - voucher__valid_until__isnull=False, - voucher__valid_until__lte=now() - ) - else: - qs = qs.filter(voucher__isnull=True) - - if self.request.GET.get("item", "") != "": - i = self.request.GET.get("item", "") - qs = qs.filter(item_id=i) - - if self.request.GET.get("subevent", "") != "": - s = self.request.GET.get("subevent", "") - qs = qs.filter(subevent_id=s) - - return qs +class WaitingListView(EventPermissionRequiredMixin, WaitingListQuerySetMixin, PaginationMixin, ListView): + model = WaitingListEntry + context_object_name = 'entries' + template_name = 'pretixcontrol/waitinglist/index.html' + permission = 'can_view_orders' def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) diff --git a/src/tests/control/test_permissions.py b/src/tests/control/test_permissions.py index c2d0008579..cf7ac28d94 100644 --- a/src/tests/control/test_permissions.py +++ b/src/tests/control/test_permissions.py @@ -163,6 +163,7 @@ event_urls = [ "checkinlists/1/delete", "waitinglist/", "waitinglist/auto_assign", + "waitinglist/action", "invoice/1", ] @@ -347,6 +348,7 @@ event_permission_urls = [ ("can_change_vouchers", "vouchers/1234/delete", 404), ("can_view_orders", "waitinglist/", 200), ("can_change_orders", "waitinglist/auto_assign", 405), + ("can_change_orders", "waitinglist/action", 405), ("can_view_orders", "checkinlists/", 200), ("can_view_orders", "checkinlists/1/", 404), ("can_change_event_settings", "checkinlists/add", 200), diff --git a/src/tests/control/test_waitinglist.py b/src/tests/control/test_waitinglist.py index af14b58481..13221bbdb9 100644 --- a/src/tests/control/test_waitinglist.py +++ b/src/tests/control/test_waitinglist.py @@ -133,7 +133,7 @@ def test_assign_single(client, env): with scopes_disabled(): wle = WaitingListEntry.objects.filter(voucher__isnull=True).last() - client.post('/control/event/dummy/dummy/waitinglist/', { + client.post('/control/event/dummy/dummy/waitinglist/action', { 'assign': wle.pk }) wle.refresh_from_db() @@ -147,17 +147,17 @@ def test_priority_single(client, env): wle = WaitingListEntry.objects.filter(voucher__isnull=True).last() assert wle.priority == 0 - client.post('/control/event/dummy/dummy/waitinglist/', { + client.post('/control/event/dummy/dummy/waitinglist/action', { 'move_top': wle.pk }) wle.refresh_from_db() assert wle.priority == 1 - client.post('/control/event/dummy/dummy/waitinglist/', { + client.post('/control/event/dummy/dummy/waitinglist/action', { 'move_top': wle.pk }) wle.refresh_from_db() assert wle.priority == 2 - client.post('/control/event/dummy/dummy/waitinglist/', { + client.post('/control/event/dummy/dummy/waitinglist/action', { 'move_end': wle.pk }) wle.refresh_from_db() @@ -176,6 +176,21 @@ def test_delete_single(client, env): WaitingListEntry.objects.get(id=wle.id) +@pytest.mark.django_db +def test_delete_bulk(client, env): + client.login(email='dummy@dummy.dummy', password='dummy') + with scopes_disabled(): + wle = WaitingListEntry.objects.first() + + client.post('/control/event/dummy/dummy/waitinglist/action', data={ + 'entry': wle.pk, + 'action': 'delete_confirm', + }) + with pytest.raises(WaitingListEntry.DoesNotExist): + with scopes_disabled(): + WaitingListEntry.objects.get(id=wle.id) + + @pytest.mark.django_db def test_dashboard(client, env): with scopes_disabled():