Check-in API: Improve handling of unknown ticket codes

This commit is contained in:
Raphael Michel
2021-07-23 10:49:33 +02:00
parent 78f4f35ca3
commit 4655d8237f
5 changed files with 123 additions and 7 deletions

View File

@@ -32,6 +32,7 @@ from django.utils.functional import cached_property
from django.utils.timezone import now
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_scopes import scopes_disabled
from packaging.version import parse
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.fields import DateTimeField
@@ -421,6 +422,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
nonce=nonce,
forced=force,
)
raw_barcode_for_checkin = None
try:
queryset = self.get_queryset(ignore_status=True, ignore_products=True)
@@ -455,7 +457,41 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
error_reason=Checkin.REASON_INVALID,
**common_checkin_args,
)
raise Http404()
if force and isinstance(self.request.auth, Device):
# There was a bug in libpretixsync: If you scanned a ticket in offline mode that was
# valid at the time but no longer exists at time of upload, the device would retry to
# upload the same scan over and over again. Since we can't update all devices quickly,
# here's a dirty workaround to make it stop.
try:
brand = self.request.auth.software_brand
ver = parse(self.request.auth.software_version)
legacy_mode = (
(brand == 'pretixSCANPROXY' and ver < parse('0.0.3')) or
(brand == 'pretixSCAN Android' and ver < parse('1.11.2')) or
(brand == 'pretixSCAN' and ver < parse('1.11.2'))
)
if legacy_mode:
return Response({
'status': 'error',
'reason': Checkin.REASON_ALREADY_REDEEMED,
'reason_explanation': None,
'require_attention': False,
'__warning': 'Compatibility hack active due to detected old pretixSCAN version',
}, status=400)
except: # we don't care e.g. about invalid version numbers
pass
return Response({
'detail': 'Not found.', # for backwards compatibility
'status': 'error',
'reason': Checkin.REASON_INVALID,
'reason_explanation': None,
'require_attention': False,
}, status=404)
elif revoked_matches and force:
op = revoked_matches[0].position
raw_barcode_for_checkin = self.kwargs['pk']
else:
op = revoked_matches[0].position
op.order.log_action('pretix.event.checkin.revoked', data={
@@ -506,7 +542,8 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
user=self.request.user,
auth=self.request.auth,
type=type,
raw_barcode=None,
raw_barcode=raw_barcode_for_checkin,
from_revoked_secret=True,
)
except RequiredQuestionsError as e:
return Response({

View File

@@ -567,7 +567,7 @@ def _save_answers(op, answers, given_answers):
def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict, force=False,
ignore_unpaid=False, nonce=None, datetime=None, questions_supported=True,
user=None, auth=None, canceled_supported=False, type=Checkin.TYPE_ENTRY,
raw_barcode=None):
raw_barcode=None, from_revoked_secret=False):
"""
Create a checkin for this particular order position and check-in list. Fails with CheckInError if the check in is
not valid at this time.
@@ -669,7 +669,7 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
device=device,
gate=device.gate if device else None,
nonce=nonce,
forced=force and not entry_allowed,
forced=force and (not entry_allowed or from_revoked_secret),
raw_barcode=raw_barcode,
)
op.order.log_action('pretix.event.checkin', data={

View File

@@ -45,7 +45,7 @@ def validate_plan_change(event, subevent, plan):
seat=OuterRef('pk'),
canceled=False,
).exclude(
order__status=Order.STATUS_CANCELED
order__status=(Order.STATUS_CANCELED, Order.STATUS_EXPIRED)
))
).annotate(has_v=Count('vouchers')).filter(
subevent=subevent,

View File

@@ -1106,3 +1106,81 @@ def test_store_failed(token_client, organizer, clist, event, order):
'error_reason': 'unknown'
}, format='json')
assert resp.status_code == 400
@pytest.mark.django_db
def test_redeem_unknown(token_client, organizer, clist, event, order):
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
organizer.slug, event.slug, clist.pk, 'unknown_secret'
), {
'force': True
}, format='json')
assert resp.status_code == 404
assert resp.data["status"] == "error"
assert resp.data["reason"] == "invalid"
with scopes_disabled():
assert not Checkin.objects.last()
@pytest.mark.django_db
def test_redeem_unknown_revoked(token_client, organizer, clist, event, order):
with scopes_disabled():
p = order.positions.first()
event.revoked_secrets.create(position=p, secret='revoked_secret')
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
organizer.slug, event.slug, clist.pk, 'revoked_secret'
), {
}, format='json')
assert resp.status_code == 400
assert resp.data["status"] == "error"
assert resp.data["reason"] == "revoked"
with scopes_disabled():
assert not Checkin.objects.last()
@pytest.mark.django_db
def test_redeem_unknown_revoked_force(token_client, organizer, clist, event, order):
with scopes_disabled():
p = order.positions.first()
event.revoked_secrets.create(position=p, secret='revoked_secret')
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():
assert Checkin.objects.last().forced
@pytest.mark.django_db
def test_redeem_unknown_legacy_device_bug(device, device_client, organizer, clist, event, order):
device.software_brand = "pretixSCAN"
device.software_version = "1.11.1"
device.save()
resp = device_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
organizer.slug, event.slug, clist.pk, 'unknown_secret'
), {
'force': True
}, format='json')
print(resp.data)
assert resp.status_code == 400
assert resp.data["status"] == "error"
assert resp.data["reason"] == "already_redeemed"
with scopes_disabled():
assert not Checkin.objects.last()
device.software_brand = "pretixSCAN"
device.software_version = "1.11.2"
device.save()
resp = device_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
organizer.slug, event.slug, clist.pk, 'unknown_secret'
), {
'force': True
}, format='json')
assert resp.status_code == 404
assert resp.data["status"] == "error"
assert resp.data["reason"] == "invalid"
with scopes_disabled():
assert not Checkin.objects.last()