From 3fbccf3f649cbfc320b33c909f658e37ab9ce1e1 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Wed, 21 Feb 2018 16:08:53 +0100 Subject: [PATCH] Allow check-in lists to include unpaid orders --- doc/api/resources/checkinlists.rst | 11 ++- doc/plugins/pretixdroid.rst | 12 +++ src/pretix/api/serializers/checkin.py | 3 +- src/pretix/api/views/checkin.py | 2 +- .../migrations/0081_auto_20180220_1031.py | 46 ++++++++++++ src/pretix/base/models/checkin.py | 73 ++++++++++++++++--- src/pretix/control/forms/checkin.py | 3 +- .../pretixcontrol/checkin/index.html | 3 + .../pretixcontrol/checkin/list_edit.html | 1 + src/pretix/control/views/checkin.py | 2 +- src/pretix/plugins/checkinlists/exporters.py | 12 +-- src/pretix/plugins/pretixdroid/views.py | 44 +++++++---- src/tests/api/test_checkin.py | 1 + src/tests/base/test_models.py | 22 +++++- src/tests/plugins/pretixdroid/test_simple.py | 23 ++++++ 15 files changed, 220 insertions(+), 38 deletions(-) create mode 100644 src/pretix/base/migrations/0081_auto_20180220_1031.py diff --git a/doc/api/resources/checkinlists.rst b/doc/api/resources/checkinlists.rst index e92c9a394e..0c8e713bcb 100644 --- a/doc/api/resources/checkinlists.rst +++ b/doc/api/resources/checkinlists.rst @@ -21,11 +21,12 @@ Field Type Description ===================================== ========================== ======================================================= id integer Internal ID of the check-in list name string The internal name of the check-in list -all_products boolean If ``True``, the check-in lists contains tickets of all products in this event. The ``limit_products`` field is ignored in this case. +all_products boolean If ``true``, the check-in lists contains tickets of all products in this event. The ``limit_products`` field is ignored in this case. limit_products list of integers List of item IDs to include in this list. subevent integer ID of the date inside an event series this list belongs to (or ``null``). 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. ===================================== ========================== ======================================================= .. versionchanged:: 1.10 @@ -36,6 +37,10 @@ checkin_count integer Number of check The ``positions`` endpoints have been added. +.. versionchanged:: 1.13 + + The ``include_pending`` field has been added. + Endpoints --------- @@ -71,6 +76,7 @@ Endpoints "position_count": 456, "all_products": true, "limit_products": [], + "include_pending": false, "subevent": null } ] @@ -111,6 +117,7 @@ Endpoints "position_count": 456, "all_products": true, "limit_products": [], + "include_pending": false, "subevent": null } @@ -156,6 +163,7 @@ Endpoints "position_count": 0, "all_products": false, "limit_products": [1, 2], + "include_pending": false, "subevent": null } @@ -204,6 +212,7 @@ Endpoints "position_count": 42, "all_products": false, "limit_products": [1, 2], + "include_pending": false, "subevent": null } diff --git a/doc/plugins/pretixdroid.rst b/doc/plugins/pretixdroid.rst index b16b6b2bcd..715297269b 100644 --- a/doc/plugins/pretixdroid.rst +++ b/doc/plugins/pretixdroid.rst @@ -15,6 +15,10 @@ uses to communicate with the pretix server. negotiated live, so clients which do not need this feature can ignore the change. For this reason, the API version has not been increased and is still set to 3. +.. versionchanged:: 1.13 + + Support for checking in unpaid tickets has been added. + .. http:post:: /pretixdroid/api/(organizer)/(event)/redeem/ @@ -49,6 +53,9 @@ uses to communicate with the pretix server. check-in. This is meant to be used to prevent duplicate check-ins when you are just retrying after a connection failure. + You **may** set the additional parameter ``ignore_unpaid`` to indicate that the check-in should be performed even + if the order is in pending state. + If questions are supported and required, you will receive a dictionary ``questions`` containing details on the particular questions to ask. To answer them, just re-send your redemption request with additional parameters of the form ``answer_=``, e.g. ``answer_12=24``. @@ -73,6 +80,7 @@ uses to communicate with the pretix server. "attendee_name": "Peter Higgs", "attention": false, "redeemed": true, + "checkin_allowed": true, "paid": true } } @@ -97,6 +105,7 @@ uses to communicate with the pretix server. "attendee_name": "Peter Higgs", "attention": false, "redeemed": true, + "checkin_allowed": true, "paid": true }, "questions": [ @@ -142,6 +151,7 @@ uses to communicate with the pretix server. "attendee_name": "Peter Higgs", "attention": false, "redeemed": true, + "checkin_allowed": true, "paid": true } } @@ -201,6 +211,7 @@ uses to communicate with the pretix server. "attendee_name": "Peter Higgs", "redeemed": false, "attention": false, + "checkin_allowed": true, "paid": true }, ... @@ -244,6 +255,7 @@ uses to communicate with the pretix server. "attendee_name": "Peter Higgs", "redeemed": false, "attention": false, + "checkin_allowed": true, "paid": true }, ... diff --git a/src/pretix/api/serializers/checkin.py b/src/pretix/api/serializers/checkin.py index 2a6c405410..2461d53173 100644 --- a/src/pretix/api/serializers/checkin.py +++ b/src/pretix/api/serializers/checkin.py @@ -12,7 +12,8 @@ class CheckinListSerializer(I18nAwareModelSerializer): class Meta: model = CheckinList - fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count') + fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count', + 'include_pending') def validate(self, data): data = super().validate(data) diff --git a/src/pretix/api/views/checkin.py b/src/pretix/api/views/checkin.py index 5538bcb0a5..ca87b90cd7 100644 --- a/src/pretix/api/views/checkin.py +++ b/src/pretix/api/views/checkin.py @@ -126,7 +126,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet): qs = OrderPosition.objects.filter( order__event=self.request.event, - order__status=Order.STATUS_PAID, + order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING] if self.checkinlist.include_pending else [Order.STATUS_PAID], subevent=self.checkinlist.subevent ).annotate( last_checked_in=Subquery(cqs) diff --git a/src/pretix/base/migrations/0081_auto_20180220_1031.py b/src/pretix/base/migrations/0081_auto_20180220_1031.py new file mode 100644 index 0000000000..38a93da781 --- /dev/null +++ b/src/pretix/base/migrations/0081_auto_20180220_1031.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.10 on 2018-02-20 10:31 +from __future__ import unicode_literals + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0080_question_ask_during_checkin'), + ] + + operations = [ + migrations.AddField( + model_name='checkinlist', + name='include_pending', + field=models.BooleanField(default=False, verbose_name='Include pending orders'), + ), + migrations.AlterField( + model_name='event', + name='presale_end', + field=models.DateTimeField(blank=True, help_text='Optional. No products will be sold after this date. If you do not set this value, the presale will end after the end date of your event.', null=True, verbose_name='End of presale'), + ), + migrations.AlterField( + model_name='logentry', + name='event', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='pretixbase.Event'), + ), + migrations.AlterField( + model_name='question', + name='ask_during_checkin', + field=models.BooleanField(default=False, help_text='This will only work if you handle your check-in with pretixdroid 1.8 or newer or pretixdesk 0.2 or newer.', verbose_name='Ask during check-in instead of in the ticket buying process'), + ), + migrations.AlterField( + model_name='subevent', + name='presale_end', + field=models.DateTimeField(blank=True, help_text='Optional. No products will be sold after this date. If you do not set this value, the presale will end after the end date of your event.', null=True, verbose_name='End of presale'), + ), + migrations.AlterField( + model_name='user', + name='require_2fa', + field=models.BooleanField(default=False, verbose_name='Two-factor authentification is required to log in'), + ), + ] diff --git a/src/pretix/base/models/checkin.py b/src/pretix/base/models/checkin.py index 4d82989b19..d726d1bbb1 100644 --- a/src/pretix/base/models/checkin.py +++ b/src/pretix/base/models/checkin.py @@ -14,6 +14,11 @@ class CheckinList(LoggedModel): limit_products = models.ManyToManyField('Item', verbose_name=_("Limit to products"), blank=True) subevent = models.ForeignKey('SubEvent', null=True, blank=True, verbose_name=pgettext_lazy('subevent', 'Date')) + include_pending = models.BooleanField(verbose_name=pgettext_lazy('checkin', 'Include pending orders'), + default=False, + help_text=_('With this option, people will be able to check in even if the ' + 'order have not been paid. This only works with pretixdesk ' + '0.3.0 or newer or pretixdroid 1.9 or newer.')) @staticmethod def annotate_with_numbers(qs, event): @@ -29,7 +34,7 @@ class CheckinList(LoggedModel): # position and to the list in question. Then, we check that it also belongs to the # correct subevent (just to be sure) and aggregate over lists (so, over everything, # since we filtered by lists). - cqs = Checkin.objects.filter( + cqs_paid = Checkin.objects.filter( position__order__event=event, position__order__status=Order.STATUS_PAID, list=OuterRef('pk') @@ -41,12 +46,24 @@ class CheckinList(LoggedModel): ).order_by().values('list').annotate( c=Count('*') ).values('c') + cqs_paid_and_pending = Checkin.objects.filter( + position__order__event=event, + position__order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING], + list=OuterRef('pk') + ).filter( + # This assumes that in an event with subevents, *all* positions have subevents + # and *all* checkin lists have a subevent assigned + Q(position__subevent=OuterRef('subevent')) + | (Q(position__subevent__isnull=True)) + ).order_by().values('list').annotate( + c=Count('*') + ).values('c') # Now for the hard part: getting all order positions that contribute to this list. This # requires us to use TWO subqueries. The first one, pqs_all, will only be used for check-in # lists that contain all the products of the event. This is the simpler one, it basically # looks like the check-in counter above. - pqs_all = OrderPosition.objects.filter( + pqs_all_paid = OrderPosition.objects.filter( order__event=event, order__status=Order.STATUS_PAID, ).filter( @@ -57,13 +74,24 @@ class CheckinList(LoggedModel): ).order_by().values('order__event').annotate( c=Count('*') ).values('c') + pqs_all_paid_and_pending = OrderPosition.objects.filter( + order__event=event, + order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING] + ).filter( + # This assumes that in an event with subevents, *all* positions have subevents + # and *all* checkin lists have a subevent assigned + Q(subevent=OuterRef('subevent')) + | (Q(subevent__isnull=True)) + ).order_by().values('order__event').annotate( + c=Count('*') + ).values('c') # Now we need a subquery for the case of checkin lists that are limited to certain # products. We cannot use OuterRef("limit_products") since that would do a cross-product # with the products table and we'd get duplicate rows in the output with different annotations # on them, which isn't useful at all. Therefore, we need to add a second layer of subqueries # to retrieve all of those items and then check if the item_id is IN this subquery result. - pqs_limited = OrderPosition.objects.filter( + pqs_limited_paid = OrderPosition.objects.filter( order__event=event, order__status=Order.STATUS_PAID, item_id__in=Subquery(Item.objects.filter(checkinlist__pk=OuterRef(OuterRef('pk'))).values('pk')) @@ -75,17 +103,44 @@ class CheckinList(LoggedModel): ).order_by().values('order__event').annotate( c=Count('*') ).values('c') + pqs_limited_paid_and_pending = OrderPosition.objects.filter( + order__event=event, + order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING], + item_id__in=Subquery(Item.objects.filter(checkinlist__pk=OuterRef(OuterRef('pk'))).values('pk')) + ).filter( + # This assumes that in an event with subevents, *all* positions have subevents + # and *all* checkin lists have a subevent assigned + Q(subevent=OuterRef('subevent')) + | (Q(subevent__isnull=True)) + ).order_by().values('order__event').annotate( + c=Count('*') + ).values('c') # Finally, we put all of this together. We force empty subquery aggregates to 0 by using Coalesce() # and decide which subquery to use for this row. In the end, we compute an integer percentage in case # we want to display a progress bar. return qs.annotate( - checkin_count=Coalesce(Subquery(cqs, output_field=models.IntegerField()), 0), - position_count=Coalesce(Case( - When(all_products=True, then=Subquery(pqs_all, output_field=models.IntegerField())), - default=Subquery(pqs_limited, output_field=models.IntegerField()), - output_field=models.IntegerField() - ), 0) + checkin_count=Coalesce( + Case( + When(include_pending=True, then=Subquery(cqs_paid_and_pending, output_field=models.IntegerField())), + default=Subquery(cqs_paid, output_field=models.IntegerField()), + output_field=models.IntegerField() + ), + 0 + ), + position_count=Coalesce( + Case( + When(all_products=True, include_pending=False, + then=Subquery(pqs_all_paid, output_field=models.IntegerField())), + When(all_products=True, include_pending=True, + then=Subquery(pqs_all_paid_and_pending, output_field=models.IntegerField())), + When(all_products=False, include_pending=False, + then=Subquery(pqs_limited_paid, output_field=models.IntegerField())), + default=Subquery(pqs_limited_paid_and_pending, output_field=models.IntegerField()), + output_field=models.IntegerField() + ), + 0 + ) ).annotate( percent=Case( When(position_count__gt=0, then=F('checkin_count') * 100 / F('position_count')), diff --git a/src/pretix/control/forms/checkin.py b/src/pretix/control/forms/checkin.py index 89769b9348..e3e1106f0a 100644 --- a/src/pretix/control/forms/checkin.py +++ b/src/pretix/control/forms/checkin.py @@ -36,7 +36,8 @@ class CheckinListForm(forms.ModelForm): 'name', 'all_products', 'limit_products', - 'subevent' + 'subevent', + 'include_pending' ] widgets = { 'limit_products': forms.CheckboxSelectMultiple(attrs={ diff --git a/src/pretix/control/templates/pretixcontrol/checkin/index.html b/src/pretix/control/templates/pretixcontrol/checkin/index.html index f61e6b4cef..614f4bf389 100644 --- a/src/pretix/control/templates/pretixcontrol/checkin/index.html +++ b/src/pretix/control/templates/pretixcontrol/checkin/index.html @@ -86,6 +86,9 @@ {{ e.order.code }} + {% if e.order.status == "n" %} + {% trans "unpaid" %} + {% endif %} {{ e.item.name }}{% if e.variation %} – {{ e.variation }}{% endif %} {{ e.order.email }} diff --git a/src/pretix/control/templates/pretixcontrol/checkin/list_edit.html b/src/pretix/control/templates/pretixcontrol/checkin/list_edit.html index 336a7bc483..15d31d7492 100644 --- a/src/pretix/control/templates/pretixcontrol/checkin/list_edit.html +++ b/src/pretix/control/templates/pretixcontrol/checkin/list_edit.html @@ -23,6 +23,7 @@ {% if form.subevent %} {% bootstrap_field form.subevent layout="control" %} {% endif %} + {% bootstrap_field form.include_pending layout="control" %} {% trans "Products" %}

{% blocktrans trimmed %} diff --git a/src/pretix/control/views/checkin.py b/src/pretix/control/views/checkin.py index 14ebfcb377..af6ee8a7aa 100644 --- a/src/pretix/control/views/checkin.py +++ b/src/pretix/control/views/checkin.py @@ -35,7 +35,7 @@ class CheckInListShow(EventPermissionRequiredMixin, PaginationMixin, ListView): qs = OrderPosition.objects.filter( order__event=self.request.event, - order__status=Order.STATUS_PAID, + 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) diff --git a/src/pretix/plugins/checkinlists/exporters.py b/src/pretix/plugins/checkinlists/exporters.py index 2b3eea3ba8..576ea8ba42 100644 --- a/src/pretix/plugins/checkinlists/exporters.py +++ b/src/pretix/plugins/checkinlists/exporters.py @@ -37,12 +37,6 @@ class BaseCheckinList(BaseExporter): label=_('Include QR-code secret'), required=False )), - ('paid_only', - forms.BooleanField( - label=_('Only paid orders'), - initial=True, - required=False - )), ('sort', forms.ChoiceField( label=_('Sort by'), @@ -182,7 +176,7 @@ class PDFCheckinList(ReportlabExportMixin, BaseCheckinList): elif form_data['sort'] == 'code': qs = qs.order_by('order__code') - if form_data['paid_only']: + if not cl.include_pending: qs = qs.filter(order__status=Order.STATUS_PAID) else: qs = qs.filter(order__status__in=(Order.STATUS_PAID, Order.STATUS_PENDING)) @@ -267,7 +261,7 @@ class CSVCheckinList(BaseCheckinList): headers = [ _('Order code'), _('Attendee name'), _('Product'), _('Price'), _('Checked in') ] - if form_data['paid_only']: + if not cl.include_pending: qs = qs.filter(order__status=Order.STATUS_PAID) else: qs = qs.filter(order__status__in=(Order.STATUS_PAID, Order.STATUS_PENDING)) @@ -303,7 +297,7 @@ class CSVCheckinList(BaseCheckinList): date_format(last_checked_in.astimezone(self.event.timezone), 'SHORT_DATETIME_FORMAT') if last_checked_in else '' ] - if not form_data['paid_only']: + if cl.include_pending: row.append(_('Yes') if op.order.status == Order.STATUS_PAID else _('No')) if form_data['secrets']: row.append(op.secret) diff --git a/src/pretix/plugins/pretixdroid/views.py b/src/pretix/plugins/pretixdroid/views.py index ee50bb3a39..102f989851 100644 --- a/src/pretix/plugins/pretixdroid/views.py +++ b/src/pretix/plugins/pretixdroid/views.py @@ -122,7 +122,6 @@ class ConfigView(EventPermissionRequiredMixin, TemplateView): class ApiView(View): - @method_decorator(csrf_exempt) def dispatch(self, request, **kwargs): try: @@ -156,7 +155,6 @@ class ApiView(View): class ApiRedeemView(ApiView): - def _save_answers(self, op, answers, given_answers): for q, a in given_answers.items(): if not a: @@ -193,6 +191,7 @@ class ApiRedeemView(ApiView): def post(self, request, **kwargs): secret = request.POST.get('secret', '!INVALID!') force = request.POST.get('force', 'false') in ('true', 'True') + ignore_unpaid = request.POST.get('ignore_unpaid', 'false') in ('true', 'True') nonce = request.POST.get('nonce') response = { 'version': API_VERSION @@ -237,23 +236,26 @@ class ApiRedeemView(ApiView): self._save_answers(op, answers, given_answers) - if not self.config.list.all_products and op.item_id not in [i.pk for i in self.config.list.limit_products.all()]: + if not self.config.list.all_products and op.item_id not in [i.pk for i in + self.config.list.limit_products.all()]: response['status'] = 'error' response['reason'] = 'product' elif not self.config.all_items and op.item_id not in [i.pk for i in self.config.items.all()]: response['status'] = 'error' response['reason'] = 'product' + elif op.order.status != Order.STATUS_PAID and not force and not ( + ignore_unpaid and self.config.list.include_pending and op.order.status == Order.STATUS_PENDING + ): + response['status'] = 'error' + response['reason'] = 'unpaid' elif require_answers and not force and request.POST.get('questions_supported'): response['status'] = 'incomplete' response['questions'] = require_answers - elif op.order.status == Order.STATUS_PAID or force: + else: ci, created = Checkin.objects.get_or_create(position=op, list=self.config.list, defaults={ 'datetime': dt, 'nonce': nonce, }) - else: - response['status'] = 'error' - response['reason'] = 'unpaid' if 'status' not in response: if created or (nonce and nonce == ci.nonce): @@ -282,7 +284,8 @@ class ApiRedeemView(ApiView): 'list': self.config.list.pk }) - response['data'] = serialize_op(op, redeemed=op.order.status == Order.STATUS_PAID or force) + response['data'] = serialize_op(op, redeemed=op.order.status == Order.STATUS_PAID or force, + clist=self.config.list) except OrderPosition.DoesNotExist: response['status'] = 'error' @@ -310,7 +313,7 @@ def serialize_question(q, items=False): return d -def serialize_op(op, redeemed): +def serialize_op(op, redeemed, clist): name = op.attendee_name if not name and op.addon_to: name = op.addon_to.attendee_name @@ -319,6 +322,13 @@ def serialize_op(op, redeemed): name = op.order.invoice_address.name except: pass + checkin_allowed = ( + op.order.status == Order.STATUS_PAID + or ( + op.order.status == Order.STATUS_PENDING + and clist.include_pending + ) + ) return { 'secret': op.secret, 'order': op.order.code, @@ -330,6 +340,7 @@ def serialize_op(op, redeemed): 'attention': op.item.checkin_attention, 'redeemed': redeemed, 'paid': op.order.status == Order.STATUS_PAID, + 'checkin_allowed': checkin_allowed } @@ -371,7 +382,7 @@ class ApiSearchView(ApiView): | Q(order__invoice_address__name__icontains=query) )[:25] - response['results'] = [serialize_op(op, bool(op.last_checked_in)) for op in ops] + response['results'] = [serialize_op(op, bool(op.last_checked_in), self.config.list) for op in ops] else: response['results'] = [] @@ -393,7 +404,8 @@ class ApiDownloadView(ApiView): qs = OrderPosition.objects.filter( order__event=self.event, - order__status=Order.STATUS_PAID, + order__status__in=[Order.STATUS_PAID] + ([Order.STATUS_PENDING] if self.config.list.include_pending else + []), subevent=self.config.list.subevent ).annotate( last_checked_in=Subquery(cqs) @@ -405,7 +417,7 @@ class ApiDownloadView(ApiView): if not self.config.all_items: qs = qs.filter(item__in=self.config.items.all()) - response['results'] = [serialize_op(op, bool(op.last_checked_in)) for op in qs] + response['results'] = [serialize_op(op, bool(op.last_checked_in), self.config.list) for op in qs] questions = self.event.questions.filter(ask_during_checkin=True).prefetch_related('items', 'options') response['questions'] = [serialize_question(q, items=True) for q in questions] @@ -417,11 +429,15 @@ class ApiStatusView(ApiView): cqs = Checkin.objects.filter( position__order__event=self.event, position__subevent=self.subevent, - position__order__status=Order.STATUS_PAID, + position__order__status__in=[Order.STATUS_PAID] + ([Order.STATUS_PENDING] if + self.config.list.include_pending else []), list=self.config.list ) pqs = OrderPosition.objects.filter( - order__event=self.event, order__status=Order.STATUS_PAID, subevent=self.subevent, + order__event=self.event, + order__status__in=[Order.STATUS_PAID] + ([Order.STATUS_PENDING] if self.config.list.include_pending else + []), + subevent=self.subevent, ) if not self.config.list.all_products: pqs = pqs.filter(item__in=self.config.list.limit_products.values_list('id', flat=True)) diff --git a/src/tests/api/test_checkin.py b/src/tests/api/test_checkin.py index 976ecd06bb..6d9ce03905 100644 --- a/src/tests/api/test_checkin.py +++ b/src/tests/api/test_checkin.py @@ -111,6 +111,7 @@ TEST_LIST_RES = { "limit_products": [], "position_count": 0, "checkin_count": 0, + "include_pending": False, "subevent": None } diff --git a/src/tests/base/test_models.py b/src/tests/base/test_models.py index c8f134b460..c5bc25e849 100644 --- a/src/tests/base/test_models.py +++ b/src/tests/base/test_models.py @@ -1116,6 +1116,9 @@ class CheckinListTestCase(TestCase): cls.cl_all = cls.event.checkin_lists.create( name='All', all_products=True ) + cls.cl_all_pending = cls.event.checkin_lists.create( + name='Z Pending', all_products=True, include_pending=True + ) cls.cl_both = cls.event.checkin_lists.create( name='Both', all_products=False ) @@ -1152,9 +1155,23 @@ class CheckinListTestCase(TestCase): op2.checkins.create(list=cls.cl_tickets) op3.checkins.create(list=cls.cl_both) + o = Order.objects.create( + code='FOO', event=cls.event, email='dummy@dummy.test', + status=Order.STATUS_PENDING, + datetime=now(), expires=now() + timedelta(days=10), + total=Decimal("30"), payment_provider='banktransfer', locale='en' + ) + op4 = OrderPosition.objects.create( + order=o, + item=cls.item2, + variation=None, + price=Decimal("6"), + ) + op4.checkins.create(list=cls.cl_all_pending) + def test_annotated(self): lists = list(CheckinList.annotate_with_numbers(self.event.checkin_lists.order_by('name'), self.event)) - assert lists == [self.cl_all, self.cl_both, self.cl_tickets] + assert lists == [self.cl_all, self.cl_both, self.cl_tickets, self.cl_all_pending] assert lists[0].checkin_count == 0 assert lists[0].position_count == 3 assert lists[0].percent == 0 @@ -1164,6 +1181,9 @@ class CheckinListTestCase(TestCase): assert lists[2].checkin_count == 1 assert lists[2].position_count == 2 assert lists[2].percent == 50 + assert lists[3].checkin_count == 1 + assert lists[3].position_count == 4 + assert lists[3].percent == 25 @pytest.mark.django_db diff --git a/src/tests/plugins/pretixdroid/test_simple.py b/src/tests/plugins/pretixdroid/test_simple.py index b6c8ea8fdd..6d49ab35bd 100644 --- a/src/tests/plugins/pretixdroid/test_simple.py +++ b/src/tests/plugins/pretixdroid/test_simple.py @@ -172,6 +172,29 @@ def test_require_paid(client, env): jdata = json.loads(resp.content.decode("utf-8")) assert jdata['status'] == 'error' assert jdata['reason'] == 'unpaid' + assert jdata['data']['checkin_allowed'] is False + + +@pytest.mark.django_db +def test_check_in_pending(client, env): + AppConfiguration.objects.create(event=env[0], key='abcdefg', list=env[5]) + env[2].status = Order.STATUS_PENDING + env[2].save() + + env[5].include_pending = True + env[5].save() + + resp = client.post('/pretixdroid/api/%s/%s/redeem/?key=%s' % (env[0].organizer.slug, env[0].slug, 'abcdefg'), + data={'secret': '1234'}) + jdata = json.loads(resp.content.decode("utf-8")) + assert jdata['status'] == 'error' + assert jdata['reason'] == 'unpaid' + assert jdata['data']['checkin_allowed'] is True + + resp = client.post('/pretixdroid/api/%s/%s/redeem/?key=%s' % (env[0].organizer.slug, env[0].slug, 'abcdefg'), + data={'secret': '1234', 'ignore_unpaid': 'true'}) + jdata = json.loads(resp.content.decode("utf-8")) + assert jdata['status'] == 'ok' @pytest.mark.django_db