# # This file is part of pretix (Community Edition). # # Copyright (C) 2014-2020 Raphael Michel and contributors # Copyright (C) 2020-2021 rami.io GmbH and contributors # # This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General # Public License as published by the Free Software Foundation in version 3 of the License. # # ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are # applicable granting you additional permissions and placing additional restrictions on your usage of this software. # Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive # this file, see . # # This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more # details. # # You should have received a copy of the GNU Affero General Public License along with this program. If not, see # . # # This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of # the Apache License 2.0 can be obtained at . # # This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A # full history of changes and contributors is available at . # # This file contains Apache-licensed contributions copyrighted by: Jakob Schnell, jasonwaiting@live.hk, pajowu # # Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations under the License. import dateutil.parser from django.contrib import messages from django.db import transaction from django.db.models import Exists, Max, OuterRef, Subquery from django.http import Http404, HttpResponseRedirect from django.shortcuts import get_object_or_404, redirect from django.urls import reverse from django.utils.functional import cached_property from django.utils.timezone import is_aware, make_aware, now from django.utils.translation import gettext_lazy as _ from django.views.generic import DeleteView, ListView from pytz import UTC 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.control.forms.checkin import CheckinListForm from pretix.control.forms.filter import CheckInFilterForm from pretix.control.permissions import EventPermissionRequiredMixin 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' def get_queryset(self, filter=True): cqs = Checkin.objects.filter( position_id=OuterRef('pk'), list_id=self.list.pk, type=Checkin.TYPE_ENTRY ).order_by().values('position_id').annotate( m=Max('datetime') ).values('m') cqs_exit = Checkin.objects.filter( position_id=OuterRef('pk'), list_id=self.list.pk, type=Checkin.TYPE_EXIT ).order_by().values('position_id').annotate( m=Max('datetime') ).values('m') qs = OrderPosition.objects.filter( order__event=self.request.event, order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING] if self.list.include_pending else [Order.STATUS_PAID], ).annotate( last_entry=Subquery(cqs), last_exit=Subquery(cqs_exit), auto_checked_in=Exists( Checkin.objects.filter(position_id=OuterRef('pk'), list_id=self.list.pk, auto_checked_in=True) ) ).select_related('item', 'variation', 'order', 'addon_to') if self.list.subevent: qs = qs.filter( subevent=self.list.subevent ) if not self.list.all_products: qs = qs.filter(item__in=self.list.limit_products.values_list('id', flat=True)) if filter and self.filter_form.is_valid(): qs = self.filter_form.filter_qs(qs) return qs @cached_property def filter_form(self): return CheckInFilterForm( data=self.request.GET, event=self.request.event, list=self.list ) 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_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) ctx['checkinlist'] = self.list if self.request.event.has_subevents: ctx['seats'] = ( self.list.subevent.seating_plan_id if self.list.subevent else self.request.event.subevents.filter(seating_plan__isnull=False).exists() ) else: ctx['seats'] = self.request.event.seating_plan_id ctx['filter_form'] = self.filter_form for e in ctx['entries']: if e.last_entry: if isinstance(e.last_entry, str): # Apparently only happens on SQLite e.last_entry_aware = make_aware(dateutil.parser.parse(e.last_entry), UTC) elif not is_aware(e.last_entry): # Apparently only happens on MySQL e.last_entry_aware = make_aware(e.last_entry, UTC) else: # This would be correct, so guess on which database it works… Yes, it's PostgreSQL. e.last_entry_aware = e.last_entry if e.last_exit: if isinstance(e.last_exit, str): # Apparently only happens on SQLite e.last_exit_aware = make_aware(dateutil.parser.parse(e.last_exit), UTC) elif not is_aware(e.last_exit): # Apparently only happens on MySQL e.last_exit_aware = make_aware(e.last_exit, UTC) else: # This would be correct, so guess on which database it works… Yes, it's PostgreSQL. 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') ) 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): Checkin.objects.filter(position=op, list=self.list).delete() op.order.log_action('pretix.event.checkin.reverted', data={ 'position': op.id, 'positionid': op.positionid, 'list': self.list.pk, 'web': True }, user=request.user) op.order.touch() messages.success(request, _('The selected check-ins have been reverted.')) else: for op in positions: if op.order.status == Order.STATUS_PAID or (self.list.include_pending and op.order.status == Order.STATUS_PENDING): t = Checkin.TYPE_EXIT if request.POST.get('checkout') == 'true' else Checkin.TYPE_ENTRY lci = op.checkins.filter(list=self.list).first() if self.list.allow_multiple_entries or t != Checkin.TYPE_ENTRY or (lci and lci.type != Checkin.TYPE_ENTRY): ci = Checkin.objects.create(position=op, list=self.list, datetime=now(), type=t) created = True else: try: ci, created = Checkin.objects.get_or_create(position=op, list=self.list, defaults={ 'datetime': now(), }) except Checkin.MultipleObjectsReturned: ci, created = Checkin.objects.filter(position=op, list=self.list).first(), False op.order.log_action('pretix.event.checkin', data={ 'position': op.id, 'positionid': op.positionid, 'first': created, 'forced': False, 'datetime': now(), 'type': t, 'list': self.list.pk, 'web': True }, user=request.user) checkin_created.send(op.order.event, checkin=ci) messages.success(request, _('The selected tickets have been marked as checked in.')) return redirect(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()) class CheckinListList(EventPermissionRequiredMixin, PaginationMixin, ListView): model = CheckinList context_object_name = 'checkinlists' permission = 'can_view_orders' template_name = 'pretixcontrol/checkin/lists.html' def get_queryset(self): qs = self.request.event.checkin_lists.select_related('subevent').prefetch_related("limit_products") if self.request.GET.get("subevent", "") != "": s = self.request.GET.get("subevent", "") qs = qs.filter(subevent_id=s) return qs def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) clists = list(ctx['checkinlists']) sales_channels = get_all_sales_channels() for cl in clists: if cl.subevent: cl.subevent.event = self.request.event # re-use same event object to make sure settings are cached cl.auto_checkin_sales_channels = [sales_channels[channel] for channel in cl.auto_checkin_sales_channels] ctx['checkinlists'] = clists ctx['can_change_organizer_settings'] = self.request.user.has_organizer_permission( self.request.organizer, 'can_change_organizer_settings', self.request ) return ctx class CheckinListCreate(EventPermissionRequiredMixin, CreateView): model = CheckinList form_class = CheckinListForm template_name = 'pretixcontrol/checkin/list_edit.html' permission = 'can_change_event_settings' context_object_name = 'checkinlist' def dispatch(self, request, *args, **kwargs): r = super().dispatch(request, *args, **kwargs) r['Content-Security-Policy'] = 'script-src \'unsafe-eval\'' return r @cached_property def copy_from(self): if self.request.GET.get("copy_from") and not getattr(self, 'object', None): try: return self.request.event.checkin_lists.get(pk=self.request.GET.get("copy_from")) except CheckinList.DoesNotExist: pass def get_form_kwargs(self): kwargs = super().get_form_kwargs() if self.copy_from: i = modelcopy(self.copy_from) i.pk = None kwargs['instance'] = i else: kwargs['instance'] = CheckinList(event=self.request.event) return kwargs def get_success_url(self) -> str: return reverse('control:event.orders.checkinlists', kwargs={ 'organizer': self.request.event.organizer.slug, 'event': self.request.event.slug, }) @transaction.atomic def form_valid(self, form): form.instance.event = self.request.event messages.success(self.request, _('The new check-in list has been created.')) ret = super().form_valid(form) form.instance.log_action('pretix.event.checkinlist.added', user=self.request.user, data=dict(form.cleaned_data)) return ret def form_invalid(self, form): messages.error(self.request, _('We could not save your changes. See below for details.')) return super().form_invalid(form) class CheckinListUpdate(EventPermissionRequiredMixin, UpdateView): model = CheckinList form_class = CheckinListForm template_name = 'pretixcontrol/checkin/list_edit.html' permission = 'can_change_event_settings' context_object_name = 'checkinlist' def dispatch(self, request, *args, **kwargs): r = super().dispatch(request, *args, **kwargs) r['Content-Security-Policy'] = 'script-src \'unsafe-eval\'' return r def get_object(self, queryset=None) -> CheckinList: try: return self.request.event.checkin_lists.get( id=self.kwargs['list'] ) except CheckinList.DoesNotExist: raise Http404(_("The requested list does not exist.")) @transaction.atomic def form_valid(self, form): messages.success(self.request, _('Your changes have been saved.')) if form.has_changed(): self.object.log_action( 'pretix.event.checkinlist.changed', user=self.request.user, data={ k: form.cleaned_data.get(k) for k in form.changed_data } ) return super().form_valid(form) def get_success_url(self) -> str: return reverse('control:event.orders.checkinlists.edit', kwargs={ 'organizer': self.request.event.organizer.slug, 'event': self.request.event.slug, 'list': self.object.pk }) def form_invalid(self, form): messages.error(self.request, _('We could not save your changes. See below for details.')) return super().form_invalid(form) class CheckinListDelete(EventPermissionRequiredMixin, DeleteView): model = CheckinList template_name = 'pretixcontrol/checkin/list_delete.html' permission = 'can_change_event_settings' context_object_name = 'checkinlist' def get_object(self, queryset=None) -> CheckinList: try: return self.request.event.checkin_lists.get( id=self.kwargs['list'] ) except CheckinList.DoesNotExist: raise Http404(_("The requested list does not exist.")) @transaction.atomic def delete(self, request, *args, **kwargs): self.object = self.get_object() success_url = self.get_success_url() self.object.checkins.all().delete() self.object.log_action(action='pretix.event.checkinlists.deleted', user=request.user) self.object.delete() messages.success(self.request, _('The selected list has been deleted.')) return HttpResponseRedirect(success_url) def get_success_url(self) -> str: return reverse('control:event.orders.checkinlists', kwargs={ 'organizer': self.request.event.organizer.slug, 'event': self.request.event.slug, })