Allow to bulk-select many tickets to check in or out (#2678)

* Allow to bulk-select many tickets to check in or out

* Update tests

* Add permission test

* Update src/pretix/control/templates/pretixcontrol/checkin/index.html

Co-authored-by: Richard Schreiber <wiffbi@gmail.com>

* Update src/pretix/static/pretixbase/js/asynctask.js

Co-authored-by: Richard Schreiber <wiffbi@gmail.com>

* Remove console.warn

* Simplify stuff

* minor refactor

* fix missing checked-out success message

Co-authored-by: Richard Schreiber <wiffbi@gmail.com>
Co-authored-by: Richard Schreiber <schreiber@rami.io>
This commit is contained in:
Raphael Michel
2022-06-10 12:14:44 +02:00
committed by GitHub
parent f3a84c1d6e
commit 86085d9368
8 changed files with 183 additions and 34 deletions

View File

@@ -71,13 +71,20 @@
</p>
</div>
{% else %}
<form method="post" action="">
<form method="post" action="{% url "control:event.orders.checkinlists.bulk_action" event=request.event.slug organizer=request.event.organizer.slug list=checkinlist.pk %}" data-asynctask>
<div class="hidden">
{{ filter_form.as_p }}
<input name="returnquery" type="hidden" value="{{ request.META.QUERY_STRING }}">
</div>
{% csrf_token %}
<div class="table-responsive">
<table class="table table-condensed table-hover">
<thead>
<tr>
<th></th>
<th>
<label aria-label="{% trans "select all rows for batch-operation" %}"
class="batch-select-label"><input type="checkbox" data-toggle-table/></label>
</th>
<th>{% trans "Order code" %} <a href="?{% url_replace request 'ordering' '-code'%}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'code'%}"><i class="fa fa-caret-up"></i></a></th>
<th>{% trans "Item" %} <a href="?{% url_replace request 'ordering' '-item'%}"><i class="fa fa-caret-down"></i></a>
@@ -100,6 +107,19 @@
<th>{% trans "Timestamp" %} <a href="?{% url_replace request 'ordering' '-timestamp'%}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'timestamp'%}"><i class="fa fa-caret-up"></i></a></th>
</tr>
{% if 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="8">
<label for="__all">
{% trans "Select all results on other pages as well" %}
</label>
</td>
</tr>
{% endif %}
</thead>
<tbody>
{% for e in entries %}
@@ -180,13 +200,16 @@
</div>
{% if "can_change_orders" in request.eventpermset %}
<button type="submit" class="btn btn-primary btn-save">
<span class="fa fa-sign-in" aria-hidden="true"></span>
{% trans "Check-In selected attendees" %}
</button>
<button type="submit" class="btn btn-default btn-save" name="checkout" value="true">
<span class="fa fa-sign-out" aria-hidden="true"></span>
{% trans "Check-Out selected attendees" %}
</button>
<button type="submit" class="btn btn-default btn-save" name="revert" value="true">
{% trans "Revert selected check-ins" %}
<button type="submit" class="btn btn-danger btn-save" name="revert" value="true">
<span class="fa fa-trash" aria-hidden="true"></span>
{% trans "Delete all check-ins of selected attendees" %}
</button>
{% endif %}
</form>

View File

@@ -392,6 +392,7 @@ urlpatterns = [
re_path(r'^checkinlists/add$', checkin.CheckinListCreate.as_view(), name='event.orders.checkinlists.add'),
re_path(r'^checkinlists/select2$', typeahead.checkinlist_select2, name='event.orders.checkinlists.select2'),
re_path(r'^checkinlists/(?P<list>\d+)/$', checkin.CheckInListShow.as_view(), name='event.orders.checkinlists.show'),
re_path(r'^checkinlists/(?P<list>\d+)/bulk_action$', checkin.CheckInListBulkActionView.as_view(), name='event.orders.checkinlists.bulk_action'),
re_path(r'^checkinlists/(?P<list>\d+)/change$', checkin.CheckinListUpdate.as_view(),
name='event.orders.checkinlists.edit'),
re_path(r'^checkinlists/(?P<list>\d+)/delete$', checkin.CheckinListDelete.as_view(),

View File

@@ -37,7 +37,7 @@ from django.contrib import messages
from django.db import transaction
from django.db.models import Exists, Max, OuterRef, Prefetch, Subquery
from django.http import Http404, HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.timezone import is_aware, make_aware, now
@@ -49,6 +49,7 @@ from pretix.base.channels import get_all_sales_channels
from pretix.base.models import Checkin, Order, OrderPosition
from pretix.base.models.checkin import CheckinList
from pretix.base.signals import checkin_created
from pretix.base.views.tasks import AsyncPostView
from pretix.control.forms.checkin import CheckinListForm
from pretix.control.forms.filter import (
CheckinFilterForm, CheckinListAttendeeFilterForm,
@@ -58,11 +59,13 @@ from pretix.control.views import CreateView, PaginationMixin, UpdateView
from pretix.helpers.models import modelcopy
class CheckInListShow(EventPermissionRequiredMixin, PaginationMixin, ListView):
model = Checkin
context_object_name = 'entries'
template_name = 'pretixcontrol/checkin/index.html'
permission = 'can_view_orders'
class CheckInListQueryMixin:
@cached_property
def request_data(self):
if self.request.method == "POST":
return self.request.POST
return self.request.GET
def get_queryset(self, filter=True):
cqs = Checkin.objects.filter(
@@ -105,16 +108,28 @@ class CheckInListShow(EventPermissionRequiredMixin, PaginationMixin, ListView):
if filter and self.filter_form.is_valid():
qs = self.filter_form.filter_qs(qs)
if 'checkin' in self.request_data and '__ALL' not in self.request_data:
qs = qs.filter(
id__in=self.request_data.getlist('checkin')
)
return qs
@cached_property
def filter_form(self):
return CheckinListAttendeeFilterForm(
data=self.request.GET,
data=self.request_data,
event=self.request.event,
list=self.list
)
class CheckInListShow(EventPermissionRequiredMixin, PaginationMixin, CheckInListQueryMixin, ListView):
model = Checkin
context_object_name = 'entries'
template_name = 'pretixcontrol/checkin/index.html'
permission = 'can_view_orders'
def dispatch(self, request, *args, **kwargs):
self.list = get_object_or_404(self.request.event.checkin_lists.all(), pk=kwargs.get("list"))
return super().dispatch(request, *args, **kwargs)
@@ -153,18 +168,23 @@ class CheckInListShow(EventPermissionRequiredMixin, PaginationMixin, ListView):
e.last_exit_aware = e.last_exit
return ctx
def post(self, request, *args, **kwargs):
if "can_change_orders" not in request.eventpermset:
messages.error(request, _('You do not have permission to perform this action.'))
return redirect(reverse('control:event.orders.checkins', kwargs={
'event': self.request.event.slug,
'organizer': self.request.event.organizer.slug
}) + '?' + request.GET.urlencode())
positions = self.get_queryset(filter=False).filter(
pk__in=request.POST.getlist('checkin')
)
class CheckInListBulkActionView(CheckInListQueryMixin, EventPermissionRequiredMixin, AsyncPostView):
template_name = 'pretixcontrol/organizers/device_bulk_edit.html'
permission = 'can_change_orders'
context_object_name = 'device'
def dispatch(self, request, *args, **kwargs):
self.list = get_object_or_404(self.request.event.checkin_lists.all(), pk=kwargs.get("list"))
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
return super().get_queryset().prefetch_related(None).order_by()
@transaction.atomic()
def async_post(self, request, *args, **kwargs):
self.list = get_object_or_404(request.event.checkin_lists.all(), pk=kwargs.get("list"))
positions = self.get_queryset()
if request.POST.get('revert') == 'true':
for op in positions:
if op.order.status == Order.STATUS_PAID or (self.list.include_pending and op.order.status == Order.STATUS_PENDING):
@@ -177,7 +197,7 @@ class CheckInListShow(EventPermissionRequiredMixin, PaginationMixin, ListView):
}, user=request.user)
op.order.touch()
messages.success(request, _('The selected check-ins have been reverted.'))
return 'reverted', request.POST.get('returnquery')
else:
for op in positions:
if op.order.status == Order.STATUS_PAID or (self.list.include_pending and op.order.status == Order.STATUS_PENDING):
@@ -206,14 +226,22 @@ class CheckInListShow(EventPermissionRequiredMixin, PaginationMixin, ListView):
'web': True
}, user=request.user)
checkin_created.send(op.order.event, checkin=ci)
return 'checked-out' if t == Checkin.TYPE_EXIT else 'checked-in', request.POST.get('returnquery')
messages.success(request, _('The selected tickets have been marked as checked in.'))
def get_success_message(self, value):
if value[0] == 'reverted':
return _('The selected check-ins have been reverted.')
elif value[0] == 'checked-out':
return _('The selected tickets have been marked as checked out.')
else:
return _('The selected tickets have been marked as checked in.')
return redirect(reverse('control:event.orders.checkinlists.show', kwargs={
def get_success_url(self, value):
return reverse('control:event.orders.checkinlists.show', kwargs={
'event': self.request.event.slug,
'organizer': self.request.event.organizer.slug,
'list': self.list.pk
}) + '?' + request.GET.urlencode())
}) + ('?' + value[1] if value[1] else '')
class CheckinListList(EventPermissionRequiredMixin, PaginationMixin, ListView):

View File

@@ -830,7 +830,7 @@ class DeviceQueryMixin:
@cached_property
def filter_form(self):
return DeviceFilterForm(data=self.request.GET, request=self.request)
return DeviceFilterForm(data=self.request_data, request=self.request)
def get_queryset(self):
qs = self.request.organizer.devices.prefetch_related(