From fae35cc56f34f83a1be1df4e6432f7b0e8aa306e Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Thu, 3 Sep 2020 14:07:21 +0200 Subject: [PATCH] Improve error handling of check-in scans --- src/pretix/api/views/checkin.py | 38 ++++++++++++++++++--------- src/pretix/control/logdisplay.py | 45 +++++++++++++++++++++++++++++++- src/tests/api/test_checkin.py | 14 ++++++++++ 3 files changed, 84 insertions(+), 13 deletions(-) diff --git a/src/pretix/api/views/checkin.py b/src/pretix/api/views/checkin.py index cf51e209ef..5b96b51d79 100644 --- a/src/pretix/api/views/checkin.py +++ b/src/pretix/api/views/checkin.py @@ -200,7 +200,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet): except ValueError: raise Http404() - def get_queryset(self, ignore_status=False): + def get_queryset(self, ignore_status=False, ignore_products=False): cqs = Checkin.objects.filter( position_id=OuterRef('pk'), list_id=self.checkinlist.pk @@ -255,12 +255,12 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet): Prefetch('addons', OrderPosition.objects.select_related('item', 'variation')) ).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address', 'order', 'seat') - if not self.checkinlist.all_products: + if not self.checkinlist.all_products and not ignore_products: qs = qs.filter(item__in=self.checkinlist.limit_products.values_list('id', flat=True)) return qs - @action(detail=True, methods=['POST']) + @action(detail=False, methods=['POST'], url_name='redeem', url_path='(?P[^/]+)/redeem') def redeem(self, *args, **kwargs): force = bool(self.request.data.get('force', False)) type = self.request.data.get('type', None) or Checkin.TYPE_ENTRY @@ -268,13 +268,27 @@ 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') - op = self.get_object(ignore_status=True) if 'datetime' in self.request.data: dt = DateTimeField().to_internal_value(self.request.data.get('datetime')) else: dt = now() + try: + queryset = self.get_queryset(ignore_status=True, ignore_products=True) + if self.kwargs['pk'].isnumeric(): + op = queryset.get(Q(pk=self.kwargs['pk']) | Q(secret=self.kwargs['pk'])) + else: + op = queryset.get(secret=self.kwargs['pk']) + except OrderPosition.DoesNotExist: + self.request.event.log_action('pretix.event.checkin.unknown', data={ + 'datetime': dt, + 'type': type, + 'list': self.checkinlist.pk, + 'barcode': self.kwargs['pk'] + }, user=self.request.user, auth=self.request.auth) + raise Http404() + given_answers = {} if 'answers' in self.request.data: aws = self.request.data.get('answers') @@ -310,6 +324,14 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet): ] }, status=400) except CheckInError as e: + op.order.log_action('pretix.event.checkin.denied', data={ + 'position': op.id, + 'positionid': op.positionid, + 'errorcode': e.code, + 'datetime': dt, + 'type': type, + 'list': self.checkinlist.pk + }, user=self.request.user, auth=self.request.auth) return Response({ 'status': 'error', 'reason': e.code, @@ -322,11 +344,3 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet): 'require_attention': op.item.checkin_attention or op.order.checkin_attention, 'position': CheckinListOrderPositionSerializer(op, context=self.get_serializer_context()).data }, status=201) - - def get_object(self, ignore_status=False): - queryset = self.filter_queryset(self.get_queryset(ignore_status=ignore_status)) - if self.kwargs['pk'].isnumeric(): - obj = get_object_or_404(queryset, Q(pk=self.kwargs['pk']) | Q(secret=self.kwargs['pk'])) - else: - obj = get_object_or_404(queryset, secret=self.kwargs['pk']) - return obj diff --git a/src/pretix/control/logdisplay.py b/src/pretix/control/logdisplay.py index 52638e43a7..50c6f0aa2d 100644 --- a/src/pretix/control/logdisplay.py +++ b/src/pretix/control/logdisplay.py @@ -162,6 +162,49 @@ def _display_checkin(event, logentry): else: checkin_list = _("(unknown)") + if logentry.action_type == 'pretix.event.checkin.unknown': + if show_dt: + return _( + 'Unknown scan of code "{barcode}" at {datetime} for list "{list}", type "{type}".' + ).format( + posid=data.get('positionid'), + type=data.get('type'), + barcode=data.get('barcode'), + datetime=dt_formatted, + list=checkin_list + ) + else: + return _( + 'Unknown scan of code "{barcode}" for list "{list}", type "{type}".' + ).format( + posid=data.get('positionid'), + type=data.get('type'), + barcode=data.get('barcode'), + list=checkin_list + ) + + if logentry.action_type == 'pretix.event.checkin.denied': + if show_dt: + return _( + 'Denied scan of position #{posid} at {datetime} for list "{list}", type "{type}", ' + 'error code "{errorcode}".' + ).format( + posid=data.get('positionid'), + type=data.get('type'), + errorcode=data.get('errorcode'), + datetime=dt_formatted, + list=checkin_list + ) + else: + return _( + 'Denied scan of position #{posid} for list "{list}", type "{type}", error code "{errorcode}".' + ).format( + posid=data.get('positionid'), + type=data.get('type'), + errorcode=data.get('errorcode'), + list=checkin_list + ) + if data.get('type') == Checkin.TYPE_EXIT: if show_dt: return _('Position #{posid} has been checked out at {datetime} for list "{list}".').format( @@ -397,7 +440,7 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs): bleach.clean(logentry.parsed_data.get('msg'), tags=[], strip=True) ) - if logentry.action_type == 'pretix.event.checkin': + if logentry.action_type.startswith('pretix.event.checkin'): return _display_checkin(sender, logentry) if logentry.action_type == 'pretix.control.views.checkin': diff --git a/src/tests/api/test_checkin.py b/src/tests/api/test_checkin.py index c9868951d1..3662095197 100644 --- a/src/tests/api/test_checkin.py +++ b/src/tests/api/test_checkin.py @@ -746,6 +746,20 @@ def test_forced_multiple(token_client, organizer, clist, event, order): assert resp.data['status'] == 'ok' +@pytest.mark.django_db +def test_require_product(token_client, organizer, clist, event, order): + with scopes_disabled(): + clist.limit_products.clear() + p = order.positions.first() + + resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format( + organizer.slug, event.slug, clist.pk, p.pk + ), {}, format='json') + assert resp.status_code == 400 + assert resp.data['status'] == 'error' + assert resp.data['reason'] == 'product' + + @pytest.mark.django_db def test_require_paid(token_client, organizer, clist, event, order): with scopes_disabled():