diff --git a/doc/api/resources/checkin.rst b/doc/api/resources/checkin.rst index 89ab1cff1..479fa28a3 100644 --- a/doc/api/resources/checkin.rst +++ b/doc/api/resources/checkin.rst @@ -421,3 +421,94 @@ Annulment of a check-in :statuscode 401: Authentication failure :statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource. :statuscode 404: The requested nonce does not exist. + + +Check-in history +---------------- + +.. rst-class:: rest-resource-table + +===================================== ========================== ======================================================= +Field Type Description +===================================== ========================== ======================================================= +id integer Internal ID of the check-in +successful boolean Whether the check-in was successful +error_reason string Category of reason why the check-in was unsuccessful. Currently + ``"canceled"``, ``"invalid"``, ``"unpaid"`` ``"product"``, + ``"rules"``, ``"revoked"``, ``"incomplete"``, ``"already_redeemed"``, + ``"ambiguous"``, ``"error"``, ``"blocked"``, ``"unapproved"``, + ``"invalid_time"``, ``"annulled"`` or ``null`` +error_explanation string Additional, human-readable reason for the check-in to be unsuccessful (or ``null``) +position integer Internal ID of the order position (or ``null`` for unknown scans) +datetime datetime Logical time when the check-in happened +created datetime Time when the check-in appeared on the server +list integer Internal ID of the check-in list +auto_checked_in boolean Whether the check-in was performed by the system automatically +gate integer Internal ID of the gate (or ``null``) +device integer Internal ID of the device (or ``null``) +device_id integer Organizer-internal ID of the device (or ``null``) +type string Type of check-in, currently ``"entry"`` or ``"exit"`` +===================================== ========================== ======================================================= + +.. http:get:: /api/v1/organizers/(organizer)/events/(event)/checkins/ + + Returns a list of all check-in events within a given event. + + **Example request**: + + .. sourcecode:: http + + GET /api/v1/organizers/bigevents/events/sampleconf/checkins/ HTTP/1.1 + Host: pretix.eu + Accept: application/json, text/javascript + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: application/json + + { + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "id": 1, + "successful": true, + "error_reason": null, + "error_explanation": null, + "position": 1234, + "datetime": "2017-12-25T12:45:23Z", + "created": "2017-12-25T12:45:23Z", + "list": 2, + "auto_checked_in": false, + "gate": null, + "device": null, + "device_id": null, + "type": "entry", + } + ] + } + + :query integer page: The page number in case of a multi-page result set, default is 1 + :query datetime created_since: Only return check-ins that have been created since the given date (inclusive). + :query datetime created_before: Only return check-ins that have been created before the given date (exclusive). + :query datetime datetime_since: Only return check-ins that have happened since the given date (inclusive). + :query datetime datetime_before: Only return check-ins that have happened before the given date (exclusive). + :query boolean successful: Only return check-ins that have (not) been successful. + :query boolean error_reason: Only return check-ins with a specific error reason. + :query integer list: Only return check-ins from a specific list. + :query string type: Only return check-ins of a specific type. + :query integer gate: Only return check-ins from a specific gate. + :query integer device: Only return check-ins from a specific device. + :query boolean auto_checked_in: Only return check-ins that are (not) auto-checked in. + :query string ordering: Manually set the ordering of results. Valid fields to be used are ``datetime``, ``created``, + and ``id``. + :param organizer: The ``slug`` field of the organizer to fetch + :param event: The ``slug`` field of the event to fetch + :statuscode 200: no error + :statuscode 401: Authentication failure + :statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource. diff --git a/src/pretix/api/serializers/order.py b/src/pretix/api/serializers/order.py index b58754ed9..ab3ff47bf 100644 --- a/src/pretix/api/serializers/order.py +++ b/src/pretix/api/serializers/order.py @@ -325,7 +325,7 @@ class AnswerSerializer(I18nAwareModelSerializer): return data -class CheckinSerializer(I18nAwareModelSerializer): +class InlineCheckinSerializer(I18nAwareModelSerializer): device_id = serializers.SlugRelatedField( source='device', slug_field='device_id', @@ -337,6 +337,21 @@ class CheckinSerializer(I18nAwareModelSerializer): fields = ('id', 'datetime', 'list', 'auto_checked_in', 'gate', 'device', 'device_id', 'type') +class CheckinSerializer(I18nAwareModelSerializer): + device_id = serializers.SlugRelatedField( + source='device', + slug_field='device_id', + read_only=True, + ) + + class Meta: + model = Checkin + fields = ( + 'id', 'successful', 'error_reason', 'error_explanation', 'position', 'datetime', 'list', 'created', + 'auto_checked_in', 'gate', 'device', 'device_id', 'type' + ) + + class PrintLogSerializer(serializers.ModelSerializer): device_id = serializers.SlugRelatedField( source='device', @@ -560,7 +575,7 @@ class OrderPositionPluginDataField(serializers.Field): class OrderPositionSerializer(I18nAwareModelSerializer): - checkins = CheckinSerializer(many=True, read_only=True) + checkins = InlineCheckinSerializer(many=True, read_only=True) print_logs = PrintLogSerializer(many=True, read_only=True) answers = AnswerSerializer(many=True) downloads = PositionDownloadsField(source='*', read_only=True) diff --git a/src/pretix/api/urls.py b/src/pretix/api/urls.py index 15c654567..71b7a1e94 100644 --- a/src/pretix/api/urls.py +++ b/src/pretix/api/urls.py @@ -92,6 +92,7 @@ event_router.register(r'taxrules', event.TaxRuleViewSet) event_router.register(r'seats', event.SeatViewSet) event_router.register(r'waitinglistentries', waitinglist.WaitingListViewSet) event_router.register(r'checkinlists', checkin.CheckinListViewSet) +event_router.register(r'checkins', checkin.CheckinViewSet) event_router.register(r'cartpositions', cart.CartPositionViewSet) event_router.register(r'scheduled_exports', exporters.ScheduledEventExportViewSet) event_router.register(r'exporters', exporters.EventExportersViewSet, basename='exporters') diff --git a/src/pretix/api/views/checkin.py b/src/pretix/api/views/checkin.py index 02304446d..26c0bbcae 100644 --- a/src/pretix/api/views/checkin.py +++ b/src/pretix/api/views/checkin.py @@ -56,7 +56,8 @@ from pretix.api.serializers.checkin import ( ) from pretix.api.serializers.item import QuestionSerializer from pretix.api.serializers.order import ( - CheckinListOrderPositionSerializer, FailedCheckinSerializer, + CheckinListOrderPositionSerializer, CheckinSerializer, + FailedCheckinSerializer, ) from pretix.api.views import RichOrderingFilter from pretix.api.views.order import OrderPositionFilter @@ -96,6 +97,16 @@ with scopes_disabled(): ) return queryset.filter(expr) + class CheckinFilter(FilterSet): + created_since = django_filters.IsoDateTimeFilter(field_name='created', lookup_expr='gte') + created_before = django_filters.IsoDateTimeFilter(field_name='created', lookup_expr='lt') + datetime_since = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='gte') + datetime_before = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='lt') + + class Meta: + model = Checkin + fields = ['successful', 'error_reason', 'list', 'type', 'gate', 'device', 'auto_checked_in'] + class CheckinListViewSet(viewsets.ModelViewSet): serializer_class = CheckinListSerializer @@ -1080,3 +1091,25 @@ class CheckinRPCAnnulView(views.APIView): checkin_annulled.send(ci.position.order.event, checkin=ci) return Response({"status": "ok"}, status=status.HTTP_200_OK) + + +class CheckinViewSet(viewsets.ReadOnlyModelViewSet): + serializer_class = CheckinSerializer + queryset = Checkin.all.none() + filter_backends = (DjangoFilterBackend, RichOrderingFilter) + filterset_class = CheckinFilter + ordering = ('created', 'id') + ordering_fields = ('created', 'datetime', 'id',) + permission = 'can_view_orders' + + def get_queryset(self): + qs = Checkin.all.filter().select_related( + "position", + "device", + ) + return qs + + def get_serializer_context(self): + ctx = super().get_serializer_context() + ctx['event'] = self.request.event + return ctx diff --git a/src/tests/api/test_checkin.py b/src/tests/api/test_checkin.py index fd18b497d..1dc696e6f 100644 --- a/src/tests/api/test_checkin.py +++ b/src/tests/api/test_checkin.py @@ -236,6 +236,21 @@ TEST_LIST_RES = { "rules": {} } +TEST_HISTORY_RES = { + "successful": True, + "error_reason": None, + "error_explanation": None, + "position": 1234, + "datetime": "2017-12-25T12:45:23Z", + "created": "2017-12-25T12:45:23Z", + "list": 2, + "auto_checked_in": False, + "gate": None, + "device": None, + "device_id": None, + "type": "entry", +} + @pytest.fixture def clist(event, item): @@ -1366,3 +1381,57 @@ def test_expand(token_client, organizer, event, clist, clist_all, item, other_it )) assert resp.status_code == 200 assert 'value' in resp.data['results'][0]['variation'] + + +@pytest.mark.django_db +def test_history(token_client, organizer, event, clist, order): + with scopes_disabled(): + ci = order.positions.first().checkins.create(list=clist, type=Checkin.TYPE_ENTRY, datetime=now()) + res = dict(TEST_HISTORY_RES) + res["id"] = ci.pk + res["datetime"] = ci.datetime.isoformat().replace('+00:00', 'Z') + res["created"] = ci.created.isoformat().replace('+00:00', 'Z') + res["list"] = clist.pk + res["position"] = ci.position_id + + resp = token_client.get('/api/v1/organizers/{}/events/{}/checkins/'.format( + organizer.slug, event.slug, + )) + assert resp.status_code == 200 + assert res == resp.data['results'][0] + + resp = token_client.get('/api/v1/organizers/{}/events/{}/checkins/?auto_checked_in=false'.format( + organizer.slug, event.slug, + )) + assert len(resp.data['results']) == 1 + resp = token_client.get('/api/v1/organizers/{}/events/{}/checkins/?auto_checked_in=true'.format( + organizer.slug, event.slug, + )) + assert len(resp.data['results']) == 0 + + resp = token_client.get('/api/v1/organizers/{}/events/{}/checkins/?successful=true'.format( + organizer.slug, event.slug, + )) + assert len(resp.data['results']) == 1 + resp = token_client.get('/api/v1/organizers/{}/events/{}/checkins/?successful=false'.format( + organizer.slug, event.slug, + )) + assert len(resp.data['results']) == 0 + + resp = token_client.get('/api/v1/organizers/{}/events/{}/checkins/?type=entry'.format( + organizer.slug, event.slug, + )) + assert len(resp.data['results']) == 1 + resp = token_client.get('/api/v1/organizers/{}/events/{}/checkins/?type=exit'.format( + organizer.slug, event.slug, + )) + assert len(resp.data['results']) == 0 + + resp = token_client.get('/api/v1/organizers/{}/events/{}/checkins/?created_before=2099-01-01T00:00:00Z'.format( + organizer.slug, event.slug, + )) + assert len(resp.data['results']) == 1 + resp = token_client.get('/api/v1/organizers/{}/events/{}/checkins/?created_before=2017-01-01T00:00:00Z'.format( + organizer.slug, event.slug, + )) + assert len(resp.data['results']) == 0 diff --git a/src/tests/api/test_permissions.py b/src/tests/api/test_permissions.py index 91249dd75..15da07f72 100644 --- a/src/tests/api/test_permissions.py +++ b/src/tests/api/test_permissions.py @@ -54,6 +54,7 @@ event_urls = [ (None, 'taxrules/'), ('can_view_orders', 'waitinglistentries/'), ('can_view_orders', 'checkinlists/'), + ('can_view_orders', 'checkins/'), (None, 'seats/'), ] @@ -176,6 +177,8 @@ event_permission_sub_urls = [ ('post', 'can_change_orders', 'orders/ABC12/refunds/1/done/', 404), ('get', 'can_view_orders', 'checkinlists/', 200), ('post', 'can_change_orders', 'checkinlists/1/failed_checkins/', 400), + ('get', 'can_view_orders', 'checkins/', 200), + ('get', 'can_view_orders', 'checkins/1/', 404), ('post', 'can_change_event_settings', 'checkinlists/', 400), ('put', 'can_change_event_settings', 'checkinlists/1/', 404), ('patch', 'can_change_event_settings', 'checkinlists/1/', 404),