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" %}
+
+{% 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" %}
-
{% 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():