From b564fe8a0d7cf63ea73ee6d0c42625cb125c8486 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ture=20Gj=C3=B8rup?= Date: Fri, 16 Mar 2018 14:50:22 +0100 Subject: [PATCH] Refs #654 -- API: Writable category endpoints (#818) * EBILL-5: Added POST, PATCH, PUT and DELETE for categories * EBILL-5: Fixed item category not removed on category delete --- doc/api/resources/categories.rst | 119 ++++++++++++++++++++++++++++++ src/pretix/api/views/item.py | 37 +++++++++- src/tests/api/test_items.py | 61 ++++++++++++++- src/tests/api/test_permissions.py | 4 + 4 files changed, 218 insertions(+), 3 deletions(-) diff --git a/doc/api/resources/categories.rst b/doc/api/resources/categories.rst index e766c5567f..22fb45675d 100644 --- a/doc/api/resources/categories.rst +++ b/doc/api/resources/categories.rst @@ -22,6 +22,10 @@ is_addon boolean If ``True``, it defining add-ons for other products. ===================================== ========================== ======================================================= +.. versionchanged:: 1.14 + + The operations POST, PATCH, PUT and DELETE have been added. + Endpoints --------- @@ -106,3 +110,118 @@ Endpoints :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)/categories/ + + Creates a new category + + **Example request**: + + .. sourcecode:: http + + POST /api/v1/organizers/bigevents/events/sampleconf/categories/ HTTP/1.1 + Host: pretix.eu + Accept: application/json, text/javascript + Content: application/json + + { + "name": {"en": "Tickets"}, + "description": {"en": "Tickets are what you need to get in."}, + "position": 1, + "is_addon": false + } + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 201 Created + Vary: Accept + Content-Type: application/json + + { + "id": 1, + "name": {"en": "Tickets"}, + "description": {"en": "Tickets are what you need to get in."}, + "position": 1, + "is_addon": false + } + + :param organizer: The ``slug`` field of the organizer of the event to create a category for + :param event: The ``slug`` field of the event to create a category for + :statuscode 201: no error + :statuscode 400: The category could not be created 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 resource. + +.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/categories/(id)/ + + Update a category. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide all fields of + the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you + want to change. + + You can change all fields of the resource except the ``id`` field. + + **Example request**: + + .. sourcecode:: http + + PATCH /api/v1/organizers/bigevents/events/sampleconf/categories/1/ HTTP/1.1 + Host: pretix.eu + Accept: application/json, text/javascript + Content-Type: application/json + Content-Length: 94 + + { + "is_addon": true + } + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: application/json + + { + "id": 1, + "name": {"en": "Tickets"}, + "description": {"en": "Tickets are what you need to get in."}, + "position": 1, + "is_addon": true + } + + :param organizer: The ``slug`` field of the organizer to modify + :param event: The ``slug`` field of the event to modify + :param id: The ``id`` field of the category to modify + :statuscode 200: no error + :statuscode 400: The category could not be modified due to invalid submitted data + :statuscode 401: Authentication failure + :statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource. + +.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/category/(id)/ + + Delete a category. + + **Example request**: + + .. sourcecode:: http + + DELETE /api/v1/organizers/bigevents/events/sampleconf/categories/1/ HTTP/1.1 + Host: pretix.eu + Accept: application/json, text/javascript + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 204 No Content + Vary: Accept + + :param organizer: The ``slug`` field of the organizer to modify + :param event: The ``slug`` field of the event to modify + :param id: The ``id`` field of the category to delete + :statuscode 204: no error + :statuscode 401: Authentication failure + :statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource. diff --git a/src/pretix/api/views/item.py b/src/pretix/api/views/item.py index 0c10260a6e..f26530c59e 100644 --- a/src/pretix/api/views/item.py +++ b/src/pretix/api/views/item.py @@ -203,7 +203,7 @@ class ItemCategoryFilter(FilterSet): fields = ['is_addon'] -class ItemCategoryViewSet(viewsets.ReadOnlyModelViewSet): +class ItemCategoryViewSet(viewsets.ModelViewSet): serializer_class = ItemCategorySerializer queryset = ItemCategory.objects.none() filter_backends = (DjangoFilterBackend, OrderingFilter) @@ -211,10 +211,45 @@ class ItemCategoryViewSet(viewsets.ReadOnlyModelViewSet): ordering_fields = ('id', 'position') ordering = ('position', 'id') permission = 'can_change_items' + write_permission = 'can_change_items' def get_queryset(self): return self.request.event.categories.all() + def perform_create(self, serializer): + serializer.save(event=self.request.event) + serializer.instance.log_action( + 'pretix.event.category.added', + user=self.request.user, + api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None), + data=self.request.data + ) + + def get_serializer_context(self): + ctx = super().get_serializer_context() + ctx['event'] = self.request.event + return ctx + + def perform_update(self, serializer): + serializer.save(event=self.request.event) + serializer.instance.log_action( + 'pretix.event.category.changed', + user=self.request.user, + api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None), + data=self.request.data + ) + + def perform_destroy(self, instance): + for item in instance.items.all(): + item.category = None + item.save() + instance.log_action( + 'pretix.event.category.deleted', + user=self.request.user, + api_token=(self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None), + ) + super().perform_destroy(instance) + class QuestionViewSet(viewsets.ModelViewSet): serializer_class = QuestionSerializer diff --git a/src/tests/api/test_items.py b/src/tests/api/test_items.py index 07e9b5bae9..f99aa41139 100644 --- a/src/tests/api/test_items.py +++ b/src/tests/api/test_items.py @@ -7,8 +7,8 @@ from django_countries.fields import Country from pytz import UTC from pretix.base.models import ( - CartPosition, InvoiceAddress, Item, ItemAddOn, ItemVariation, Order, - OrderPosition, Question, QuestionOption, Quota, + CartPosition, InvoiceAddress, Item, ItemAddOn, ItemCategory, ItemVariation, + Order, OrderPosition, Question, QuestionOption, Quota, ) from pretix.base.models.orders import OrderFee @@ -23,6 +23,14 @@ def category2(event2): return event2.categories.create(name="Tickets2") +@pytest.fixture +def category3(event, item): + cat = event.categories.create(name="Tickets") + item.category = cat + item.save() + return cat + + @pytest.fixture def order(event, item, taxrule): testtime = datetime(2017, 12, 1, 10, 0, 0, tzinfo=UTC) @@ -118,6 +126,55 @@ def test_category_detail(token_client, organizer, event, team, category): assert res == resp.data +@pytest.mark.django_db +def test_category_create(token_client, organizer, event, team): + resp = token_client.post( + '/api/v1/organizers/{}/events/{}/categories/'.format(organizer.slug, event.slug), + { + "name": {"en": "Tickets"}, + "description": {"en": ""}, + "position": 0, + "is_addon": False + }, + format='json' + ) + assert resp.status_code == 201 + + +@pytest.mark.django_db +def test_category_update(token_client, organizer, event, team, category): + resp = token_client.patch( + '/api/v1/organizers/{}/events/{}/categories/{}/'.format(organizer.slug, event.slug, category.pk), + { + "name": {"en": "Test"}, + }, + format='json' + ) + assert resp.status_code == 200 + assert ItemCategory.objects.get(pk=category.pk).name == {"en": "Test"} + + +@pytest.mark.django_db +def test_category_update_wrong_event(token_client, organizer, event2, category): + resp = token_client.patch( + '/api/v1/organizers/{}/events/{}/categories/{}/'.format(organizer.slug, event2.slug, category.pk), + { + "name": {"en": "Test"}, + }, + format='json' + ) + assert resp.status_code == 404 + + +@pytest.mark.django_db +def test_category_delete(token_client, organizer, event, category3, item): + resp = token_client.delete( + '/api/v1/organizers/{}/events/{}/categories/{}/'.format(organizer.slug, event.slug, category3.pk)) + assert resp.status_code == 204 + assert not event.categories.filter(pk=category3.id).exists() + assert Item.objects.get(pk=item.pk).category is None + + @pytest.fixture def item(event): return event.items.create(name="Budget Ticket", default_price=23) diff --git a/src/tests/api/test_permissions.py b/src/tests/api/test_permissions.py index 3e4d056b61..d4f3e83ca8 100644 --- a/src/tests/api/test_permissions.py +++ b/src/tests/api/test_permissions.py @@ -34,6 +34,10 @@ event_permission_urls = [ ('put', 'can_change_items', 'items/1/', 404), ('patch', 'can_change_items', 'items/1/', 404), ('delete', 'can_change_items', 'items/1/', 404), + ('post', 'can_change_items', 'categories/', 400), + ('put', 'can_change_items', 'categories/1/', 404), + ('patch', 'can_change_items', 'categories/1/', 404), + ('delete', 'can_change_items', 'categories/1/', 404), ('post', 'can_change_items', 'items/1/variations/', 404), ('put', 'can_change_items', 'items/1/variations/1/', 404), ('patch', 'can_change_items', 'items/1/variations/1/', 404),