diff --git a/doc/api/resources/orders.rst b/doc/api/resources/orders.rst index 4215416633..6ac57f2b6c 100644 --- a/doc/api/resources/orders.rst +++ b/doc/api/resources/orders.rst @@ -563,6 +563,57 @@ Order ticket download :statuscode 409: The file is not yet ready and will now be prepared. Retry the request after waiting for a few seconds. +Updating order fields +--------------------- + +.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/ + + Updates specific fields on an order. Currently, only the following fields are supported: + + * ``email`` + + * ``checkin_attention`` + + * ``locale`` + + * ``comment`` + + **Example request**: + + .. sourcecode:: http + + PATCH /api/v1/organizers/bigevents/events/sampleconf/orders/ABC12/ HTTP/1.1 + Host: pretix.eu + Accept: application/json, text/javascript + Content: application/json + + { + "email": "other@example.org", + "locale": "de", + "comment": "Foo", + "checkin_attention": True + } + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: application/json + + (Full order resource, see above.) + + :param organizer: The ``slug`` field of the organizer of the event + :param event: The ``slug`` field of the event + :param code: The ``code`` field of the order to update + + :statuscode 200: no error + :statuscode 400: The order could not be updated due to invalid submitted data. + :statuscode 401: Authentication failure + :statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this + order. + Deleting orders --------------- @@ -774,10 +825,10 @@ Creating orders (Full order resource, see above.) - :param organizer: The ``slug`` field of the organizer of the event to create an item for - :param event: The ``slug`` field of the event to create an item for + :param organizer: The ``slug`` field of the organizer of the event to create an order for + :param event: The ``slug`` field of the event to create an order for :statuscode 201: no error - :statuscode 400: The item could not be created due to invalid submitted data or lack of quota. + :statuscode 400: The order could not be created due to invalid submitted data or lack of quota. :statuscode 401: Authentication failure :statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this order. diff --git a/src/pretix/api/serializers/order.py b/src/pretix/api/serializers/order.py index 267f31e247..3d90086448 100644 --- a/src/pretix/api/serializers/order.py +++ b/src/pretix/api/serializers/order.py @@ -220,26 +220,49 @@ class OrderRefundSerializer(I18nAwareModelSerializer): class OrderSerializer(I18nAwareModelSerializer): - invoice_address = InvoiceAddressSerializer() - positions = OrderPositionSerializer(many=True) - fees = OrderFeeSerializer(many=True) - downloads = OrderDownloadsField(source='*') - payments = OrderPaymentSerializer(many=True) - refunds = OrderRefundSerializer(many=True) - payment_date = OrderPaymentDateField(source='*') - payment_provider = OrderPaymentTypeField(source='*') + invoice_address = InvoiceAddressSerializer(read_only=True) + positions = OrderPositionSerializer(many=True, read_only=True) + fees = OrderFeeSerializer(many=True, read_only=True) + downloads = OrderDownloadsField(source='*', read_only=True) + payments = OrderPaymentSerializer(many=True, read_only=True) + refunds = OrderRefundSerializer(many=True, read_only=True) + payment_date = OrderPaymentDateField(source='*', read_only=True) + payment_provider = OrderPaymentTypeField(source='*', read_only=True) class Meta: model = Order - fields = ('code', 'status', 'testmode', 'secret', 'email', 'locale', 'datetime', 'expires', 'payment_date', - 'payment_provider', 'fees', 'total', 'comment', 'invoice_address', 'positions', 'downloads', - 'checkin_attention', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel') + fields = ( + 'code', 'status', 'testmode', 'secret', 'email', 'locale', 'datetime', 'expires', 'payment_date', + 'payment_provider', 'fees', 'total', 'comment', 'invoice_address', 'positions', 'downloads', + 'checkin_attention', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel' + ) + read_only_fields = ( + 'code', 'status', 'testmode', 'secret', 'datetime', 'expires', 'payment_date', + 'payment_provider', 'fees', 'total', 'invoice_address', 'positions', 'downloads', + 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel' + ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if not self.context['request'].query_params.get('pdf_data', 'false') == 'true': self.fields['positions'].child.fields.pop('pdf_data') + def validate_locale(self, l): + if l not in set(k for k in self.instance.event.settings.locales): + raise ValidationError('"{}" is not a supported locale for this event.'.format(l)) + return l + + def update(self, instance, validated_data): + # Even though all fields that shouldn't be edited are marked as read_only in the serializer + # (hopefully), we'll be extra careful here and be explicit about the model fields we update. + update_fields = ['comment', 'checkin_attention', 'email', 'locale'] + + for attr, value in validated_data.items(): + if attr in update_fields: + setattr(instance, attr, value) + instance.save(update_fields=update_fields) + return instance + class AnswerCreateSerializer(I18nAwareModelSerializer): diff --git a/src/pretix/api/views/order.py b/src/pretix/api/views/order.py index 40a45103ba..a5b70ddeb1 100644 --- a/src/pretix/api/views/order.py +++ b/src/pretix/api/views/order.py @@ -16,7 +16,7 @@ from rest_framework.exceptions import ( APIException, NotFound, PermissionDenied, ValidationError, ) from rest_framework.filters import OrderingFilter -from rest_framework.mixins import CreateModelMixin, DestroyModelMixin +from rest_framework.mixins import CreateModelMixin from rest_framework.response import Response from pretix.api.models import OAuthAccessToken @@ -54,7 +54,7 @@ class OrderFilter(FilterSet): fields = ['code', 'status', 'email', 'locale', 'testmode', 'require_approval'] -class OrderViewSet(DestroyModelMixin, CreateModelMixin, viewsets.ReadOnlyModelViewSet): +class OrderViewSet(viewsets.ModelViewSet): serializer_class = OrderSerializer queryset = Order.objects.none() filter_backends = (DjangoFilterBackend, OrderingFilter) @@ -375,6 +375,61 @@ class OrderViewSet(DestroyModelMixin, CreateModelMixin, viewsets.ReadOnlyModelVi headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + def update(self, request, *args, **kwargs): + partial = kwargs.get('partial', False) + if not partial: + return Response( + {"detail": "Method \"PUT\" not allowed."}, + status=status.HTTP_405_METHOD_NOT_ALLOWED, + ) + return super().update(request, *args, **kwargs) + + @transaction.atomic + def perform_update(self, serializer): + if 'comment' in self.request.data and serializer.instance.comment != self.request.data.get('comment'): + serializer.instance.log_action( + 'pretix.event.order.comment', + user=self.request.user, + auth=self.request.auth, + data={ + 'new_comment': self.request.data.get('comment') + } + ) + + if 'checkin_attention' in self.request.data and serializer.instance.checkin_attention != self.request.data.get('checkin_attention'): + serializer.instance.log_action( + 'pretix.event.order.checkin_attention', + user=self.request.user, + auth=self.request.auth, + data={ + 'new_value': self.request.data.get('checkin_attention') + } + ) + + if 'email' in self.request.data and serializer.instance.email != self.request.data.get('email'): + serializer.instance.log_action( + 'pretix.event.order.contact.changed', + user=self.request.user, + auth=self.request.auth, + data={ + 'old_email': serializer.instance.email, + 'new_email': self.request.data.get('email'), + } + ) + + if 'locale' in self.request.data and serializer.instance.locale != self.request.data.get('locale'): + serializer.instance.log_action( + 'pretix.event.order.locale.changed', + user=self.request.user, + auth=self.request.auth, + data={ + 'old_locale': serializer.instance.locale, + 'new_locale': self.request.data.get('locale'), + } + ) + + serializer.save() + def perform_create(self, serializer): serializer.save() diff --git a/src/tests/api/test_orders.py b/src/tests/api/test_orders.py index b6ba08a4c6..528d75f043 100644 --- a/src/tests/api/test_orders.py +++ b/src/tests/api/test_orders.py @@ -2548,3 +2548,81 @@ def test_order_delete_test_mode(token_client, organizer, event, order): ) assert resp.status_code == 204 assert not Order.objects.filter(code=order.code).exists() + + +@pytest.mark.django_db +def test_order_update_ignore_fields(token_client, organizer, event, order): + resp = token_client.patch( + '/api/v1/organizers/{}/events/{}/orders/{}/'.format( + organizer.slug, event.slug, order.code + ), format='json', data={ + 'status': 'c' + } + ) + assert resp.status_code == 200 + order.refresh_from_db() + assert order.status == 'n' + + +@pytest.mark.django_db +def test_order_update_only_partial(token_client, organizer, event, order): + resp = token_client.put( + '/api/v1/organizers/{}/events/{}/orders/{}/'.format( + organizer.slug, event.slug, order.code + ), format='json', data={ + 'status': 'c' + } + ) + assert resp.status_code == 405 + + +@pytest.mark.django_db +def test_order_update_allowed_fields(token_client, organizer, event, order): + event.settings.locales = ['de', 'en'] + resp = token_client.patch( + '/api/v1/organizers/{}/events/{}/orders/{}/'.format( + organizer.slug, event.slug, order.code + ), format='json', data={ + 'comment': 'Here is a comment', + 'checkin_attention': True, + 'email': 'foo@bar.com', + 'locale': 'de' + } + ) + assert resp.status_code == 200 + order.refresh_from_db() + assert order.comment == 'Here is a comment' + assert order.checkin_attention + assert order.email == 'foo@bar.com' + assert order.locale == 'de' + assert order.all_logentries().get(action_type='pretix.event.order.comment') + assert order.all_logentries().get(action_type='pretix.event.order.checkin_attention') + assert order.all_logentries().get(action_type='pretix.event.order.contact.changed') + assert order.all_logentries().get(action_type='pretix.event.order.locale.changed') + + +@pytest.mark.django_db +def test_order_update_email_to_none(token_client, organizer, event, order): + resp = token_client.patch( + '/api/v1/organizers/{}/events/{}/orders/{}/'.format( + organizer.slug, event.slug, order.code + ), format='json', data={ + 'email': None, + } + ) + assert resp.status_code == 200 + order.refresh_from_db() + assert order.email is None + + +@pytest.mark.django_db +def test_order_update_locale_to_invalid(token_client, organizer, event, order): + resp = token_client.patch( + '/api/v1/organizers/{}/events/{}/orders/{}/'.format( + organizer.slug, event.slug, order.code + ), format='json', data={ + 'locale': 'de', + } + ) + assert resp.status_code == 400 + assert resp.data == {'locale': ['"de" is not a supported locale for this event.']}