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" %}
{% 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",""
""")
|