diff --git a/doc/api/resources/checkinlists.rst b/doc/api/resources/checkinlists.rst index b03abb1800..34e49aa272 100644 --- a/doc/api/resources/checkinlists.rst +++ b/doc/api/resources/checkinlists.rst @@ -1,3 +1,5 @@ +.. spelling:: checkin + Check-in lists ============== @@ -27,6 +29,7 @@ subevent integer ID of the date position_count integer Number of tickets that match this list (read-only). checkin_count integer Number of check-ins performed on this list (read-only). include_pending boolean If ``true``, the check-in list also contains tickets from orders in pending state. +auto_checkin_sales_channels list of strings All items on the check-in list will be automatically marked as checked-in when purchased through any of the listed sales channels. ===================================== ========================== ======================================================= .. versionchanged:: 1.10 @@ -41,6 +44,10 @@ include_pending boolean If ``true``, th The ``include_pending`` field has been added. +.. versionchanged:: 3.2 + + The ``auto_checkin_sales_channels`` field has been added. + Endpoints --------- @@ -81,7 +88,10 @@ Endpoints "all_products": true, "limit_products": [], "include_pending": false, - "subevent": null + "subevent": null, + "auto_checkin_sales_channels": [ + "pretixpos" + ] } ] } @@ -122,7 +132,10 @@ Endpoints "all_products": true, "limit_products": [], "include_pending": false, - "subevent": null + "subevent": null, + "auto_checkin_sales_channels": [ + "pretixpos" + ] } :param organizer: The ``slug`` field of the organizer to fetch @@ -215,7 +228,10 @@ Endpoints "name": "VIP entry", "all_products": false, "limit_products": [1, 2], - "subevent": null + "subevent": null, + "auto_checkin_sales_channels": [ + "pretixpos" + ] } **Example response**: @@ -234,7 +250,10 @@ Endpoints "all_products": false, "limit_products": [1, 2], "include_pending": false, - "subevent": null + "subevent": null, + "auto_checkin_sales_channels": [ + "pretixpos" + ] } :param organizer: The ``slug`` field of the organizer of the event/item to create a list for @@ -283,7 +302,10 @@ Endpoints "all_products": false, "limit_products": [1, 2], "include_pending": false, - "subevent": null + "subevent": null, + "auto_checkin_sales_channels": [ + "pretixpos" + ] } :param organizer: The ``slug`` field of the organizer to modify @@ -342,6 +364,11 @@ Order position endpoints ``ignore_status`` filter. The ``attendee_name`` field is now "smart" (see below) and the redemption endpoint returns ``400`` instead of ``404`` on tickets which are known but not paid. +.. versionchanged:: 3.2 + + The ``checkins`` dict now also contains a ``auto_checked_in`` value to indicate if the check-in has been performed + automatically by the system. + .. http:get:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/(list)/positions/ Returns a list of all order positions within a given event. The result is the same as @@ -400,7 +427,8 @@ Order position endpoints "checkins": [ { "list": 1, - "datetime": "2017-12-25T12:45:23Z" + "datetime": "2017-12-25T12:45:23Z", + "auto_checked_in": true } ], "answers": [ @@ -510,7 +538,8 @@ Order position endpoints "checkins": [ { "list": 1, - "datetime": "2017-12-25T12:45:23Z" + "datetime": "2017-12-25T12:45:23Z", + "auto_checked_in": true } ], "answers": [ diff --git a/doc/api/resources/orders.rst b/doc/api/resources/orders.rst index 44e87be9a8..0df580de03 100644 --- a/doc/api/resources/orders.rst +++ b/doc/api/resources/orders.rst @@ -175,7 +175,8 @@ subevent integer ID of the date pseudonymization_id string A random ID, e.g. for use in lead scanning apps checkins list of objects List of check-ins with this ticket ├ list integer Internal ID of the check-in list -└ datetime datetime Time of check-in +├ datetime datetime Time of check-in +└ auto_checked_in boolean Indicates if this check-in been performed automatically by the system downloads list of objects List of ticket download options ├ output string Ticket output provider (e.g. ``pdf``, ``passbook``) └ url string Download URL @@ -214,6 +215,10 @@ pdf_data object Data object req The attribute ``seat`` has been added. +.. versionchanged:: 3.2 + + The value ``auto_checked_in`` has been added to the ``checkins``-attribute. + .. _order-payment-resource: Order payment resource @@ -365,7 +370,8 @@ List of all orders "checkins": [ { "list": 44, - "datetime": "2017-12-25T12:45:23Z" + "datetime": "2017-12-25T12:45:23Z", + "auto_checked_in": false } ], "answers": [ @@ -512,7 +518,8 @@ Fetching individual orders "checkins": [ { "list": 44, - "datetime": "2017-12-25T12:45:23Z" + "datetime": "2017-12-25T12:45:23Z", + "auto_checked_in": false } ], "answers": [ @@ -1286,6 +1293,11 @@ List of all order positions The order positions endpoint has been extended by the filter queries ``voucher``, ``voucher__code`` and ``pseudonymization_id``. +.. versionchanged:: 3.2 + + The value ``auto_checked_in`` has been added to the ``checkins``-attribute. + + .. note:: Individually canceled order positions are currently not visible via the API at all. .. http:get:: /api/v1/organizers/(organizer)/events/(event)/orderpositions/ @@ -1337,7 +1349,8 @@ List of all order positions "checkins": [ { "list": 44, - "datetime": "2017-12-25T12:45:23Z" + "datetime": "2017-12-25T12:45:23Z", + "auto_checked_in": false } ], "answers": [ @@ -1438,7 +1451,8 @@ Fetching individual positions "checkins": [ { "list": 44, - "datetime": "2017-12-25T12:45:23Z" + "datetime": "2017-12-25T12:45:23Z", + "auto_checked_in": false } ], "answers": [ diff --git a/src/pretix/api/serializers/checkin.py b/src/pretix/api/serializers/checkin.py index 2461d53173..8cfd43392b 100644 --- a/src/pretix/api/serializers/checkin.py +++ b/src/pretix/api/serializers/checkin.py @@ -3,6 +3,7 @@ from rest_framework import serializers from rest_framework.exceptions import ValidationError from pretix.api.serializers.i18n import I18nAwareModelSerializer +from pretix.base.channels import get_all_sales_channels from pretix.base.models import CheckinList @@ -13,7 +14,7 @@ class CheckinListSerializer(I18nAwareModelSerializer): class Meta: model = CheckinList fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count', - 'include_pending') + 'include_pending', 'auto_checkin_sales_channels') def validate(self, data): data = super().validate(data) @@ -35,4 +36,8 @@ class CheckinListSerializer(I18nAwareModelSerializer): if full_data.get('subevent'): raise ValidationError(_('The subevent does not belong to this event.')) + for channel in full_data.get('auto_checkin_sales_channels') or []: + if channel not in get_all_sales_channels(): + raise ValidationError(_('Unknown sales channel.')) + return data diff --git a/src/pretix/api/serializers/order.py b/src/pretix/api/serializers/order.py index e664356b54..a21fea7af7 100644 --- a/src/pretix/api/serializers/order.py +++ b/src/pretix/api/serializers/order.py @@ -114,7 +114,7 @@ class AnswerSerializer(I18nAwareModelSerializer): class CheckinSerializer(I18nAwareModelSerializer): class Meta: model = Checkin - fields = ('datetime', 'list') + fields = ('datetime', 'list', 'auto_checked_in') class OrderDownloadsField(serializers.Field): diff --git a/src/pretix/base/__init__.py b/src/pretix/base/__init__.py index bbfcd9b1d1..ed61669342 100644 --- a/src/pretix/base/__init__.py +++ b/src/pretix/base/__init__.py @@ -13,7 +13,7 @@ class PretixBaseConfig(AppConfig): from . import invoice # NOQA from . import notifications # NOQA from . import email # NOQA - from .services import auth, export, mail, tickets, cart, orders, invoices, cleanup, update_check, quotas, notifications # NOQA + from .services import auth, checkin, export, mail, tickets, cart, orders, invoices, cleanup, update_check, quotas, notifications # NOQA try: from .celery_app import app as celery_app # NOQA diff --git a/src/pretix/base/migrations/0136_auto_20190918_1742.py b/src/pretix/base/migrations/0136_auto_20190918_1742.py new file mode 100644 index 0000000000..54326c377b --- /dev/null +++ b/src/pretix/base/migrations/0136_auto_20190918_1742.py @@ -0,0 +1,25 @@ +# Generated by Django 2.2 on 2019-09-18 17:42 + +from django.db import migrations, models + +import pretix.base.models.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0135_auto_20191007_0803'), + ] + + operations = [ + migrations.AddField( + model_name='checkin', + name='auto_checked_in', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='checkinlist', + name='auto_checkin_sales_channels', + field=pretix.base.models.fields.MultiStringField(default=[]), + ) + ] diff --git a/src/pretix/base/models/checkin.py b/src/pretix/base/models/checkin.py index 25f7fd1745..b800ed25c7 100644 --- a/src/pretix/base/models/checkin.py +++ b/src/pretix/base/models/checkin.py @@ -5,6 +5,7 @@ from django.utils.translation import pgettext_lazy, ugettext_lazy as _ from django_scopes import ScopedManager from pretix.base.models import LoggedModel +from pretix.base.models.fields import MultiStringField class CheckinList(LoggedModel): @@ -20,6 +21,15 @@ class CheckinList(LoggedModel): 'order have not been paid. This only works with pretixdesk ' '0.3.0 or newer or pretixdroid 1.9 or newer.')) + auto_checkin_sales_channels = MultiStringField( + default=[], + blank=True, + verbose_name=_('Sales channels to automatically check in'), + help_text=_('All items on this check-in list will be automatically marked as checked-in when purchased through ' + 'any of the selected sales channels. This option can be useful when tickets sold at the box office ' + 'are not checked again before entry and should be considered validated directly upon purchase.') + ) + objects = ScopedManager(organizer='event__organizer') class Meta: @@ -87,6 +97,7 @@ class Checkin(models.Model): list = models.ForeignKey( 'pretixbase.CheckinList', related_name='checkins', on_delete=models.PROTECT, ) + auto_checked_in = models.BooleanField(default=False) objects = ScopedManager(organizer='position__order__event__organizer') diff --git a/src/pretix/base/services/checkin.py b/src/pretix/base/services/checkin.py index f7d0cf08fb..875a5ff5c0 100644 --- a/src/pretix/base/services/checkin.py +++ b/src/pretix/base/services/checkin.py @@ -1,11 +1,13 @@ from django.db import transaction from django.db.models import Prefetch +from django.dispatch import receiver from django.utils.timezone import now from django.utils.translation import ugettext as _ from pretix.base.models import ( Checkin, CheckinList, Order, OrderPosition, Question, QuestionOption, ) +from pretix.base.signals import order_placed class CheckInError(Exception): @@ -155,3 +157,18 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict, 'datetime': dt, 'list': clist.pk }, user=user, auth=auth) + + +@receiver(order_placed, dispatch_uid="autocheckin_order_placed") +def order_placed(sender, **kwargs): + order = kwargs['order'] + event = sender + + cls = list(event.checkin_lists.filter(auto_checkin_sales_channels__contains=order.sales_channel).prefetch_related( + 'limit_products')) + if not cls: + return + for op in order.positions.all(): + for cl in cls: + if cl.all_products or op.item_id in {i.pk for i in cl.limit_products.all()}: + Checkin.objects.create(position=op, list=cl, auto_checked_in=True) diff --git a/src/pretix/control/forms/checkin.py b/src/pretix/control/forms/checkin.py index 5740308639..7699c49c23 100644 --- a/src/pretix/control/forms/checkin.py +++ b/src/pretix/control/forms/checkin.py @@ -5,6 +5,7 @@ from django_scopes.forms import ( SafeModelChoiceField, SafeModelMultipleChoiceField, ) +from pretix.base.channels import get_all_sales_channels from pretix.base.models.checkin import CheckinList from pretix.control.forms.widgets import Select2 @@ -15,6 +16,16 @@ class CheckinListForm(forms.ModelForm): kwargs.pop('locales', None) super().__init__(**kwargs) self.fields['limit_products'].queryset = self.event.items.all() + self.fields['auto_checkin_sales_channels'] = forms.MultipleChoiceField( + label=self.fields['auto_checkin_sales_channels'].label, + help_text=self.fields['auto_checkin_sales_channels'].help_text, + required=self.fields['auto_checkin_sales_channels'].required, + choices=( + (c.identifier, c.verbose_name) for c in get_all_sales_channels().values() + ), + widget=forms.CheckboxSelectMultiple + ) + if self.event.has_subevents: self.fields['subevent'].queryset = self.event.subevents.all() self.fields['subevent'].widget = Select2( @@ -40,12 +51,14 @@ class CheckinListForm(forms.ModelForm): 'all_products', 'limit_products', 'subevent', - 'include_pending' + 'include_pending', + 'auto_checkin_sales_channels' ] widgets = { 'limit_products': forms.CheckboxSelectMultiple(attrs={ 'data-inverse-dependency': '<[name$=all_products]' }), + 'auto_checkin_sales_channels': forms.CheckboxSelectMultiple() } field_classes = { 'limit_products': SafeModelMultipleChoiceField, diff --git a/src/pretix/control/templates/pretixcontrol/checkin/index.html b/src/pretix/control/templates/pretixcontrol/checkin/index.html index 73126a40b7..d4baab2a67 100644 --- a/src/pretix/control/templates/pretixcontrol/checkin/index.html +++ b/src/pretix/control/templates/pretixcontrol/checkin/index.html @@ -119,6 +119,10 @@ {% trans "Not checked in" %} {% else %} {% trans "Checked in" %} + {% if e.auto_checked_in %} + + {% endif %} {% endif %} diff --git a/src/pretix/control/templates/pretixcontrol/checkin/list_edit.html b/src/pretix/control/templates/pretixcontrol/checkin/list_edit.html index 15d31d7492..eae6c63471 100644 --- a/src/pretix/control/templates/pretixcontrol/checkin/list_edit.html +++ b/src/pretix/control/templates/pretixcontrol/checkin/list_edit.html @@ -24,6 +24,7 @@ {% bootstrap_field form.subevent layout="control" %} {% endif %} {% bootstrap_field form.include_pending layout="control" %} + {% bootstrap_field form.auto_checkin_sales_channels layout="control" %} {% trans "Products" %}

{% blocktrans trimmed %} diff --git a/src/pretix/control/templates/pretixcontrol/checkin/lists.html b/src/pretix/control/templates/pretixcontrol/checkin/lists.html index 21c85d22d8..8bb189301b 100644 --- a/src/pretix/control/templates/pretixcontrol/checkin/lists.html +++ b/src/pretix/control/templates/pretixcontrol/checkin/lists.html @@ -60,6 +60,7 @@ {% if request.event.has_subevents %} {% trans "Date" context "subevent" %} {% endif %} + {% trans "Automated check-in" %} {% trans "Products" %} @@ -84,6 +85,12 @@ {% if request.event.has_subevents %} {{ cl.subevent.name }} – {{ cl.subevent.get_date_range_display }} {% endif %} + + {% for channel in cl.auto_checkin_sales_channels %} + + {% endfor %} + {% if cl.all_products %} {% trans "All" %} diff --git a/src/pretix/control/templates/pretixcontrol/order/index.html b/src/pretix/control/templates/pretixcontrol/order/index.html index 02499e28db..07c2f9b7d5 100644 --- a/src/pretix/control/templates/pretixcontrol/order/index.html +++ b/src/pretix/control/templates/pretixcontrol/order/index.html @@ -255,7 +255,11 @@ {% endif %} {% if line.checkins.all %} {% for c in line.checkins.all %} - + {% if c.auto_checked_in %} + + {% else %} + + {% endif %} {% endfor %} {% endif %} {% if line.seat %} diff --git a/src/pretix/control/views/checkin.py b/src/pretix/control/views/checkin.py index c29fc2d561..2cfee1d09e 100644 --- a/src/pretix/control/views/checkin.py +++ b/src/pretix/control/views/checkin.py @@ -1,7 +1,7 @@ import dateutil.parser from django.contrib import messages from django.db import transaction -from django.db.models import Max, OuterRef, Subquery +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 @@ -11,6 +11,7 @@ from django.utils.translation import ugettext_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.control.forms.checkin import CheckinListForm @@ -38,7 +39,10 @@ class CheckInListShow(EventPermissionRequiredMixin, PaginationMixin, ListView): order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING] if self.list.include_pending else [Order.STATUS_PAID], subevent=self.list.subevent ).annotate( - last_checked_in=Subquery(cqs) + last_checked_in=Subquery(cqs), + 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 not self.list.all_products: @@ -146,11 +150,14 @@ class CheckinListList(EventPermissionRequiredMixin, PaginationMixin, ListView): 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 + return ctx diff --git a/src/pretix/plugins/checkinlists/exporters.py b/src/pretix/plugins/checkinlists/exporters.py index a1ad8aa515..0292d3faba 100644 --- a/src/pretix/plugins/checkinlists/exporters.py +++ b/src/pretix/plugins/checkinlists/exporters.py @@ -3,7 +3,7 @@ from collections import OrderedDict import dateutil.parser from django import forms from django.conf import settings -from django.db.models import Max, OuterRef, Subquery +from django.db.models import Exists, Max, OuterRef, Subquery from django.db.models.functions import Coalesce from django.urls import reverse from django.utils.formats import date_format @@ -93,10 +93,14 @@ class CheckInListMixin(BaseExporter): ).order_by().values('position_id').annotate( m=Max('datetime') ).values('m') + qs = OrderPosition.objects.filter( order__event=self.event, ).annotate( - last_checked_in=Subquery(cqs) + last_checked_in=Subquery(cqs), + auto_checked_in=Exists( + Checkin.objects.filter(position_id=OuterRef('pk'), list_id=cl.pk, auto_checked_in=True) + ) ).prefetch_related( 'answers', 'answers__question', 'addon_to__answers', 'addon_to__answers__question' ).select_related('order', 'item', 'variation', 'addon_to', 'order__invoice_address', 'voucher') @@ -309,7 +313,7 @@ class CSVCheckinList(CheckInListMixin, ListExporter): for k, label, w in name_scheme['fields']: headers.append(_('Attendee name: {part}').format(part=label)) headers += [ - _('Product'), _('Price'), _('Checked in') + _('Product'), _('Price'), _('Checked in'), _('Automatically checked in') ] if not cl.include_pending: qs = qs.filter(order__status=Order.STATUS_PAID) @@ -365,7 +369,8 @@ class CSVCheckinList(CheckInListMixin, ListExporter): str(op.item) + (" – " + str(op.variation.value) if op.variation else ""), op.price, date_format(last_checked_in.astimezone(self.event.timezone), 'SHORT_DATETIME_FORMAT') - if last_checked_in else '' + if last_checked_in else '', + _('Yes') if op.auto_checked_in else _('No'), ] if cl.include_pending: row.append(_('Yes') if op.order.status == Order.STATUS_PAID else _('No')) diff --git a/src/pretix/settings.py b/src/pretix/settings.py index 9cf5985094..cefb7452ed 100644 --- a/src/pretix/settings.py +++ b/src/pretix/settings.py @@ -133,7 +133,7 @@ CSRF_TRUSTED_ORIGINS = [urlparse(SITE_URL).hostname] TRUST_X_FORWARDED_FOR = config.get('pretix', 'trust_x_forwarded_for', fallback=False) PRETIX_PLUGINS_DEFAULT = config.get('pretix', 'plugins_default', - fallback='pretix.plugins.sendmail,pretix.plugins.statistics,pretix.plugins.checkinlists') + fallback='pretix.plugins.sendmail,pretix.plugins.statistics,pretix.plugins.checkinlists,pretix.plugins.autocheckin') PRETIX_PLUGINS_EXCLUDE = config.get('pretix', 'plugins_exclude', fallback='').split(',') FETCH_ECB_RATES = config.getboolean('pretix', 'ecb_rates', fallback=True) diff --git a/src/tests/api/test_checkin.py b/src/tests/api/test_checkin.py index cdbb1268e1..b619dd4378 100644 --- a/src/tests/api/test_checkin.py +++ b/src/tests/api/test_checkin.py @@ -147,6 +147,7 @@ def test_list_list(token_client, organizer, event, clist, item, subevent): res = dict(TEST_LIST_RES) res["id"] = clist.pk res["limit_products"] = [item.pk] + res["auto_checkin_sales_channels"] = [] resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/'.format(organizer.slug, event.slug)) assert resp.status_code == 200 @@ -171,6 +172,7 @@ def test_list_detail(token_client, organizer, event, clist, item): res["id"] = clist.pk res["limit_products"] = [item.pk] + res["auto_checkin_sales_channels"] = [] resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/'.format(organizer.slug, event.slug, clist.pk)) assert resp.status_code == 200 @@ -196,6 +198,27 @@ def test_list_create(token_client, organizer, event, item, item_on_wrong_event): assert cl.limit_products.count() == 1 assert not cl.all_products + resp = token_client.post( + '/api/v1/organizers/{}/events/{}/checkinlists/'.format(organizer.slug, event.slug), + { + "name": "VIP", + "limit_products": [item.pk], + "all_products": False, + "subevent": None, + "auto_checkin_sales_channels": [ + "web" + ] + }, + format='json' + ) + assert resp.status_code == 201 + with scopes_disabled(): + cl = CheckinList.objects.get(pk=resp.data['id']) + assert cl.name == "VIP" + assert cl.limit_products.count() == 1 + assert not cl.all_products + assert "web" in cl.auto_checkin_sales_channels + resp = token_client.post( '/api/v1/organizers/{}/events/{}/checkinlists/'.format(organizer.slug, event.slug), { @@ -224,6 +247,24 @@ def test_list_create_with_subevent(token_client, organizer, event, event3, item, ) assert resp.status_code == 201 + resp = token_client.post( + '/api/v1/organizers/{}/events/{}/checkinlists/'.format(organizer.slug, event.slug), + { + "name": "VIP", + "limit_products": [item.pk], + "all_products": True, + "subevent": subevent.pk, + "auto_checkin_sales_channels": [ + "web" + ] + }, + format='json' + ) + assert resp.status_code == 201 + with scopes_disabled(): + cl = CheckinList.objects.get(pk=resp.data['id']) + assert "web" in cl.auto_checkin_sales_channels + resp = token_client.post( '/api/v1/organizers/{}/events/{}/checkinlists/'.format(organizer.slug, event.slug), { @@ -278,6 +319,20 @@ def test_list_update(token_client, organizer, event, clist): cl = CheckinList.objects.get(pk=resp.data['id']) assert cl.name == "VIP" + resp = token_client.patch( + '/api/v1/organizers/{}/events/{}/checkinlists/{}/'.format(organizer.slug, event.slug, clist.pk), + { + "auto_checkin_sales_channels": [ + "web" + ], + }, + format='json' + ) + assert resp.status_code == 200 + with scopes_disabled(): + cl = CheckinList.objects.get(pk=resp.data['id']) + assert "web" in cl.auto_checkin_sales_channels + @pytest.mark.django_db def test_list_all_items_positions(token_client, organizer, event, clist, clist_all, item, other_item, order): @@ -316,7 +371,8 @@ def test_list_all_items_positions(token_client, organizer, event, clist, clist_a p1['checkins'] = [ { 'list': clist_all.pk, - 'datetime': c.datetime.isoformat().replace('+00:00', 'Z') + 'datetime': c.datetime.isoformat().replace('+00:00', 'Z'), + 'auto_checked_in': False } ] resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?has_checkin=1'.format( @@ -353,7 +409,8 @@ def test_list_all_items_positions(token_client, organizer, event, clist, clist_a p2['checkins'] = [ { 'list': clist_all.pk, - 'datetime': c.datetime.isoformat().replace('+00:00', 'Z') + 'datetime': c.datetime.isoformat().replace('+00:00', 'Z'), + 'auto_checked_in': False } ] resp = token_client.get( diff --git a/src/tests/api/test_orders.py b/src/tests/api/test_orders.py index a91d4eca65..5d8c04c005 100644 --- a/src/tests/api/test_orders.py +++ b/src/tests/api/test_orders.py @@ -122,6 +122,12 @@ def order(event, item, taxrule, question): return o +@pytest.fixture +def clist_autocheckin(event): + c = event.checkin_lists.create(name="Default", all_products=True, auto_checkin_sales_channels=['web']) + return c + + TEST_ORDERPOSITION_RES = { "id": 1, "order": "FOO", @@ -699,7 +705,7 @@ def test_orderposition_list(token_client, organizer, event, order, item, subeven with scopes_disabled(): cl = event.checkin_lists.create(name="Default") op.checkins.create(datetime=datetime.datetime(2017, 12, 26, 10, 0, 0, tzinfo=UTC), list=cl) - res['checkins'] = [{'datetime': '2017-12-26T10:00:00Z', 'list': cl.pk}] + res['checkins'] = [{'datetime': '2017-12-26T10:00:00Z', 'list': cl.pk, 'auto_checked_in': False}] resp = token_client.get( '/api/v1/organizers/{}/events/{}/orderpositions/?has_checkin=true'.format(organizer.slug, event.slug)) assert [res] == resp.data['results'] @@ -1432,6 +1438,37 @@ def test_order_create(token_client, organizer, event, item, quota, question): assert answ.answer == "S" +@pytest.mark.django_db +def test_order_create_autocheckin(token_client, organizer, event, item, quota, question, clist_autocheckin): + res = copy.deepcopy(ORDER_CREATE_PAYLOAD) + res['positions'][0]['item'] = item.pk + res['positions'][0]['answers'][0]['question'] = question.pk + resp = token_client.post( + '/api/v1/organizers/{}/events/{}/orders/'.format( + organizer.slug, event.slug + ), format='json', data=res + ) + assert resp.status_code == 201 + with scopes_disabled(): + o = Order.objects.get(code=resp.data['code']) + assert "web" in clist_autocheckin.auto_checkin_sales_channels + assert o.positions.first().checkins.first().auto_checked_in + + clist_autocheckin.auto_checkin_sales_channels = [] + clist_autocheckin.save() + + resp = token_client.post( + '/api/v1/organizers/{}/events/{}/orders/'.format( + organizer.slug, event.slug + ), format='json', data=res + ) + assert resp.status_code == 201 + with scopes_disabled(): + o = Order.objects.get(code=resp.data['code']) + assert clist_autocheckin.auto_checkin_sales_channels == [] + assert o.positions.first().checkins.count() == 0 + + @pytest.mark.django_db def test_order_create_invoice_address_optional(token_client, organizer, event, item, quota, question): res = copy.deepcopy(ORDER_CREATE_PAYLOAD) diff --git a/src/tests/base/test_orders.py b/src/tests/base/test_orders.py index df15423cdf..6301c0c266 100644 --- a/src/tests/base/test_orders.py +++ b/src/tests/base/test_orders.py @@ -38,6 +38,12 @@ def event(): yield event +@pytest.fixture +def clist_autocheckin(event): + c = event.checkin_lists.create(name="Default", all_products=True, auto_checkin_sales_channels=['web']) + return c + + @pytest.mark.django_db def test_expiry_days(event): today = now() @@ -1848,3 +1854,28 @@ class OrderChangeManagerTests(TestCase): self.ocm.change_subevent(self.op1, se2) with self.assertRaises(OrderError): self.ocm.add_position(self.ticket, None, price=Decimal('13.00'), subevent=se2, seat=self.seat_a1) + + +@pytest.mark.django_db +def test_autocheckin(clist_autocheckin, event): + today = now() + tr7 = event.tax_rules.create(rate=Decimal('17.00')) + ticket = Item.objects.create(event=event, name='Early-bird ticket', tax_rule=tr7, + default_price=Decimal('23.00'), admission=True) + cp1 = CartPosition.objects.create( + item=ticket, price=23, expires=now() + timedelta(days=1), event=event, cart_id="123" + ) + order = _create_order(event, email='dummy@example.org', positions=[cp1], + now_dt=today, payment_provider=FreeOrderProvider(event), + locale='de')[0] + assert "web" in clist_autocheckin.auto_checkin_sales_channels + assert order.positions.first().checkins.first().auto_checked_in + + clist_autocheckin.auto_checkin_sales_channels = [] + clist_autocheckin.save() + + order = _create_order(event, email='dummy@example.org', positions=[cp1], + now_dt=today, payment_provider=FreeOrderProvider(event), + locale='de')[0] + assert clist_autocheckin.auto_checkin_sales_channels == [] + assert order.positions.first().checkins.count() == 0 diff --git a/src/tests/plugins/test_checkinlist.py b/src/tests/plugins/test_checkinlist.py index 0c08b4f702..9683790769 100644 --- a/src/tests/plugins/test_checkinlist.py +++ b/src/tests/plugins/test_checkinlist.py @@ -66,11 +66,11 @@ def test_csv_simple(event): 'questions': [] }) assert clean(content.decode()) == clean(""""Order code","Attendee name","Attendee name: Title","Attendee name: - First name","Attendee name: Middle name","Attendee name: Family name","Product","Price","Checked in","Secret", -"E-mail","Company","Voucher code","Order date","Requires special attention","Comment" -"FOO","Mr Peter A Jones","Mr","Peter","A","Jones","Ticket","23.00","","hutjztuxhkbtwnesv2suqv26k6ttytxx", + First name","Attendee name: Middle name","Attendee name: Family name","Product","Price","Checked in","Automatically + checked in","Secret","E-mail","Company","Voucher code","Order date","Requires special attention","Comment" +"FOO","Mr Peter A Jones","Mr","Peter","A","Jones","Ticket","23.00","","No","hutjztuxhkbtwnesv2suqv26k6ttytxx", "dummy@dummy.test","","","2019-02-22","No","" -"FOO","Mrs Andrea J Zulu","Mrs","Andrea","J","Zulu","Ticket","13.00","","ggsngqtnmhx74jswjngw3fk8pfwz2a7k", +"FOO","Mrs Andrea J Zulu","Mrs","Andrea","J","Zulu","Ticket","13.00","","No","ggsngqtnmhx74jswjngw3fk8pfwz2a7k", "dummy@dummy.test","","","2019-02-22","No","" """) @@ -90,10 +90,11 @@ def test_csv_order_by_name_parts(event): # noqa }) assert clean(content.decode()) == clean(""""Order code","Attendee name","Attendee name: Title", "Attendee name: First name","Attendee name: Middle name","Attendee name: Family name","Product","Price", -"Checked in","Secret","E-mail","Company","Voucher code","Order date","Requires special attention","Comment" -"FOO","Mrs Andrea J Zulu","Mrs","Andrea","J","Zulu","Ticket","13.00","","ggsngqtnmhx74jswjngw3fk8pfwz2a7k", +"Checked in","Automatically checked in","Secret","E-mail","Company","Voucher code","Order date","Requires special + attention","Comment" +"FOO","Mrs Andrea J Zulu","Mrs","Andrea","J","Zulu","Ticket","13.00","","No","ggsngqtnmhx74jswjngw3fk8pfwz2a7k", "dummy@dummy.test","","","2019-02-22","No","" -"FOO","Mr Peter A Jones","Mr","Peter","A","Jones","Ticket","23.00","","hutjztuxhkbtwnesv2suqv26k6ttytxx", +"FOO","Mr Peter A Jones","Mr","Peter","A","Jones","Ticket","23.00","","No","hutjztuxhkbtwnesv2suqv26k6ttytxx", "dummy@dummy.test","","","2019-02-22","No","" """) c = CSVCheckinList(event) @@ -106,9 +107,10 @@ def test_csv_order_by_name_parts(event): # noqa }) assert clean(content.decode()) == clean(""""Order code","Attendee name","Attendee name: Title", "Attendee name: First name","Attendee name: Middle name","Attendee name: Family name","Product","Price", -"Checked in","Secret","E-mail","Company","Voucher code","Order date","Requires special attention","Comment" -"FOO","Mr Peter A Jones","Mr","Peter","A","Jones","Ticket","23.00","","hutjztuxhkbtwnesv2suqv26k6ttytxx", +"Checked in","Automatically checked in","Secret","E-mail","Company","Voucher code","Order date","Requires special + attention","Comment" +"FOO","Mr Peter A Jones","Mr","Peter","A","Jones","Ticket","23.00","","No","hutjztuxhkbtwnesv2suqv26k6ttytxx", "dummy@dummy.test","","","2019-02-22","No","" -"FOO","Mrs Andrea J Zulu","Mrs","Andrea","J","Zulu","Ticket","13.00","","ggsngqtnmhx74jswjngw3fk8pfwz2a7k", +"FOO","Mrs Andrea J Zulu","Mrs","Andrea","J","Zulu","Ticket","13.00","","No","ggsngqtnmhx74jswjngw3fk8pfwz2a7k", "dummy@dummy.test","","","2019-02-22","No","" """)