diff --git a/doc/api/resources/checkinlists.rst b/doc/api/resources/checkinlists.rst index 621fcc60e3..d1a2edad5c 100644 --- a/doc/api/resources/checkinlists.rst +++ b/doc/api/resources/checkinlists.rst @@ -33,6 +33,7 @@ auto_checkin_sales_channels list of strings All items on th allow_multiple_entries boolean If ``true``, subsequent scans of a ticket on this list should not show a warning but instead be stored as an additional check-in. allow_entry_after_exit boolean If ``true``, subsequent scans of a ticket on this list are valid if the last scan of the ticket was an exit scan. rules object Custom check-in logic. The contents of this field are currently not considered a stable API and modifications through the API are highly discouraged. +exit_all_at datetime Automatically check out (i.e. perform an exit scan) at this point in time. After this happened, this property will automatically be set exactly one day into the future. Note that this field is considered "internal configuration" and if you pull the list with ``If-Modified-Since``, the daily change in this field will not trigger a response. ===================================== ========================== ======================================================= .. versionchanged:: 1.10 @@ -60,6 +61,10 @@ rules object Custom check-in The ``subevent_match`` and ``exclude`` query parameters have been added. +.. versionchanged:: 3.12 + + The ``exit_all_at`` attribute has been added. + Endpoints --------- @@ -103,6 +108,7 @@ Endpoints "subevent": null, "allow_multiple_entries": false, "allow_entry_after_exit": true, + "exit_all_at": null, "rules": {}, "auto_checkin_sales_channels": [ "pretixpos" @@ -152,6 +158,7 @@ Endpoints "subevent": null, "allow_multiple_entries": false, "allow_entry_after_exit": true, + "exit_all_at": null, "rules": {}, "auto_checkin_sales_channels": [ "pretixpos" diff --git a/src/pretix/api/serializers/checkin.py b/src/pretix/api/serializers/checkin.py index a49f3b860b..4f7b4aff91 100644 --- a/src/pretix/api/serializers/checkin.py +++ b/src/pretix/api/serializers/checkin.py @@ -15,7 +15,7 @@ class CheckinListSerializer(I18nAwareModelSerializer): model = CheckinList fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count', 'include_pending', 'auto_checkin_sales_channels', 'allow_multiple_entries', 'allow_entry_after_exit', - 'rules') + 'rules', 'exit_all_at') def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/src/pretix/base/migrations/0167_checkinlist_exit_all_at.py b/src/pretix/base/migrations/0167_checkinlist_exit_all_at.py new file mode 100644 index 0000000000..820de9d788 --- /dev/null +++ b/src/pretix/base/migrations/0167_checkinlist_exit_all_at.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.10 on 2020-10-20 06:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0166_auto_20201015_2029'), + ] + + operations = [ + migrations.AddField( + model_name='checkinlist', + name='exit_all_at', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/src/pretix/base/models/checkin.py b/src/pretix/base/models/checkin.py index 1ef71be924..c350e703cc 100644 --- a/src/pretix/base/models/checkin.py +++ b/src/pretix/base/models/checkin.py @@ -30,7 +30,10 @@ class CheckinList(LoggedModel): help_text=_('Use this option to turn off warnings if a ticket is scanned a second time.'), default=False ) - + exit_all_at = models.DateTimeField( + verbose_name=_('Automatically check out everyone at'), + null=True, blank=True + ) auto_checkin_sales_channels = MultiStringField( default=[], blank=True, @@ -62,7 +65,7 @@ class CheckinList(LoggedModel): return qs @property - def inside_count(self): + def positions_inside(self): return self.positions.annotate( last_entry=Subquery( Checkin.objects.filter( @@ -87,7 +90,11 @@ class CheckinList(LoggedModel): & Q( Q(last_exit__isnull=True) | Q(last_exit__lt=F('last_entry')) ) - ).count() + ) + + @property + def inside_count(self): + return self.positions_inside.count() @property @scopes_disabled() diff --git a/src/pretix/base/services/checkin.py b/src/pretix/base/services/checkin.py index f90223a5a8..03c3e17491 100644 --- a/src/pretix/base/services/checkin.py +++ b/src/pretix/base/services/checkin.py @@ -7,11 +7,12 @@ from django.dispatch import receiver from django.utils.functional import cached_property from django.utils.timezone import now, override from django.utils.translation import gettext as _ +from django_scopes import scope, scopes_disabled from pretix.base.models import ( Checkin, CheckinList, Device, Order, OrderPosition, QuestionOption, ) -from pretix.base.signals import checkin_created, order_placed +from pretix.base.signals import checkin_created, order_placed, periodic_task from pretix.helpers.jsonlogic import Logic @@ -262,5 +263,23 @@ def order_placed(sender, **kwargs): for cl in cls: if cl.all_products or op.item_id in {i.pk for i in cl.limit_products.all()}: if not cl.subevent_id or cl.subevent_id == op.subevent_id: - ci = Checkin.objects.create(position=op, list=cl, auto_checked_in=True) + ci = Checkin.objects.create(position=op, list=cl, auto_checked_in=True, type=Checkin.TYPE_ENTRY) checkin_created.send(event, checkin=ci) + + +@receiver(periodic_task, dispatch_uid="autocheckin_exit_all") +@scopes_disabled() +def process_exit_all(sender, **kwargs): + qs = CheckinList.objects.filter( + exit_all_at__lte=now(), + exit_all_at__isnull=False + ).select_related('event', 'event__organizer') + for cl in qs: + for p in cl.positions_inside: + with scope(organizer=cl.event.organizer): + ci = Checkin.objects.create( + position=p, list=cl, auto_checked_in=True, type=Checkin.TYPE_EXIT, datetime=cl.exit_all_at + ) + checkin_created.send(cl.event, checkin=ci) + cl.exit_all_at = cl.exit_all_at + timedelta(days=1) + cl.save(update_fields=['exit_all_at']) diff --git a/src/pretix/control/forms/checkin.py b/src/pretix/control/forms/checkin.py index ff7f0eb7bf..4ba39bde26 100644 --- a/src/pretix/control/forms/checkin.py +++ b/src/pretix/control/forms/checkin.py @@ -1,5 +1,8 @@ +from datetime import datetime, timedelta + from django import forms from django.urls import reverse +from django.utils.timezone import get_current_timezone, make_aware, now from django.utils.translation import pgettext_lazy from django_scopes.forms import ( SafeModelChoiceField, SafeModelMultipleChoiceField, @@ -10,6 +13,21 @@ from pretix.base.models.checkin import CheckinList from pretix.control.forms.widgets import Select2 +class NextTimeField(forms.TimeField): + def to_python(self, value): + value = super().to_python(value) + if value is None: + return + tz = get_current_timezone() + result = make_aware(datetime.combine( + now().astimezone(tz).date(), + value, + ), tz) + if result <= now(): + result += timedelta(days=1) + return result + + class CheckinListForm(forms.ModelForm): def __init__(self, **kwargs): self.event = kwargs.pop('event') @@ -55,16 +73,19 @@ class CheckinListForm(forms.ModelForm): 'allow_multiple_entries', 'allow_entry_after_exit', 'rules', + 'exit_all_at', ] widgets = { 'limit_products': forms.CheckboxSelectMultiple(attrs={ 'data-inverse-dependency': '<[name$=all_products]' }), 'auto_checkin_sales_channels': forms.CheckboxSelectMultiple(), + 'exit_all_at': forms.TimeInput(attrs={'class': 'timepickerfield'}), } field_classes = { 'limit_products': SafeModelMultipleChoiceField, 'subevent': SafeModelChoiceField, + 'exit_all_at': NextTimeField, } diff --git a/src/pretix/control/templates/pretixcontrol/checkin/list_edit.html b/src/pretix/control/templates/pretixcontrol/checkin/list_edit.html index 4ef22fd8c2..58ce46cc7f 100644 --- a/src/pretix/control/templates/pretixcontrol/checkin/list_edit.html +++ b/src/pretix/control/templates/pretixcontrol/checkin/list_edit.html @@ -58,6 +58,7 @@ {% bootstrap_field form.allow_multiple_entries layout="control" %} {% bootstrap_field form.allow_entry_after_exit layout="control" %} + {% bootstrap_field form.exit_all_at layout="control" %} {% bootstrap_field form.auto_checkin_sales_channels layout="control" %}