mirror of
https://github.com/pretix/pretix.git
synced 2026-08-07 10:17:49 +00:00
Allow check-in lists to include unpaid orders
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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_<question>=<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
|
||||
},
|
||||
...
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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')),
|
||||
|
||||
@@ -36,7 +36,8 @@ class CheckinListForm(forms.ModelForm):
|
||||
'name',
|
||||
'all_products',
|
||||
'limit_products',
|
||||
'subevent'
|
||||
'subevent',
|
||||
'include_pending'
|
||||
]
|
||||
widgets = {
|
||||
'limit_products': forms.CheckboxSelectMultiple(attrs={
|
||||
|
||||
@@ -86,6 +86,9 @@
|
||||
</td>
|
||||
<td>
|
||||
<strong><a href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=e.order.code %}">{{ e.order.code }}</a></strong>
|
||||
{% if e.order.status == "n" %}
|
||||
<span class="label label-warning">{% trans "unpaid" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ e.item.name }}{% if e.variation %} – {{ e.variation }}{% endif %}</td>
|
||||
<td>{{ e.order.email }}</td>
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
{% if form.subevent %}
|
||||
{% bootstrap_field form.subevent layout="control" %}
|
||||
{% endif %}
|
||||
{% bootstrap_field form.include_pending layout="control" %}
|
||||
<legend>{% trans "Products" %}</legend>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -111,6 +111,7 @@ TEST_LIST_RES = {
|
||||
"limit_products": [],
|
||||
"position_count": 0,
|
||||
"checkin_count": 0,
|
||||
"include_pending": False,
|
||||
"subevent": None
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user