forked from CGM_Public/pretix_original
Check-in API: Improve handling of unknown ticket codes
This commit is contained in:
@@ -618,8 +618,9 @@ Order position endpoints
|
|||||||
:<json boolean canceled_supported: When this parameter is set to ``true``, the response code ``canceled`` may be
|
:<json boolean canceled_supported: When this parameter is set to ``true``, the response code ``canceled`` may be
|
||||||
returned. Otherwise, canceled orders will return ``unpaid``.
|
returned. Otherwise, canceled orders will return ``unpaid``.
|
||||||
:<json datetime datetime: Specifies the datetime of the check-in. If not supplied, the current time will be used.
|
:<json datetime datetime: Specifies the datetime of the check-in. If not supplied, the current time will be used.
|
||||||
:<json boolean force: Specifies that the check-in should succeed regardless of previous check-ins or required
|
:<json boolean force: Specifies that the check-in should succeed regardless of revoked barcode, previous check-ins or required
|
||||||
questions that have not been filled. Defaults to ``false``.
|
questions that have not been filled. This is usually used to upload offline scans that already happened,
|
||||||
|
because there's no point in validating them since they happend whether they are valid or not. Defaults to ``false``.
|
||||||
:<json string type: Send ``"exit"`` for an exit and ``"entry"`` (default) for an entry.
|
:<json string type: Send ``"exit"`` for an exit and ``"entry"`` (default) for an entry.
|
||||||
:<json boolean ignore_unpaid: Specifies that the check-in should succeed even if the order is in pending state.
|
:<json boolean ignore_unpaid: Specifies that the check-in should succeed even if the order is in pending state.
|
||||||
Defaults to ``false`` and only works when ``include_pending`` is set on the check-in
|
Defaults to ``false`` and only works when ``include_pending`` is set on the check-in
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ from django.utils.functional import cached_property
|
|||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
|
from packaging.version import parse
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.fields import DateTimeField
|
from rest_framework.fields import DateTimeField
|
||||||
@@ -421,6 +422,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
nonce=nonce,
|
nonce=nonce,
|
||||||
forced=force,
|
forced=force,
|
||||||
)
|
)
|
||||||
|
raw_barcode_for_checkin = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
queryset = self.get_queryset(ignore_status=True, ignore_products=True)
|
queryset = self.get_queryset(ignore_status=True, ignore_products=True)
|
||||||
@@ -455,7 +457,41 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
error_reason=Checkin.REASON_INVALID,
|
error_reason=Checkin.REASON_INVALID,
|
||||||
**common_checkin_args,
|
**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:
|
else:
|
||||||
op = revoked_matches[0].position
|
op = revoked_matches[0].position
|
||||||
op.order.log_action('pretix.event.checkin.revoked', data={
|
op.order.log_action('pretix.event.checkin.revoked', data={
|
||||||
@@ -506,7 +542,8 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
auth=self.request.auth,
|
auth=self.request.auth,
|
||||||
type=type,
|
type=type,
|
||||||
raw_barcode=None,
|
raw_barcode=raw_barcode_for_checkin,
|
||||||
|
from_revoked_secret=True,
|
||||||
)
|
)
|
||||||
except RequiredQuestionsError as e:
|
except RequiredQuestionsError as e:
|
||||||
return Response({
|
return Response({
|
||||||
|
|||||||
@@ -567,7 +567,7 @@ def _save_answers(op, answers, given_answers):
|
|||||||
def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict, force=False,
|
def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict, force=False,
|
||||||
ignore_unpaid=False, nonce=None, datetime=None, questions_supported=True,
|
ignore_unpaid=False, nonce=None, datetime=None, questions_supported=True,
|
||||||
user=None, auth=None, canceled_supported=False, type=Checkin.TYPE_ENTRY,
|
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
|
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.
|
not valid at this time.
|
||||||
@@ -669,7 +669,7 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
|||||||
device=device,
|
device=device,
|
||||||
gate=device.gate if device else None,
|
gate=device.gate if device else None,
|
||||||
nonce=nonce,
|
nonce=nonce,
|
||||||
forced=force and not entry_allowed,
|
forced=force and (not entry_allowed or from_revoked_secret),
|
||||||
raw_barcode=raw_barcode,
|
raw_barcode=raw_barcode,
|
||||||
)
|
)
|
||||||
op.order.log_action('pretix.event.checkin', data={
|
op.order.log_action('pretix.event.checkin', data={
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ def validate_plan_change(event, subevent, plan):
|
|||||||
seat=OuterRef('pk'),
|
seat=OuterRef('pk'),
|
||||||
canceled=False,
|
canceled=False,
|
||||||
).exclude(
|
).exclude(
|
||||||
order__status=Order.STATUS_CANCELED
|
order__status=(Order.STATUS_CANCELED, Order.STATUS_EXPIRED)
|
||||||
))
|
))
|
||||||
).annotate(has_v=Count('vouchers')).filter(
|
).annotate(has_v=Count('vouchers')).filter(
|
||||||
subevent=subevent,
|
subevent=subevent,
|
||||||
|
|||||||
@@ -1106,3 +1106,81 @@ def test_store_failed(token_client, organizer, clist, event, order):
|
|||||||
'error_reason': 'unknown'
|
'error_reason': 'unknown'
|
||||||
}, format='json')
|
}, format='json')
|
||||||
assert resp.status_code == 400
|
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()
|
||||||
|
|||||||
Reference in New Issue
Block a user