Auto-check-in for specific sales channels (#1409)

* Autocheckin data model/cosmetics

* Expose automatically checked-in OrderPositions

* Expose automatically checked-in OrderPositions in CSV/PDF Exports

* Fix some tests, try to fix MultiStringField/CheckboxSelectMultiple

* Actually fix MultiStringField/CheckboxSelectMultiple.
(Not pretty, but it works)

* Fix more tests

* Squash migration

* Also fix CSV/nameparts-test

* Changes for Autocheckin code-review

* Perform Auto-Checkins through new core plugin

* Update config-doc to reflect also checkinlists

* Explicitly output AutoCheckin Yes/No for CSV-Export (+ fix test)

* Move autocheckin from plugin to service

* API-doc

* Fix API-doc spelling

* Checkinlist-API and autocheckin order tests

* Performance improvement when reading checkinlists for autocheckin

Co-Authored-By: Raphael Michel <michel@rami.io>

* Autocheckin test for order created through API

* Resolve migration conflict
This commit is contained in:
Martin Gross
2019-10-08 15:50:22 +02:00
committed by Raphael Michel
co-authored by Raphael Michel
parent 05a1df244b
commit 748a389acb
20 changed files with 306 additions and 37 deletions
+36 -7
View File
@@ -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": [
+19 -5
View File
@@ -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": [
+6 -1
View File
@@ -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
+1 -1
View File
@@ -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):
+1 -1
View File
@@ -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
@@ -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=[]),
)
]
+11
View File
@@ -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')
+17
View File
@@ -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)
+14 -1
View File
@@ -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,
@@ -119,6 +119,10 @@
<span class="label label-danger">{% trans "Not checked in" %}</span>
{% else %}
<span class="label label-success">{% trans "Checked in" %}</span>
{% if e.auto_checked_in %}
<span class="fa fa-magic text-muted"
data-toggle="tooltip" title="{% trans "Checked in automatically" %}"></span>
{% endif %}
{% endif %}
</td>
<td>
@@ -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" %}
<legend>{% trans "Products" %}</legend>
<p>
{% blocktrans trimmed %}
@@ -60,6 +60,7 @@
{% if request.event.has_subevents %}
<th>{% trans "Date" context "subevent" %}</th>
{% endif %}
<th class="iconcol">{% trans "Automated check-in" %}</th>
<th>{% trans "Products" %}</th>
<th class="action-col-2"></th>
</tr>
@@ -84,6 +85,12 @@
{% if request.event.has_subevents %}
<td>{{ cl.subevent.name }} {{ cl.subevent.get_date_range_display }}</td>
{% endif %}
<td>
{% for channel in cl.auto_checkin_sales_channels %}
<span class="fa fa-{{ channel.icon }} text-muted"
data-toggle="tooltip" title="{% trans channel.verbose_name %}"></span>
{% endfor %}
</td>
<td>
{% if cl.all_products %}
<em>{% trans "All" %}</em>
@@ -255,7 +255,11 @@
{% endif %}
{% if line.checkins.all %}
{% for c in line.checkins.all %}
<span class="fa fa-fw fa-check" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}First scanned: {{ date }}{% endblocktrans %}"></span>
{% if c.auto_checked_in %}
<span class="fa fa-fw fa-magic" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically checked in: {{ date }}{% endblocktrans %}"></span>
{% else %}
<span class="fa fa-fw fa-check" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}First scanned: {{ date }}{% endblocktrans %}"></span>
{% endif %}
{% endfor %}
{% endif %}
{% if line.seat %}
+9 -2
View File
@@ -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
+9 -4
View File
@@ -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'))
+1 -1
View File
@@ -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)
+59 -2
View File
@@ -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(
+38 -1
View File
@@ -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)
+31
View File
@@ -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
+12 -10
View File
@@ -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",""
""")