mirror of
https://github.com/pretix/pretix.git
synced 2026-08-20 12:26:27 +00:00
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:
committed by
Raphael Michel
co-authored by
Raphael Michel
parent
05a1df244b
commit
748a389acb
@@ -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": [
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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=[]),
|
||||
)
|
||||
]
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",""
|
||||
""")
|
||||
|
||||
Reference in New Issue
Block a user