diff --git a/doc/api/resources/orders.rst b/doc/api/resources/orders.rst index f863a6df45..e09b8e09de 100644 --- a/doc/api/resources/orders.rst +++ b/doc/api/resources/orders.rst @@ -1719,6 +1719,56 @@ List of all order positions :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)/orderpositions/ + + Returns a list of all order positions within all events of a given organizer (with sufficient access permissions). + + The supported query parameters and output format of this endpoint are almost identical to those of the list endpoint + within an event. + The only changes are that responses also contain the ``event`` attribute in each result and that the 'pdf_data' + parameter is not supported. + + **Example request**: + + .. sourcecode:: http + + GET /api/v1/organizers/bigevents/orderpositions/ 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 + X-Page-Generated: 2017-12-01T10:00:00Z + + { + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "id:": 23442 + "event": "sampleconf", + "order": "ABC12", + "positionid": 1, + "canceled": false, + "item": 1345, + ... + } + ] + } + + :param organizer: The ``slug`` field of the organizer 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. + + + Fetching individual positions ----------------------------- diff --git a/src/pretix/api/serializers/order.py b/src/pretix/api/serializers/order.py index 33eba51d2c..3f6b0405a0 100644 --- a/src/pretix/api/serializers/order.py +++ b/src/pretix/api/serializers/order.py @@ -637,6 +637,14 @@ class OrderPositionSerializer(I18nAwareModelSerializer): return entry +class OrganizerOrderPositionSerializer(OrderPositionSerializer): + event = SlugRelatedField(slug_field='slug', read_only=True) + + class Meta(OrderPositionSerializer.Meta): + fields = OrderPositionSerializer.Meta.fields + ('event',) + read_only_fields = OrderPositionSerializer.Meta.read_only_fields + ('event',) + + class RequireAttentionField(serializers.Field): def to_representation(self, instance: OrderPosition): return instance.require_checkin_attention diff --git a/src/pretix/api/urls.py b/src/pretix/api/urls.py index a4db75dfdd..911ce8c9e0 100644 --- a/src/pretix/api/urls.py +++ b/src/pretix/api/urls.py @@ -67,6 +67,7 @@ orga_router.register(r'invoices', order.InvoiceViewSet) orga_router.register(r'scheduled_exports', exporters.ScheduledOrganizerExportViewSet) orga_router.register(r'exporters', exporters.OrganizerExportersViewSet, basename='exporters') orga_router.register(r'transactions', order.OrganizerTransactionViewSet) +orga_router.register(r'orderpositions', order.OrganizerOrderPositionViewSet, basename='orderpositions') team_router = routers.DefaultRouter() team_router.register(r'members', organizer.TeamMemberViewSet) @@ -83,7 +84,7 @@ event_router.register(r'discounts', discount.DiscountViewSet) event_router.register(r'quotas', item.QuotaViewSet) event_router.register(r'vouchers', voucher.VoucherViewSet) event_router.register(r'orders', order.EventOrderViewSet) -event_router.register(r'orderpositions', order.OrderPositionViewSet) +event_router.register(r'orderpositions', order.EventOrderPositionViewSet) event_router.register(r'transactions', order.TransactionViewSet) event_router.register(r'invoices', order.InvoiceViewSet) event_router.register(r'revokedsecrets', order.RevokedSecretViewSet, basename='revokedsecrets') diff --git a/src/pretix/api/views/order.py b/src/pretix/api/views/order.py index 8ec7828763..10c9119946 100644 --- a/src/pretix/api/views/order.py +++ b/src/pretix/api/views/order.py @@ -57,9 +57,10 @@ from pretix.api.serializers.order import ( BlockedTicketSecretSerializer, InvoiceSerializer, OrderCreateSerializer, OrderPaymentCreateSerializer, OrderPaymentSerializer, OrderPositionSerializer, OrderRefundCreateSerializer, - OrderRefundSerializer, OrderSerializer, OrganizerTransactionSerializer, - PriceCalcSerializer, PrintLogSerializer, RevokedTicketSecretSerializer, - SimulatedOrderSerializer, TransactionSerializer, + OrderRefundSerializer, OrderSerializer, OrganizerOrderPositionSerializer, + OrganizerTransactionSerializer, PriceCalcSerializer, PrintLogSerializer, + RevokedTicketSecretSerializer, SimulatedOrderSerializer, + TransactionSerializer, ) from pretix.api.serializers.orderchange import ( BlockNameSerializer, OrderChangeOperationSerializer, @@ -1065,8 +1066,7 @@ with scopes_disabled(): } -class OrderPositionViewSet(viewsets.ModelViewSet): - serializer_class = OrderPositionSerializer +class OrderPositionViewSetMixin: queryset = OrderPosition.all.none() filter_backends = (DjangoFilterBackend, RichOrderingFilter) ordering = ('order__datetime', 'positionid') @@ -1087,8 +1087,7 @@ class OrderPositionViewSet(viewsets.ModelViewSet): def get_serializer_context(self): ctx = super().get_serializer_context() - ctx['event'] = self.request.event - ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false').lower() == 'true' + ctx['pdf_data'] = False ctx['check_quotas'] = self.request.query_params.get('check_quotas', 'true').lower() == 'true' return ctx @@ -1097,9 +1096,8 @@ class OrderPositionViewSet(viewsets.ModelViewSet): qs = OrderPosition.all else: qs = OrderPosition.objects - - qs = qs.filter(order__event=self.request.event) - if self.request.query_params.get('pdf_data', 'false').lower() == 'true': + qs = qs.filter(order__event__organizer=self.request.organizer) + if self.request.query_params.get('pdf_data', 'false').lower() == 'true' and getattr(self.request, 'event', None): prefetch_related_objects([self.request.organizer], 'meta_properties') prefetch_related_objects( [self.request.event], @@ -1154,9 +1152,9 @@ class OrderPositionViewSet(viewsets.ModelViewSet): qs = qs.prefetch_related( Prefetch('checkins', queryset=Checkin.objects.select_related("device")), Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')), - 'answers', 'answers__options', 'answers__question', + 'answers', 'answers__options', 'answers__question', 'order__event', 'order__event__organizer' ).select_related( - 'item', 'order', 'order__event', 'order__event__organizer', 'seat' + 'item', 'order', 'seat' ) return qs @@ -1168,6 +1166,45 @@ class OrderPositionViewSet(viewsets.ModelViewSet): return prov raise NotFound('Unknown output provider.') + +class OrganizerOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ReadOnlyModelViewSet): + serializer_class = OrganizerOrderPositionSerializer + + def get_queryset(self): + qs = super().get_queryset() + + perm = self.permission if self.request.method in SAFE_METHODS else self.write_permission + + if isinstance(self.request.auth, (TeamAPIToken, Device)): + auth_obj = self.request.auth + elif self.request.user.is_authenticated: + auth_obj = self.request.user + else: + raise PermissionDenied("Unknown authentication scheme") + + qs = qs.filter( + order__event__in=auth_obj.get_events_with_permission(perm, request=self.request).filter( + organizer=self.request.organizer + ) + ) + + return qs + + +class EventOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ModelViewSet): + serializer_class = OrderPositionSerializer + + def get_serializer_context(self): + ctx = super().get_serializer_context() + ctx['event'] = self.request.event + ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false').lower() == 'true' + return ctx + + def get_queryset(self): + qs = super().get_queryset() + qs = qs.filter(order__event=self.request.event) + return qs + @action(detail=True, methods=['POST'], url_name='price_calc') def price_calc(self, request, *args, **kwargs): """ diff --git a/src/tests/api/test_orders.py b/src/tests/api/test_orders.py index 6f41ac82b5..ff047a00a8 100644 --- a/src/tests/api/test_orders.py +++ b/src/tests/api/test_orders.py @@ -34,7 +34,7 @@ from stripe import error from tests.plugins.stripe.test_checkout import apple_domain_create from tests.plugins.stripe.test_provider import MockedCharge -from pretix.base.models import InvoiceAddress, Order, OrderPosition +from pretix.base.models import InvoiceAddress, Order, OrderPosition, Team from pretix.base.models.orders import OrderFee, OrderPayment, OrderRefund @@ -180,6 +180,41 @@ def order2(event2, item2): return o +@pytest.fixture +@scopes_disabled() +def team2(organizer, event2): + team2 = Team.objects.create( + organizer=organizer, + name="Test-Team 2", + can_change_teams=True, + can_manage_gift_cards=True, + can_change_items=True, + can_create_events=True, + can_change_event_settings=True, + can_change_vouchers=True, + can_view_vouchers=True, + can_change_orders=True, + can_manage_customers=True, + can_manage_reusable_media=True, + can_change_organizer_settings=True, + + ) + team2.limit_events.add(event2) + team2.save() + return team2 + + +@pytest.fixture +@scopes_disabled() +def limited_token_client(client, team2): + team2.can_view_orders = True + team2.can_view_vouchers = True + team2.save() + t = team2.tokens.create(name='Foo') + client.credentials(HTTP_AUTHORIZATION='Token ' + t.token) + return client + + TEST_ORDERPOSITION_RES = { "id": 1, "order": "FOO", @@ -987,8 +1022,64 @@ def test_refund_cancel(token_client, organizer, event, order): assert resp.status_code == 400 +@pytest.mark.parametrize( + "endpoint_template, response_code", + [('/api/v1/organizers/{}/events/{}/orderpositions/', 403), ('/api/v1/organizers/{}/orderpositions/', 200)] +) @pytest.mark.django_db -def test_orderposition_list(token_client, organizer, device, event, order, item, subevent, subevent2, question, django_assert_num_queries): +def test_orderposition_list_limited_read( + endpoint_template, response_code, limited_token_client, organizer, device, event, order, item, subevent, subevent2, question +): + endpoint = endpoint_template.format(organizer.slug, event.slug) + + i2 = copy.copy(item) + i2.pk = None + i2.save() + with scopes_disabled(): + var = item.variations.create(value="Children") + res = copy.copy(TEST_ORDERPOSITION_RES) + op = order.positions.first() + op.variation = var + op.save() + res["id"] = op.pk + res["item"] = item.pk + res["variation"] = var.pk + res["answers"][0]["question"] = question.pk + res["print_logs"][0]["id"] = op.print_logs.first().pk + res["print_logs"][0]["device_id"] = device.device_id + + resp = limited_token_client.get(endpoint) + assert resp.status_code == response_code + if response_code == 200: + assert resp.json() == {'count': 0, 'next': None, 'previous': None, 'results': []} + else: + assert resp.json() == {'detail': 'You do not have permission to perform this action.'} + + +@pytest.mark.parametrize( + ("endpoint_template", "endpoint_type"), + [ + ('/api/v1/organizers/{}/events/{}/orderpositions/', "event"), + ('/api/v1/organizers/{}/orderpositions/', "organizer") + ], +) +@pytest.mark.django_db +def test_orderposition_list( + endpoint_template, + endpoint_type, + token_client, + organizer, + device, + event, + order, + item, + subevent, + subevent2, + question, + django_assert_num_queries +): + endpoint = endpoint_template.format(organizer.slug, event.slug) + i2 = copy.copy(item) i2.pk = None i2.save() @@ -1005,88 +1096,64 @@ def test_orderposition_list(token_client, organizer, device, event, order, item, res["answers"][0]["question"] = question.pk res["print_logs"][0]["id"] = op.print_logs.first().pk res["print_logs"][0]["device_id"] = device.device_id + if endpoint_type == "organizer": + res["event"] = event.slug - resp = token_client.get('/api/v1/organizers/{}/events/{}/orderpositions/'.format(organizer.slug, event.slug)) + resp = token_client.get(endpoint) assert resp.status_code == 200 assert [res] == resp.data['results'] - resp = token_client.get( - '/api/v1/organizers/{}/events/{}/orderpositions/?order__status=n'.format(organizer.slug, event.slug)) + resp = token_client.get(endpoint + '?order__status=n') assert [res] == resp.data['results'] - resp = token_client.get( - '/api/v1/organizers/{}/events/{}/orderpositions/?order__status=p'.format(organizer.slug, event.slug)) + resp = token_client.get(endpoint + '?order__status=p') assert [] == resp.data['results'] - resp = token_client.get( - '/api/v1/organizers/{}/events/{}/orderpositions/?item={}'.format(organizer.slug, event.slug, item.pk)) + resp = token_client.get(endpoint + '?item={}'.format(item.pk)) assert [res] == resp.data['results'] - resp = token_client.get( - '/api/v1/organizers/{}/events/{}/orderpositions/?item__in={},{}'.format( - organizer.slug, event.slug, item.pk, i2.pk - )) + resp = token_client.get(endpoint + '?item__in={},{}'.format(item.pk, i2.pk)) assert [res] == resp.data['results'] - resp = token_client.get( - '/api/v1/organizers/{}/events/{}/orderpositions/?item={}'.format(organizer.slug, event.slug, i2.pk)) + resp = token_client.get(endpoint + '?item={}'.format(i2.pk)) assert [] == resp.data['results'] - resp = token_client.get( - '/api/v1/organizers/{}/events/{}/orderpositions/?variation={}'.format(organizer.slug, event.slug, var.pk)) + resp = token_client.get(endpoint + '?variation={}'.format(var.pk)) assert [res] == resp.data['results'] - resp = token_client.get( - '/api/v1/organizers/{}/events/{}/orderpositions/?variation={}'.format(organizer.slug, event.slug, var2.pk)) + resp = token_client.get(endpoint + '?variation={}'.format(var2.pk)) assert [] == resp.data['results'] - resp = token_client.get( - '/api/v1/organizers/{}/events/{}/orderpositions/?attendee_name=Peter'.format(organizer.slug, event.slug)) + resp = token_client.get(endpoint + '?attendee_name=Peter') assert [res] == resp.data['results'] - resp = token_client.get( - '/api/v1/organizers/{}/events/{}/orderpositions/?attendee_name=peter'.format(organizer.slug, event.slug)) + resp = token_client.get(endpoint + '?attendee_name=peter') assert [res] == resp.data['results'] - resp = token_client.get( - '/api/v1/organizers/{}/events/{}/orderpositions/?attendee_name=Mark'.format(organizer.slug, event.slug)) + resp = token_client.get(endpoint + '?attendee_name=Mark') assert [] == resp.data['results'] - resp = token_client.get( - '/api/v1/organizers/{}/events/{}/orderpositions/?secret=z3fsn8jyufm5kpk768q69gkbyr5f4h6w'.format( - organizer.slug, event.slug)) + resp = token_client.get(endpoint + '?secret=z3fsn8jyufm5kpk768q69gkbyr5f4h6w') assert [res] == resp.data['results'] - resp = token_client.get( - '/api/v1/organizers/{}/events/{}/orderpositions/?secret=abc123'.format(organizer.slug, event.slug)) + resp = token_client.get(endpoint + '?secret=abc123') assert [] == resp.data['results'] - resp = token_client.get( - '/api/v1/organizers/{}/events/{}/orderpositions/?pseudonymization_id=ABCDEFGHKL'.format( - organizer.slug, event.slug)) + resp = token_client.get(endpoint + '?pseudonymization_id=ABCDEFGHKL') assert [res] == resp.data['results'] - resp = token_client.get( - '/api/v1/organizers/{}/events/{}/orderpositions/?pseudonymization_id=FOO'.format(organizer.slug, event.slug)) + resp = token_client.get(endpoint + '?pseudonymization_id=FOO') assert [] == resp.data['results'] - resp = token_client.get( - '/api/v1/organizers/{}/events/{}/orderpositions/?search=FO'.format(organizer.slug, event.slug)) + resp = token_client.get(endpoint + '?search=FO') assert [res] == resp.data['results'] - resp = token_client.get( - '/api/v1/organizers/{}/events/{}/orderpositions/?search=z3fsn8j'.format(organizer.slug, event.slug)) + resp = token_client.get(endpoint + '?search=z3fsn8j') assert [res] == resp.data['results'] - resp = token_client.get( - '/api/v1/organizers/{}/events/{}/orderpositions/?search=Peter'.format(organizer.slug, event.slug)) + resp = token_client.get(endpoint + '?search=Peter') assert [res] == resp.data['results'] - resp = token_client.get( - '/api/v1/organizers/{}/events/{}/orderpositions/?search=5f4h6w'.format(organizer.slug, event.slug)) + resp = token_client.get(endpoint + '?search=5f4h6w') assert [] == resp.data['results'] - resp = token_client.get( - '/api/v1/organizers/{}/events/{}/orderpositions/?order=FOO'.format(organizer.slug, event.slug)) + resp = token_client.get(endpoint + '?order=FOO') assert [res] == resp.data['results'] - resp = token_client.get( - '/api/v1/organizers/{}/events/{}/orderpositions/?order=BAR'.format(organizer.slug, event.slug)) + resp = token_client.get(endpoint + '?order=BAR') assert [] == resp.data['results'] - resp = token_client.get( - '/api/v1/organizers/{}/events/{}/orderpositions/?has_checkin=false'.format(organizer.slug, event.slug)) + resp = token_client.get(endpoint + '?has_checkin=false') assert [res] == resp.data['results'] - resp = token_client.get( - '/api/v1/organizers/{}/events/{}/orderpositions/?has_checkin=true'.format(organizer.slug, event.slug)) + resp = token_client.get(endpoint + '?has_checkin=true') assert [] == resp.data['results'] with scopes_disabled(): @@ -1103,33 +1170,28 @@ def test_orderposition_list(token_client, organizer, device, event, order, item, 'gate': None, 'type': 'entry' }] - with django_assert_num_queries(16): - resp = token_client.get( - '/api/v1/organizers/{}/events/{}/orderpositions/?has_checkin=true'.format(organizer.slug, event.slug) - ) + if '/events/' in endpoint: + with django_assert_num_queries(18): + resp = token_client.get(endpoint + '?has_checkin=true') + else: + with django_assert_num_queries(17): + resp = token_client.get(endpoint + '?has_checkin=true') assert [res] == resp.data['results'] op.subevent = subevent op.save() res['subevent'] = subevent.pk - resp = token_client.get( - '/api/v1/organizers/{}/events/{}/orderpositions/?subevent={}'.format(organizer.slug, event.slug, subevent.pk)) + resp = token_client.get(endpoint + '?subevent={}'.format(subevent.pk)) assert [res] == resp.data['results'] - resp = token_client.get( - '/api/v1/organizers/{}/events/{}/orderpositions/?subevent__in={},{}'.format(organizer.slug, event.slug, - subevent.pk, subevent2.pk)) + resp = token_client.get(endpoint + '?subevent__in={},{}'.format(subevent.pk, subevent2.pk)) assert [res] == resp.data['results'] - resp = token_client.get( - '/api/v1/organizers/{}/events/{}/orderpositions/?subevent={}'.format(organizer.slug, event.slug, - subevent.pk + 1)) + resp = token_client.get(endpoint + '?subevent={}'.format(subevent.pk + 1)) assert [] == resp.data['results'] - resp = token_client.get( - '/api/v1/organizers/{}/events/{}/orderpositions/?include_canceled_positions=false'.format(organizer.slug, event.slug)) + resp = token_client.get(endpoint + '?include_canceled_positions=false') assert len(resp.data['results']) == 1 - resp = token_client.get( - '/api/v1/organizers/{}/events/{}/orderpositions/?include_canceled_positions=true'.format(organizer.slug, event.slug)) + resp = token_client.get(endpoint + '?include_canceled_positions=true') assert len(resp.data['results']) == 2