mirror of
https://github.com/pretix/pretix.git
synced 2026-05-06 15:24:02 +00:00
Add option to scan add-on based on its parent position's secret (#2705)
This commit is contained in:
@@ -37,7 +37,7 @@ class CheckinListSerializer(I18nAwareModelSerializer):
|
||||
model = CheckinList
|
||||
fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count',
|
||||
'include_pending', 'auto_checkin_sales_channels', 'allow_multiple_entries', 'allow_entry_after_exit',
|
||||
'rules', 'exit_all_at')
|
||||
'rules', 'exit_all_at', 'addon_match')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -25,6 +25,7 @@ from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.db.models import (
|
||||
Count, Exists, F, Max, OrderBy, OuterRef, Prefetch, Q, Subquery,
|
||||
prefetch_related_objects,
|
||||
)
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.http import Http404
|
||||
@@ -280,7 +281,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = CheckinListOrderPositionSerializer
|
||||
queryset = OrderPosition.all.none()
|
||||
filter_backends = (ExtendedBackend, RichOrderingFilter)
|
||||
ordering = ('attendee_name_cached', 'positionid')
|
||||
ordering = (F('attendee_name_cached').asc(nulls_last=True), 'positionid')
|
||||
ordering_fields = (
|
||||
'order__code', 'order__datetime', 'positionid', 'attendee_name',
|
||||
'last_checked_in', 'order__email',
|
||||
@@ -408,12 +409,14 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
raise ValidationError("Invalid check-in type.")
|
||||
ignore_unpaid = bool(self.request.data.get('ignore_unpaid', False))
|
||||
nonce = self.request.data.get('nonce')
|
||||
|
||||
untrusted_input = (
|
||||
self.request.GET.get('untrusted_input', '') not in ('0', 'false', 'False', '')
|
||||
or (isinstance(self.request.auth, Device) and 'pretixscan' in (self.request.auth.software_brand or '').lower())
|
||||
)
|
||||
|
||||
if not self.checkinlist.all_products:
|
||||
prefetch_related_objects([self.checkinlist], 'limit_products')
|
||||
|
||||
if 'datetime' in self.request.data:
|
||||
dt = DateTimeField().to_internal_value(self.request.data.get('datetime'))
|
||||
else:
|
||||
@@ -432,19 +435,32 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
raw_barcode_for_checkin = None
|
||||
from_revoked_secret = False
|
||||
|
||||
try:
|
||||
queryset = self.get_queryset(ignore_status=True, ignore_products=True)
|
||||
if self.kwargs['pk'].isnumeric() and not untrusted_input:
|
||||
op = queryset.get(Q(pk=self.kwargs['pk']) | Q(secret=self.kwargs['pk']))
|
||||
else:
|
||||
# In application/x-www-form-urlencoded, you can encodes space ' ' with '+' instead of '%20'.
|
||||
# `id`, however, is part of a path where this technically is not allowed. Old versions of our
|
||||
# scan apps still do it, so we try work around it!
|
||||
try:
|
||||
op = queryset.get(secret=self.kwargs['pk'])
|
||||
except OrderPosition.DoesNotExist:
|
||||
op = queryset.get(secret=self.kwargs['pk'].replace('+', ' '))
|
||||
except OrderPosition.DoesNotExist:
|
||||
# 1. Gather a list of positions that could be the one we looking fore, either from their ID, secret or
|
||||
# parent secret
|
||||
queryset = self.get_queryset(ignore_status=True, ignore_products=True).order_by(
|
||||
F('addon_to').asc(nulls_first=True)
|
||||
)
|
||||
|
||||
q = Q(secret=self.kwargs['pk'])
|
||||
if self.checkinlist.addon_match:
|
||||
q |= Q(addon_to__secret=self.kwargs['pk'])
|
||||
if self.kwargs['pk'].isnumeric() and not untrusted_input:
|
||||
q |= Q(pk=self.kwargs['pk'])
|
||||
|
||||
op_candidates = list(queryset.filter(q))
|
||||
if not op_candidates and '+' in self.kwargs['pk']:
|
||||
# In application/x-www-form-urlencoded, you can encodes space ' ' with '+' instead of '%20'.
|
||||
# `id`, however, is part of a path where this technically is not allowed. Old versions of our
|
||||
# scan apps still do it, so we try work around it!
|
||||
q = Q(secret=self.kwargs['pk'].replace('+', ' '))
|
||||
if self.checkinlist.addon_match:
|
||||
q |= Q(addon_to__secret=self.kwargs['pk'].replace('+', ' '))
|
||||
op_candidates = list(queryset.filter(q))
|
||||
|
||||
# 2. Handle the "nothing found" case: Either it's really a bogus secret that we don't know (-> error), or it
|
||||
# might be a revoked one that we actually know (-> error, but with better error message and logging and
|
||||
# with respecting the force option).
|
||||
if not op_candidates:
|
||||
revoked_matches = list(self.request.event.revoked_secrets.filter(secret=self.kwargs['pk']))
|
||||
if len(revoked_matches) == 0:
|
||||
self.request.event.log_action('pretix.event.checkin.unknown', data={
|
||||
@@ -504,7 +520,9 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
'require_attention': False,
|
||||
}, status=404)
|
||||
elif revoked_matches and force:
|
||||
op = revoked_matches[0].position
|
||||
op_candidates = [revoked_matches[0].position]
|
||||
if self.checkinlist.addon_match:
|
||||
op_candidates += list(revoked_matches[0].position.addons.all())
|
||||
raw_barcode_for_checkin = self.kwargs['pk']
|
||||
from_revoked_secret = True
|
||||
else:
|
||||
@@ -529,6 +547,56 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
'position': CheckinListOrderPositionSerializer(op, context=self.get_serializer_context()).data
|
||||
}, status=400)
|
||||
|
||||
# 3. Handle the "multiple options found" case: Except for the unlikely case of a secret being also a valid primary
|
||||
# key on the same list, we're probably dealing with the ``addon_match`` case here and need to figure out
|
||||
# which add-on has the right product.
|
||||
if len(op_candidates) > 1:
|
||||
if self.checkinlist.addon_match and not self.checkinlist.all_products:
|
||||
op_candidates_matching_product = [
|
||||
op for op in op_candidates if op.item_id in {i.pk for i in self.checkinlist.limit_products.all()}
|
||||
]
|
||||
else:
|
||||
op_candidates_matching_product = op_candidates
|
||||
if len(op_candidates_matching_product) == 0:
|
||||
# None of the found add-ons has the correct product, too bad! We could just error out here, but
|
||||
# instead we just continue with *any* product and have it rejected by the check in perform_checkin.
|
||||
# This has the advantage of a better error message.
|
||||
op_candidates = [op_candidates[0]]
|
||||
elif len(op_candidates_matching_product) > 1:
|
||||
# It's still ambiguous, we'll error out.
|
||||
# We choose the first match (regardless of product) for the logging since it's most likely to be the
|
||||
# base product according to our order_by above.
|
||||
op = op_candidates[0]
|
||||
op.order.log_action('pretix.event.checkin.denied', data={
|
||||
'position': op.id,
|
||||
'positionid': op.positionid,
|
||||
'errorcode': Checkin.REASON_AMBIGUOUS,
|
||||
'reason_explanation': None,
|
||||
'force': force,
|
||||
'datetime': dt,
|
||||
'type': type,
|
||||
'list': self.checkinlist.pk
|
||||
}, user=self.request.user, auth=self.request.auth)
|
||||
Checkin.objects.create(
|
||||
position=op,
|
||||
successful=False,
|
||||
error_reason=Checkin.REASON_AMBIGUOUS,
|
||||
error_explanation=None,
|
||||
**common_checkin_args,
|
||||
)
|
||||
return Response({
|
||||
'status': 'error',
|
||||
'reason': Checkin.REASON_AMBIGUOUS,
|
||||
'reason_explanation': None,
|
||||
'require_attention': op.item.checkin_attention or op.order.checkin_attention,
|
||||
'position': CheckinListOrderPositionSerializer(op, context=self.get_serializer_context()).data
|
||||
}, status=400)
|
||||
else:
|
||||
op_candidates = op_candidates_matching_product
|
||||
|
||||
op = op_candidates[0]
|
||||
|
||||
# 5. Pre-validate all incoming answers, handle file upload
|
||||
given_answers = {}
|
||||
if 'answers' in self.request.data:
|
||||
aws = self.request.data.get('answers')
|
||||
@@ -542,6 +610,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
except ValidationError:
|
||||
pass
|
||||
|
||||
# 6. Pass to our actual check-in logic
|
||||
with language(self.request.event.settings.locale):
|
||||
try:
|
||||
perform_checkin(
|
||||
|
||||
18
src/pretix/base/migrations/0218_checkinlist_addon_match.py
Normal file
18
src/pretix/base/migrations/0218_checkinlist_addon_match.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.2 on 2022-06-29 17:37
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0217_eventfooterlink_organizerfooterlink'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='checkinlist',
|
||||
name='addon_match',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -56,6 +56,12 @@ class CheckinList(LoggedModel):
|
||||
default=False,
|
||||
help_text=_('With this option, people will be able to check in even if the '
|
||||
'order has not been paid.'))
|
||||
addon_match = models.BooleanField(
|
||||
verbose_name=_('Allow checking in add-on tickets by scanning the main ticket'),
|
||||
default=False,
|
||||
help_text=_('A scan will only be possible if the check-in list is configured such that there is always exactly '
|
||||
'one matching add-on ticket. Ambiguous scans will be rejected..')
|
||||
)
|
||||
gates = models.ManyToManyField(
|
||||
'Gate', verbose_name=_("Gates"), blank=True,
|
||||
help_text=_("Does not have any effect for the validation of tickets, only for the automatic configuration of "
|
||||
@@ -258,6 +264,7 @@ class Checkin(models.Model):
|
||||
REASON_REVOKED = 'revoked'
|
||||
REASON_INCOMPLETE = 'incomplete'
|
||||
REASON_ALREADY_REDEEMED = 'already_redeemed'
|
||||
REASON_AMBIGUOUS = 'ambiguous'
|
||||
REASON_ERROR = 'error'
|
||||
REASONS = (
|
||||
(REASON_CANCELED, _('Order canceled')),
|
||||
@@ -268,6 +275,7 @@ class Checkin(models.Model):
|
||||
(REASON_INCOMPLETE, _('Information required')),
|
||||
(REASON_ALREADY_REDEEMED, _('Ticket already used')),
|
||||
(REASON_PRODUCT, _('Ticket type not allowed here')),
|
||||
(REASON_AMBIGUOUS, _('Ticket code is ambiguous on list')),
|
||||
(REASON_ERROR, _('Server error')),
|
||||
)
|
||||
|
||||
|
||||
@@ -22,9 +22,10 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import get_current_timezone, make_aware, now
|
||||
from django.utils.translation import pgettext_lazy
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from django_scopes.forms import (
|
||||
SafeModelChoiceField, SafeModelMultipleChoiceField,
|
||||
)
|
||||
@@ -109,6 +110,7 @@ class CheckinListForm(forms.ModelForm):
|
||||
'rules',
|
||||
'gates',
|
||||
'exit_all_at',
|
||||
'addon_match',
|
||||
]
|
||||
widgets = {
|
||||
'limit_products': forms.CheckboxSelectMultiple(attrs={
|
||||
@@ -130,6 +132,12 @@ class CheckinListForm(forms.ModelForm):
|
||||
def clean(self):
|
||||
d = super().clean()
|
||||
d['rules'] = CheckinList.validate_rules(d.get('rules'))
|
||||
|
||||
if d.get('addon_match') and d.get('all_products'):
|
||||
raise ValidationError(_('If you allow checking in add-on tickets by scanning the main ticket, you must '
|
||||
'select a specific set of products for this check-in list, only including the '
|
||||
'possible add-on products.'))
|
||||
|
||||
return d
|
||||
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
|
||||
{% bootstrap_field form.allow_multiple_entries layout="control" %}
|
||||
{% bootstrap_field form.allow_entry_after_exit layout="control" %}
|
||||
{% bootstrap_field form.addon_match layout="control" %}
|
||||
{% bootstrap_field form.exit_all_at layout="control" %}
|
||||
{% bootstrap_field form.auto_checkin_sales_channels layout="control" %}
|
||||
{% if form.gates %}
|
||||
|
||||
@@ -59,6 +59,7 @@ window.vapp = new Vue({
|
||||
'result.rules': gettext('Entry not allowed'),
|
||||
'result.revoked': gettext('Ticket code revoked/changed'),
|
||||
'result.canceled': gettext('Order canceled'),
|
||||
'result.ambiguous': gettext('Ticket code is ambiguous on list'),
|
||||
'status.checkin': gettext('Checked-in Tickets'),
|
||||
'status.position': gettext('Valid Tickets'),
|
||||
'status.inside': gettext('Currently inside'),
|
||||
|
||||
@@ -70,7 +70,7 @@ def order(event, item, other_item, taxrule):
|
||||
total=46, locale='en'
|
||||
)
|
||||
InvoiceAddress.objects.create(order=o, company="Sample company", country=Country('NZ'))
|
||||
OrderPosition.objects.create(
|
||||
op1 = OrderPosition.objects.create(
|
||||
order=o,
|
||||
positionid=1,
|
||||
item=item,
|
||||
@@ -90,6 +90,16 @@ def order(event, item, other_item, taxrule):
|
||||
secret="sf4HZG73fU6kwddgjg2QOusFbYZwVKpK",
|
||||
pseudonymization_id="BACDEFGHKL",
|
||||
)
|
||||
OrderPosition.objects.create(
|
||||
order=o,
|
||||
positionid=3,
|
||||
item=other_item,
|
||||
addon_to=op1,
|
||||
variation=None,
|
||||
price=Decimal("0"),
|
||||
secret="3u4ez6vrrbgb3wvezxhq446p548dt2wn",
|
||||
pseudonymization_id="FOOBAR12345",
|
||||
)
|
||||
return o
|
||||
|
||||
|
||||
@@ -157,6 +167,38 @@ TEST_ORDERPOSITION2_RES = {
|
||||
"pseudonymization_id": "BACDEFGHKL",
|
||||
}
|
||||
|
||||
TEST_ORDERPOSITION3_RES = {
|
||||
"id": 3,
|
||||
"require_attention": False,
|
||||
"order__status": "p",
|
||||
"order": "FOO",
|
||||
"positionid": 3,
|
||||
"item": 1,
|
||||
"variation": None,
|
||||
"price": "0.00",
|
||||
"attendee_name": "Peter",
|
||||
"attendee_name_parts": {'full_name': "Peter"},
|
||||
"attendee_email": None,
|
||||
"voucher": None,
|
||||
"tax_rate": "0.00",
|
||||
"tax_value": "0.00",
|
||||
"tax_rule": None,
|
||||
"secret": "3u4ez6vrrbgb3wvezxhq446p548dt2wn",
|
||||
"addon_to": None,
|
||||
"checkins": [],
|
||||
"downloads": [],
|
||||
"answers": [],
|
||||
"seat": None,
|
||||
"company": None,
|
||||
"street": None,
|
||||
"zipcode": None,
|
||||
"city": None,
|
||||
"country": None,
|
||||
"state": None,
|
||||
"subevent": None,
|
||||
"pseudonymization_id": "FOOBAR12345",
|
||||
}
|
||||
|
||||
TEST_LIST_RES = {
|
||||
"name": "Default",
|
||||
"all_products": False,
|
||||
@@ -168,6 +210,7 @@ TEST_LIST_RES = {
|
||||
"allow_entry_after_exit": True,
|
||||
"subevent": None,
|
||||
"exit_all_at": None,
|
||||
"addon_match": False,
|
||||
"rules": {}
|
||||
}
|
||||
|
||||
@@ -396,27 +439,31 @@ def test_list_update(token_client, organizer, event, clist):
|
||||
def test_list_all_items_positions(token_client, organizer, event, clist, clist_all, item, other_item, order):
|
||||
with scopes_disabled():
|
||||
p1 = dict(TEST_ORDERPOSITION1_RES)
|
||||
p1["id"] = order.positions.first().pk
|
||||
p1["id"] = order.positions.get(positionid=1).pk
|
||||
p1["item"] = item.pk
|
||||
p2 = dict(TEST_ORDERPOSITION2_RES)
|
||||
p2["id"] = order.positions.last().pk
|
||||
p2["id"] = order.positions.get(positionid=2).pk
|
||||
p2["item"] = other_item.pk
|
||||
p3 = dict(TEST_ORDERPOSITION3_RES)
|
||||
p3["id"] = order.positions.get(positionid=3).pk
|
||||
p3["item"] = other_item.pk
|
||||
p3["addon_to"] = p1["id"]
|
||||
|
||||
# All items
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=positionid'.format(
|
||||
organizer.slug, event.slug, clist_all.pk
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert [p1, p2] == resp.data['results']
|
||||
assert [p1, p2, p3] == resp.data['results']
|
||||
|
||||
# Check-ins on other list ignored
|
||||
with scopes_disabled():
|
||||
order.positions.first().checkins.create(list=clist)
|
||||
c = order.positions.get(positionid=1).checkins.create(list=clist)
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=positionid'.format(
|
||||
organizer.slug, event.slug, clist_all.pk
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert [p1, p2] == resp.data['results']
|
||||
assert [p1, p2, p3] == resp.data['results']
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?has_checkin=1'.format(
|
||||
organizer.slug, event.slug, clist_all.pk
|
||||
))
|
||||
@@ -425,7 +472,7 @@ def test_list_all_items_positions(token_client, organizer, event, clist, clist_a
|
||||
|
||||
# Only checked in
|
||||
with scopes_disabled():
|
||||
c = order.positions.first().checkins.create(list=clist_all)
|
||||
c = order.positions.get(positionid=1).checkins.create(list=clist_all)
|
||||
p1['checkins'] = [
|
||||
{
|
||||
'id': c.pk,
|
||||
@@ -448,7 +495,7 @@ def test_list_all_items_positions(token_client, organizer, event, clist, clist_a
|
||||
organizer.slug, event.slug, clist_all.pk
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert [p2] == resp.data['results']
|
||||
assert [p2, p3] == resp.data['results']
|
||||
|
||||
# Order by checkin
|
||||
resp = token_client.get(
|
||||
@@ -456,18 +503,18 @@ def test_list_all_items_positions(token_client, organizer, event, clist, clist_a
|
||||
organizer.slug, event.slug, clist_all.pk
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert [p1, p2] == resp.data['results']
|
||||
assert resp.data['results'][0] == p1
|
||||
resp = token_client.get(
|
||||
'/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=last_checked_in'.format(
|
||||
organizer.slug, event.slug, clist_all.pk
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert [p2, p1] == resp.data['results']
|
||||
assert resp.data['results'][-1] == p1
|
||||
|
||||
# Order by checkin date
|
||||
time.sleep(1)
|
||||
with scopes_disabled():
|
||||
c = order.positions.last().checkins.create(list=clist_all)
|
||||
c = order.positions.get(positionid=2).checkins.create(list=clist_all)
|
||||
p2['checkins'] = [
|
||||
{
|
||||
'id': c.pk,
|
||||
@@ -480,23 +527,23 @@ def test_list_all_items_positions(token_client, organizer, event, clist, clist_a
|
||||
}
|
||||
]
|
||||
resp = token_client.get(
|
||||
'/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=-last_checked_in'.format(
|
||||
'/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=-last_checked_in,positionid'.format(
|
||||
organizer.slug, event.slug, clist_all.pk
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert [p2, p1] == resp.data['results']
|
||||
assert [p2, p1, p3] == resp.data['results']
|
||||
|
||||
# Order by attendee_name
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=-attendee_name'.format(
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=-attendee_name,positionid'.format(
|
||||
organizer.slug, event.slug, clist_all.pk
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert [p1, p2] == resp.data['results']
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=attendee_name'.format(
|
||||
assert [p1, p3, p2] == resp.data['results']
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=attendee_name,positionid'.format(
|
||||
organizer.slug, event.slug, clist_all.pk
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert [p2, p1] == resp.data['results']
|
||||
assert [p2, p1, p3] == resp.data['results']
|
||||
|
||||
# Paid only
|
||||
order.status = Order.STATUS_PENDING
|
||||
@@ -513,32 +560,41 @@ def test_list_all_items_positions(token_client, organizer, event, clist, clist_a
|
||||
assert resp.status_code == 200
|
||||
p1['order__status'] = 'n'
|
||||
p2['order__status'] = 'n'
|
||||
assert [p2, p1] == resp.data['results']
|
||||
p3['order__status'] = 'n'
|
||||
assert [p2, p1, p3] == resp.data['results']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_list_all_items_positions_by_subevent(token_client, organizer, event, clist, clist_all, item, other_item, order, subevent):
|
||||
with scopes_disabled():
|
||||
se2 = event.subevents.create(name="Foobar", date_from=datetime.datetime(2017, 12, 27, 10, 0, 0, tzinfo=UTC))
|
||||
pfirst = order.positions.first()
|
||||
pfirst = order.positions.get(positionid=1)
|
||||
pfirst.subevent = se2
|
||||
pfirst.save()
|
||||
p1 = dict(TEST_ORDERPOSITION1_RES)
|
||||
p1["id"] = pfirst.pk
|
||||
p1["subevent"] = se2.pk
|
||||
p1["item"] = item.pk
|
||||
plast = order.positions.last()
|
||||
plast.subevent = subevent
|
||||
plast.save()
|
||||
psecond = order.positions.get(positionid=2)
|
||||
psecond.subevent = subevent
|
||||
psecond.save()
|
||||
p2 = dict(TEST_ORDERPOSITION2_RES)
|
||||
p2["id"] = plast.pk
|
||||
p2["id"] = psecond.pk
|
||||
p2["item"] = other_item.pk
|
||||
p2["subevent"] = subevent.pk
|
||||
pthird = order.positions.get(positionid=3)
|
||||
pthird.subevent = se2
|
||||
pthird.save()
|
||||
p3 = dict(TEST_ORDERPOSITION3_RES)
|
||||
p3["id"] = pthird.pk
|
||||
p3["addon_to"] = pfirst.pk
|
||||
p3["item"] = other_item.pk
|
||||
p3["subevent"] = se2.pk
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=positionid'.format(
|
||||
organizer.slug, event.slug, clist_all.pk
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert [p1, p2] == resp.data['results']
|
||||
assert [p1, p2, p3] == resp.data['results']
|
||||
|
||||
clist_all.subevent = subevent
|
||||
clist_all.save()
|
||||
@@ -593,7 +649,7 @@ def test_status(token_client, organizer, event, clist_all, item, other_item, ord
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert resp.data['checkin_count'] == 1
|
||||
assert resp.data['position_count'] == 2
|
||||
assert resp.data['position_count'] == 3
|
||||
assert resp.data['inside_count'] == 1
|
||||
assert resp.data['items'] == [
|
||||
{
|
||||
@@ -622,7 +678,7 @@ def test_status(token_client, organizer, event, clist_all, item, other_item, ord
|
||||
'id': other_item.pk,
|
||||
'checkin_count': 0,
|
||||
'admission': False,
|
||||
'position_count': 1,
|
||||
'position_count': 2,
|
||||
'variations': []
|
||||
}
|
||||
]
|
||||
@@ -1185,12 +1241,14 @@ def test_redeem_unknown_revoked_force(token_client, organizer, clist, event, ord
|
||||
assert resp.status_code == 201
|
||||
assert resp.data["status"] == "ok"
|
||||
with scopes_disabled():
|
||||
assert Checkin.objects.last().forced
|
||||
assert Checkin.objects.last().force_sent
|
||||
ci = Checkin.objects.last()
|
||||
assert ci.forced
|
||||
assert ci.force_sent
|
||||
assert ci.position == p
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_redeem_unknown_legacy_device_bug(device, device_client, organizer, clist, event, order):
|
||||
def test_redeem_unknown_legacy_device_bug(device, device_client, organizer, clist, event):
|
||||
device.software_brand = "pretixSCAN"
|
||||
device.software_version = "1.11.1"
|
||||
device.save()
|
||||
@@ -1232,7 +1290,6 @@ def test_redeem_by_id_not_allowed_if_pretixscan(device, device_client, organizer
|
||||
), {
|
||||
'force': True
|
||||
}, format='json')
|
||||
print(resp.data)
|
||||
assert resp.status_code == 404
|
||||
resp = device_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.secret
|
||||
@@ -1258,3 +1315,82 @@ def test_redeem_by_id_not_allowed_if_untrusted(device, device_client, organizer,
|
||||
'force': True
|
||||
}, format='json')
|
||||
assert resp.status_code == 201
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_redeem_addon_if_match_disabled(token_client, organizer, clist, other_item, event, order):
|
||||
with scopes_disabled():
|
||||
clist.all_products = False
|
||||
clist.save()
|
||||
clist.limit_products.set([other_item])
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, 'z3fsn8jyufm5kpk768q69gkbyr5f4h6w'
|
||||
), {
|
||||
}, format='json')
|
||||
assert resp.status_code == 400
|
||||
assert resp.data["status"] == "error"
|
||||
assert resp.data["reason"] == "product"
|
||||
with scopes_disabled():
|
||||
assert not Checkin.objects.last()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_redeem_addon_if_match_enabled(token_client, organizer, clist, other_item, event, order):
|
||||
with scopes_disabled():
|
||||
clist.all_products = False
|
||||
clist.addon_match = True
|
||||
clist.save()
|
||||
clist.limit_products.set([other_item])
|
||||
p = order.positions.first().addons.all().first()
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, 'z3fsn8jyufm5kpk768q69gkbyr5f4h6w'
|
||||
), {
|
||||
}, format='json')
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
assert resp.data['position']['attendee_name'] == 'Peter' # test propagation of names
|
||||
assert resp.data['position']['item'] == other_item.pk
|
||||
with scopes_disabled():
|
||||
ci = Checkin.objects.last()
|
||||
assert ci.position == p
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_redeem_addon_if_match_ambiguous(token_client, organizer, clist, item, other_item, event, order):
|
||||
with scopes_disabled():
|
||||
clist.all_products = False
|
||||
clist.addon_match = True
|
||||
clist.save()
|
||||
clist.limit_products.set([item, other_item])
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, 'z3fsn8jyufm5kpk768q69gkbyr5f4h6w'
|
||||
), {
|
||||
}, format='json')
|
||||
assert resp.status_code == 400
|
||||
assert resp.data["status"] == "error"
|
||||
assert resp.data["reason"] == "ambiguous"
|
||||
with scopes_disabled():
|
||||
assert not Checkin.objects.last()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_redeem_addon_if_match_and_revoked_force(token_client, organizer, clist, other_item, event, order):
|
||||
with scopes_disabled():
|
||||
event.revoked_secrets.create(position=order.positions.get(positionid=1), secret='revoked_secret')
|
||||
clist.all_products = False
|
||||
clist.addon_match = True
|
||||
clist.save()
|
||||
clist.limit_products.set([other_item])
|
||||
p = order.positions.first().addons.all().first()
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, 'revoked_secret'
|
||||
), {
|
||||
'force': True
|
||||
}, format='json')
|
||||
assert resp.status_code == 201
|
||||
assert resp.data["status"] == "ok"
|
||||
with scopes_disabled():
|
||||
ci = Checkin.objects.last()
|
||||
assert ci.forced
|
||||
assert ci.force_sent
|
||||
assert ci.position == p
|
||||
|
||||
Reference in New Issue
Block a user