Waiting list: Bulk deletion

This commit is contained in:
Raphael Michel
2021-05-25 22:18:06 +02:00
parent cb42457683
commit 5029213bc5
6 changed files with 179 additions and 61 deletions

View File

@@ -0,0 +1,40 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Delete entries" %}{% endblock %}
{% block content %}
<h1>{% trans "Delete entries" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% if allowed %}
<p>{% blocktrans %}Are you sure you want to delete the following entries?{% endblocktrans %}</p>
<ul>
{% for s in allowed %}
<li>
{{ s }}
<input type="hidden" name="entry" value="{{ s.pk }}">
</li>
{% endfor %}
</ul>
{% endif %}
{% if forbidden %}
<p>{% blocktrans trimmed %}The following entries can't be deleted as they already have a voucher attached.{% endblocktrans %}</p>
<ul>
{% for s in forbidden %}
<li>
{{ s }}
<input type="hidden" name="entry" value="{{ s.pk }}">
</li>
{% endfor %}
</ul>
{% endif %}
<div class="form-group submit-group">
<a href="{% url "control:event.orders.waitinglist" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default btn-cancel">
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-danger btn-save" value="delete_confirm" name="action">
{% trans "Delete" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -126,12 +126,20 @@
{% trans "Download list" %}</a>
</form>
</p>
<form method="post" action="?next={{ request.get_full_path|urlencode }}">
<form action="{% url "control:event.orders.waitinglist.action" organizer=request.event.organizer.slug event=request.event.slug %}?next={{ request.get_full_path|urlencode }}" method="post">
{% csrf_token %}
<input name="subevent" type="hidden" value="{{ request.GET.subevent }}">
<input name="item" type="hidden" value="{{ request.GET.item }}">
<input name="status" type="hidden" value="{{ request.GET.status }}">
<div class="table-responsive">
<table class="table table-condensed table-hover">
<thead>
<tr>
<th>
{% if "can_change_orders" in request.eventpermset %}
<label aria-label="{% trans "select all rows for batch-operation" %}" class="batch-select-label"><input type="checkbox" data-toggle-table/></label>
{% endif %}
</th>
{% if request.event.settings.waiting_list_names_asked %}
<th>{% trans "Name" %}</th>
{% endif %}
@@ -148,10 +156,27 @@
<th>{% trans "Voucher" %}</th>
<th></th>
</tr>
{% if "can_change_orders" in request.eventpermset and page_obj.paginator.num_pages > 1 %}
<tr class="table-select-all warning hidden">
<td>
<input type="checkbox" name="__ALL" id="__all" data-results-total="{{ page_obj.paginator.count }}">
</td>
<td colspan="5">
<label for="__all">
{% trans "Select all results on other pages as well" %}
</label>
</td>
</tr>
{% endif %}
</thead>
<tbody>
{% for e in entries %}
<tr>
<td>
{% if "can_change_orders" in request.eventpermset %}
<label aria-label="{% trans "select row for batch-operation" %}" class="batch-select-label"><input type="checkbox" name="entry" class="batch-select-checkbox" value="{{ s.pk }}"/></label>
{% endif %}
</td>
{% if request.event.settings.waiting_list_names_asked %}
<td>{{ e.name|default:"" }}</td>
{% endif %}
@@ -233,6 +258,13 @@
</tbody>
</table>
</div>
{% if "can_change_orders" in request.eventpermset %}
<div class="batch-select-actions">
<button type="submit" class="btn btn-danger btn-save" name="action" value="delete">
<i class="fa fa-trash"></i>{% trans "Delete selected" %}
</button>
</div>
{% endif %}
</form>
{% include "pretixcontrol/pagination.html" %}
{% endblock %}

View File

@@ -358,6 +358,7 @@ urlpatterns = [
re_path(r'^shredder/download/(?P<file>[^/]+)/$', 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<entry>\d+)/delete$', waitinglist.EntryDelete.as_view(),
name='event.orders.waitinglist.delete'),

View File

@@ -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)

View File

@@ -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),

View File

@@ -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():