diff --git a/doc/api/resources/checkinlists.rst b/doc/api/resources/checkinlists.rst index f0e83bd4c..af55db676 100644 --- a/doc/api/resources/checkinlists.rst +++ b/doc/api/resources/checkinlists.rst @@ -44,6 +44,10 @@ include_pending boolean If ``true``, th Endpoints --------- +.. versionchanged:: 1.15 + + The ``../status/`` detail endpoint has been added. + .. http:get:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/ Returns a list of all check-in lists within a given event. @@ -128,6 +132,72 @@ Endpoints :statuscode 401: Authentication failure :statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource. +.. http:get:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/(id)/status/ + + Returns detailed status information on a check-in list, identified by its ID. + + **Example request**: + + .. sourcecode:: http + + GET /api/v1/organizers/bigevents/events/sampleconf/checkinlists/1/status/ 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 + + { + "checkin_count": 17, + "position_count": 42, + "event": { + "name": "Demo Converence", + }, + "items": [ + { + "name": "T-Shirt", + "id": 1, + "checkin_count": 1, + "admission": False, + "position_count": 1, + "variations": [ + { + "value": "Red", + "id": 1, + "checkin_count": 1, + "position_count": 12 + }, + { + "value": "Blue", + "id": 2, + "checkin_count": 4, + "position_count": 8 + } + ] + }, + { + "name": "Ticket", + "id": 2, + "checkin_count": 15, + "admission": True, + "position_count": 22, + "variations": [] + } + ] + } + + :param organizer: The ``slug`` field of the organizer to fetch + :param event: The ``slug`` field of the event to fetch + :param id: The ``id`` field of the check-in list 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. + .. http:post:: /api/v1/organizers/(organizer)/events/(event)/checkinlists/ Creates a new check-in list. diff --git a/src/pretix/api/views/checkin.py b/src/pretix/api/views/checkin.py index ee536c296..e017c82c1 100644 --- a/src/pretix/api/views/checkin.py +++ b/src/pretix/api/views/checkin.py @@ -1,9 +1,11 @@ -from django.db.models import F, Max, OuterRef, Prefetch, Subquery +from django.db.models import Count, F, Max, OuterRef, Prefetch, Subquery from django.db.models.functions import Coalesce from django.shortcuts import get_object_or_404 from django.utils.functional import cached_property from django_filters.rest_framework import DjangoFilterBackend, FilterSet from rest_framework import viewsets +from rest_framework.decorators import detail_route +from rest_framework.response import Response from pretix.api.serializers.checkin import CheckinListSerializer from pretix.api.serializers.order import OrderPositionSerializer @@ -66,6 +68,74 @@ class CheckinListViewSet(viewsets.ModelViewSet): ) super().perform_destroy(instance) + @detail_route(methods=['GET']) + def status(self, *args, **kwargs): + clist = self.get_object() + cqs = Checkin.objects.filter( + position__order__event=clist.event, + position__order__status__in=[Order.STATUS_PAID] + ([Order.STATUS_PENDING] if clist.include_pending else []), + list=clist + ) + pqs = OrderPosition.objects.filter( + order__event=clist.event, + order__status__in=[Order.STATUS_PAID] + ([Order.STATUS_PENDING] if clist.include_pending else []), + subevent=clist.subevent, + ) + if not clist.all_products: + pqs = pqs.filter(item__in=clist.limit_products.values_list('id', flat=True)) + + ev = clist.subevent or clist.event + response = { + 'event': { + 'name': str(ev.name), + }, + 'checkin_count': cqs.count(), + 'position_count': pqs.count() + } + + op_by_item = { + p['item']: p['cnt'] + for p in pqs.order_by().values('item').annotate(cnt=Count('id')) + } + op_by_variation = { + p['variation']: p['cnt'] + for p in pqs.order_by().values('variation').annotate(cnt=Count('id')) + } + c_by_item = { + p['position__item']: p['cnt'] + for p in cqs.order_by().values('position__item').annotate(cnt=Count('id')) + } + c_by_variation = { + p['position__variation']: p['cnt'] + for p in cqs.order_by().values('position__variation').annotate(cnt=Count('id')) + } + + if not clist.all_products: + items = clist.limit_products + else: + items = clist.event.items + + response['items'] = [] + for item in items.order_by('category__position', 'position', 'pk').prefetch_related('variations'): + i = { + 'id': item.pk, + 'name': str(item), + 'admission': item.admission, + 'checkin_count': c_by_item.get(item.pk, 0), + 'position_count': op_by_item.get(item.pk, 0), + 'variations': [] + } + for var in item.variations.all(): + i['variations'].append({ + 'id': var.pk, + 'value': str(var), + 'checkin_count': c_by_variation.get(var.pk, 0), + 'position_count': op_by_variation.get(var.pk, 0), + }) + response['items'].append(i) + + return Response(response) + class CheckinOrderPositionFilter(OrderPositionFilter): diff --git a/src/tests/api/test_checkin.py b/src/tests/api/test_checkin.py index 6d9ce0390..34400c301 100644 --- a/src/tests/api/test_checkin.py +++ b/src/tests/api/test_checkin.py @@ -8,7 +8,7 @@ from django_countries.fields import Country from pytz import UTC from pretix.base.models import ( - CheckinList, InvoiceAddress, Order, OrderPosition, + Checkin, CheckinList, InvoiceAddress, Order, OrderPosition, ) @@ -83,7 +83,6 @@ TEST_ORDERPOSITION1_RES = { "subevent": None } - TEST_ORDERPOSITION2_RES = { "id": 2, "order": "FOO", @@ -313,14 +312,16 @@ def test_list_all_items_positions(token_client, organizer, event, clist, clist_a assert [p2] == resp.data['results'] # Order by checkin - resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=-last_checked_in'.format( - organizer.slug, event.slug, clist_all.pk - )) + 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 [p1, p2] == resp.data['results'] - resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=last_checked_in'.format( - organizer.slug, event.slug, clist_all.pk - )) + 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'] @@ -333,9 +334,10 @@ def test_list_all_items_positions(token_client, organizer, event, clist, clist_a 'datetime': c.datetime.isoformat().replace('+00:00', 'Z') } ] - resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=-last_checked_in'.format( - organizer.slug, event.slug, clist_all.pk - )) + 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'] @@ -387,3 +389,50 @@ def test_list_limited_items_position_detail(token_client, organizer, event, clis )) assert resp.status_code == 200 assert p1 == resp.data + + +@pytest.mark.django_db +def test_status(token_client, organizer, event, clist_all, item, other_item, order): + op = order.positions.first() + var1 = item.variations.create(value="XS") + var2 = item.variations.create(value="S") + op.variation = var1 + op.save() + Checkin.objects.create(position=op, list=clist_all) + resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/status/'.format( + organizer.slug, event.slug, clist_all.pk, + )) + assert resp.status_code == 200 + assert resp.data['checkin_count'] == 1 + assert resp.data['position_count'] == 2 + assert resp.data['items'] == [ + { + 'name': str(item.name), + 'id': item.pk, + 'checkin_count': 1, + 'admission': False, + 'position_count': 1, + 'variations': [ + { + 'id': var1.pk, + 'value': 'XS', + 'checkin_count': 1, + 'position_count': 1, + }, + { + 'id': var2.pk, + 'value': 'S', + 'checkin_count': 0, + 'position_count': 0, + }, + ] + }, + { + 'name': other_item.name, + 'id': other_item.pk, + 'checkin_count': 0, + 'admission': False, + 'position_count': 1, + 'variations': [] + } + ]