diff --git a/doc/api/resources/events.rst b/doc/api/resources/events.rst index c5ee40f671..9d51722255 100644 --- a/doc/api/resources/events.rst +++ b/doc/api/resources/events.rst @@ -379,7 +379,7 @@ Endpoints :param organizer: The ``slug`` field of the organizer of the event to update :param event: The ``slug`` field of the event to update - :statuscode 201: no error + :statuscode 200: no error :statuscode 400: The event 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. diff --git a/doc/api/resources/subevents.rst b/doc/api/resources/subevents.rst index b0734a2d23..6f2aadd535 100644 --- a/doc/api/resources/subevents.rst +++ b/doc/api/resources/subevents.rst @@ -45,6 +45,8 @@ meta_data dict Values set for The ``event`` field has been added, together with filters on the list of dates and an organizer-level list. +.. versionchanged:: 2.6 + The write operations ``POST``, ``PATCH``, ``PUT``, and ``DELETE`` have been added. Endpoints --------- @@ -103,11 +105,83 @@ Endpoints :query is_past: If set to ``true`` (``false``), only events that are over are (not) returned. :query ends_after: If set to a date and time, only events that happen during of after the given time are returned. :param organizer: The ``slug`` field of a valid organizer - :param event: The ``slug`` field of the event to fetch + :param event: The ``slug`` field of the main event :statuscode 200: no error :statuscode 401: Authentication failure :statuscode 403: The requested organizer does not exist **or** you have no permission to view it. +.. http:post:: /api/v1/organizers/(organizer)/events/(event)/subevents/ + + Creates a new subevent. + + Permission required: "Can create events" + + **Example request**: + + .. sourcecode:: http + + POST /api/v1/organizers/bigevents/events/sampleconf/subevents/ HTTP/1.1 + Host: pretix.eu + Accept: application/json, text/javascript + Content: application/json + + { + "name": {"en": "First Sample Conference"}, + "active": false, + "date_from": "2017-12-27T10:00:00Z", + "date_to": null, + "date_admission": null, + "presale_start": null, + "presale_end": null, + "location": null, + "item_price_overrides": [ + { + "item": 2, + "price": "12.00" + } + ], + "variation_price_overrides": [], + "meta_data": {} + } + + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 201 Created + Vary: Accept + Content-Type: application/json + + { + "id": 1, + "name": {"en": "First Sample Conference"}, + "active": false, + "date_from": "2017-12-27T10:00:00Z", + "date_to": null, + "date_admission": null, + "presale_start": null, + "presale_end": null, + "location": null, + "item_price_overrides": [ + { + "item": 2, + "price": "12.00" + } + ], + "variation_price_overrides": [], + "meta_data": {} + } + + + :param organizer: The ``slug`` field of a valid organizer + :param event: The ``slug`` field of the main event + :statuscode 201: no error + :statuscode 400: The sub-event could not be created due to invalid submitted data. + :statuscode 401: Authentication failure + :statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource. + + .. http:get:: /api/v1/organizers/(organizer)/events/(event)/subevents/(id)/ Returns information on one sub-event, identified by its ID. @@ -149,13 +223,106 @@ Endpoints "meta_data": {} } - :param organizer: The ``slug`` field of the organizer to fetch - :param event: The ``slug`` field of the event to fetch - :param id: The ``slug`` field of the sub-event to fetch + :param organizer: The ``slug`` field of a valid organizer + :param event: The ``slug`` field of the main event + :param id: The ``id`` field of the sub-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 it. +.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/subevents/(id)/ + + Updates a sub-event, identified by its ID. 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. + + Permission required: "Can change event settings" + + **Example request**: + + .. sourcecode:: http + + PATCH /api/v1/organizers/bigevents/events/sampleconf/subevents/1/ HTTP/1.1 + Host: pretix.eu + Accept: application/json, text/javascript + Content: application/json + + { + "name": {"en": "New Subevent Name"}, + "item_price_overrides": [ + { + "item": 2, + "price": "23.42" + } + ], + } + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: application/json + + { + "id": 1, + "name": {"en": "New Subevent Name"}, + "event": "sampleconf", + "active": false, + "date_from": "2017-12-27T10:00:00Z", + "date_to": null, + "date_admission": null, + "presale_start": null, + "presale_end": null, + "location": null, + "item_price_overrides": [ + { + "item": 2, + "price": "23.42" + } + ], + "variation_price_overrides": [], + "meta_data": {} + } + + :param organizer: The ``slug`` field of a valid organizer + :param event: The ``slug`` field of the main event + :param id: The ``id`` field of the sub-event to update + :statuscode 200: no error + :statuscode 400: The sub-event could not be created due to invalid submitted data. + :statuscode 401: Authentication failure + :statuscode 403: The requested organizer/sub-event does not exist **or** you have no permission to update this resource. + +.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/subevents/(id)/ + + Delete a sub-event. Note that events with orders cannot be deleted to ensure data integrity. + + Permission required: "Can change event settings" + + **Example request**: + + .. sourcecode:: http + + DELETE /api/v1/organizers/bigevents/events/sampleconf/subevents/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 a valid organizer + :param event: The ``slug`` field of the main event + :param id: The ``id`` field of the sub-event to delete + :statuscode 204: no error + :statuscode 401: Authentication failure + :statuscode 403: The requested organizer/sub-event does not exist **or** you have no permission to delete this resource. + + .. http:get:: /api/v1/organizers/(organizer)/subevents/ Returns a list of all sub-events of any event series you have access to within an organizer account. diff --git a/src/pretix/api/serializers/event.py b/src/pretix/api/serializers/event.py index 6ca15e9328..4874bef240 100644 --- a/src/pretix/api/serializers/event.py +++ b/src/pretix/api/serializers/event.py @@ -191,8 +191,8 @@ class SubEventItemVariationSerializer(I18nAwareModelSerializer): class SubEventSerializer(I18nAwareModelSerializer): - item_price_overrides = SubEventItemSerializer(source='subeventitem_set', many=True) - variation_price_overrides = SubEventItemVariationSerializer(source='subeventitemvariation_set', many=True) + item_price_overrides = SubEventItemSerializer(source='subeventitem_set', many=True, required=False) + variation_price_overrides = SubEventItemVariationSerializer(source='subeventitemvariation_set', many=True, required=False) event = SlugRelatedField(slug_field='slug', read_only=True) meta_data = MetaDataField(source='*') @@ -202,6 +202,103 @@ class SubEventSerializer(I18nAwareModelSerializer): 'presale_start', 'presale_end', 'location', 'event', 'item_price_overrides', 'variation_price_overrides', 'meta_data') + def validate(self, data): + data = super().validate(data) + event = self.context['request'].event + + full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {} + full_data.update(data) + + Event.clean_dates(data.get('date_from'), data.get('date_to')) + Event.clean_presale(data.get('presale_start'), data.get('presale_end')) + + SubEvent.clean_items(event, [item['item'] for item in full_data.get('subeventitem_set')]) + SubEvent.clean_variations(event, [item['variation'] for item in full_data.get('subeventitemvariation_set')]) + return data + + def validate_item_price_overrides(self, data): + return list(filter(lambda i: 'item' in i, data)) + + def validate_variation_price_overrides(self, data): + return list(filter(lambda i: 'variation' in i, data)) + + @cached_property + def meta_properties(self): + return { + p.name: p for p in self.context['request'].organizer.meta_properties.all() + } + + def validate_meta_data(self, value): + for key in value['meta_data'].keys(): + if key not in self.meta_properties: + raise ValidationError(_('Meta data property \'{name}\' does not exist.').format(name=key)) + return value + + @transaction.atomic + def create(self, validated_data): + item_price_overrides_data = validated_data.pop('subeventitem_set') if 'subeventitem_set' in validated_data else {} + variation_price_overrides_data = validated_data.pop('subeventitemvariation_set') if 'subeventitemvariation_set' in validated_data else {} + meta_data = validated_data.pop('meta_data', None) + subevent = super().create(validated_data) + + for item_price_override_data in item_price_overrides_data: + SubEventItem.objects.create(subevent=subevent, **item_price_override_data) + for variation_price_override_data in variation_price_overrides_data: + SubEventItemVariation.objects.create(subevent=subevent, **variation_price_override_data) + + # Meta data + if meta_data is not None: + for key, value in meta_data.items(): + subevent.meta_values.create( + property=self.meta_properties.get(key), + value=value + ) + + return subevent + + @transaction.atomic + def update(self, instance, validated_data): + item_price_overrides_data = validated_data.pop('subeventitem_set') if 'subeventitem_set' in validated_data else {} + variation_price_overrides_data = validated_data.pop('subeventitemvariation_set') if 'subeventitemvariation_set' in validated_data else {} + meta_data = validated_data.pop('meta_data', None) + subevent = super().update(instance, validated_data) + + existing_item_overrides = {item.item: item.id for item in SubEventItem.objects.filter(subevent=subevent)} + + for item_price_override_data in item_price_overrides_data: + id = existing_item_overrides.pop(item_price_override_data['item'], None) + SubEventItem(id=id, subevent=subevent, **item_price_override_data).save() + + SubEventItem.objects.filter(id__in=existing_item_overrides.values()).delete() + + existing_variation_overrides = {item.variation: item.id for item in SubEventItemVariation.objects.filter(subevent=subevent)} + + for variation_price_override_data in variation_price_overrides_data: + id = existing_variation_overrides.pop(variation_price_override_data['variation'], None) + SubEventItemVariation(id=id, subevent=subevent, **variation_price_override_data).save() + + SubEventItemVariation.objects.filter(id__in=existing_variation_overrides.values()).delete() + + # Meta data + if meta_data is not None: + current = {mv.property: mv for mv in subevent.meta_values.select_related('property')} + for key, value in meta_data.items(): + prop = self.meta_properties.get(key) + if prop in current: + current[prop].value = value + current[prop].save() + else: + subevent.meta_values.create( + property=self.meta_properties.get(key), + value=value + ) + + for prop, current_object in current.items(): + if prop.name not in meta_data: + current_object.delete() + + return subevent + class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer): class Meta: diff --git a/src/pretix/api/views/event.py b/src/pretix/api/views/event.py index 75ce37d5f0..a409c2b051 100644 --- a/src/pretix/api/views/event.py +++ b/src/pretix/api/views/event.py @@ -217,9 +217,10 @@ class SubEventFilter(FilterSet): return queryset.exclude(expr) -class SubEventViewSet(ConditionalListView, viewsets.ReadOnlyModelViewSet): +class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet): serializer_class = SubEventSerializer queryset = ItemCategory.objects.none() + write_permission = 'can_change_event_settings' filter_backends = (DjangoFilterBackend, filters.OrderingFilter) filterset_class = SubEventFilter @@ -240,6 +241,42 @@ class SubEventViewSet(ConditionalListView, viewsets.ReadOnlyModelViewSet): 'subeventitem_set', 'subeventitemvariation_set' ) + def perform_update(self, serializer): + super().perform_update(serializer) + + serializer.instance.log_action( + 'pretix.subevent.changed', + user=self.request.user, + auth=self.request.auth, + data=self.request.data + ) + + def perform_create(self, serializer): + serializer.save(event=self.request.event) + serializer.instance.log_action( + 'pretix.subevent.added', + user=self.request.user, + auth=self.request.auth, + data=self.request.data + ) + + def perform_destroy(self, instance): + if not instance.allow_delete(): + raise PermissionDenied('The sub-event can not be deleted as it has already been used in orders. Please set' + ' \'active\' to false instead to hide it from users.') + try: + with transaction.atomic(): + instance.log_action( + 'pretix.subevent.deleted', + user=self.request.user, + auth=self.request.auth, + data=self.request.data + ) + super().perform_destroy(instance) + except ProtectedError: + raise PermissionDenied('The sub-event could not be deleted as some constraints (e.g. data created by ' + 'plug-ins) do not allow it.') + class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet): serializer_class = TaxRuleSerializer diff --git a/src/pretix/base/models/event.py b/src/pretix/base/models/event.py index 21cc43199f..2f58b9e91f 100644 --- a/src/pretix/base/models/event.py +++ b/src/pretix/base/models/event.py @@ -936,6 +936,18 @@ class SubEvent(EventMixin, LoggedModel): if self.event: self.event.cache.clear() + @staticmethod + def clean_items(event, items): + for item in items: + if event != item.event: + raise ValidationError(_('One or more items do not belong to this event.')) + + @staticmethod + def clean_variations(event, variations): + for variation in variations: + if event != variation.item.event: + raise ValidationError(_('One or more variations do not belong to this event.')) + def generate_invite_token(): return get_random_string(length=32, allowed_chars=string.ascii_lowercase + string.digits) diff --git a/src/tests/api/test_subevents.py b/src/tests/api/test_subevents.py index 70cb25cd02..e7bc221165 100644 --- a/src/tests/api/test_subevents.py +++ b/src/tests/api/test_subevents.py @@ -1,4 +1,66 @@ +from datetime import datetime +from decimal import Decimal +from unittest import mock + import pytest +from django_countries.fields import Country +from pytz import UTC + +from pretix.base.models import InvoiceAddress, Order, OrderPosition +from pretix.base.models.orders import OrderFee + + +@pytest.fixture +def variations(item): + v = list() + v.append(item.variations.create(value="ChildA1", default_price='12.00')) + v.append(item.variations.create(value="ChildA2", default_price='13.00')) + return v + + +@pytest.fixture +def variations2(item2): + v = list() + v.append(item2.variations.create(value="ChildB1", default_price='12.00')) + v.append(item2.variations.create(value="ChildB2", default_price='13.00')) + return v + + +@pytest.fixture +def order(event, item, taxrule): + testtime = datetime(2017, 12, 1, 10, 0, 0, tzinfo=UTC) + + with mock.patch('django.utils.timezone.now') as mock_now: + mock_now.return_value = testtime + o = Order.objects.create( + code='FOO', event=event, email='dummy@dummy.test', + status=Order.STATUS_PENDING, secret="k24fiuwvu8kxz3y1", + datetime=datetime(2017, 12, 1, 10, 0, 0, tzinfo=UTC), + expires=datetime(2017, 12, 10, 10, 0, 0, tzinfo=UTC), + total=23, locale='en' + ) + o.fees.create(fee_type=OrderFee.FEE_TYPE_PAYMENT, value=Decimal('0.25'), tax_rate=Decimal('19.00'), + tax_value=Decimal('0.05'), tax_rule=taxrule) + InvoiceAddress.objects.create(order=o, company="Sample company", country=Country('NZ')) + return o + + +@pytest.fixture +def order_position(item, order, subevent, taxrule, variations): + op = OrderPosition.objects.create( + order=order, + item=item, + subevent=subevent, + variation=variations[0], + tax_rule=taxrule, + tax_rate=taxrule.rate, + tax_value=Decimal("3"), + price=Decimal("23"), + attendee_name_parts={'full_name': "Peter"}, + secret="z3fsn8jyufm5kpk768q69gkbyr5f4h6w" + ) + return op + TEST_SUBEVENT_RES = { 'active': False, @@ -17,6 +79,16 @@ TEST_SUBEVENT_RES = { } +@pytest.fixture +def item(event): + return event.items.create(name="Budget Ticket", default_price=23) + + +@pytest.fixture +def item2(event2): + return event2.items.create(name="Another Ticket", default_price=23) + + @pytest.mark.django_db def test_subevent_list(token_client, organizer, event, subevent): res = dict(TEST_SUBEVENT_RES) @@ -50,6 +122,412 @@ def test_subevent_list(token_client, organizer, event, subevent): assert [] == resp.data['results'] +@pytest.mark.django_db +def test_subevent_get(token_client, organizer, event, subevent): + res = dict(TEST_SUBEVENT_RES) + res["id"] = subevent.pk + resp = token_client.get('/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, + subevent.pk)) + assert resp.status_code == 200 + assert res == resp.data + + +@pytest.mark.django_db +def test_subevent_create(token_client, organizer, event, subevent, meta_prop, item): + resp = token_client.post( + '/api/v1/organizers/{}/events/{}/subevents/'.format(organizer.slug, event.slug), + { + "name": { + "de": "Demo Subevent 2020 Test", + "en": "Demo Subevent 2020 Test" + }, + "active": False, + "date_from": "2017-12-27T10:00:00Z", + "date_to": "2017-12-28T10:00:00Z", + "date_admission": None, + "presale_start": None, + "presale_end": None, + "location": None, + "item_price_overrides": [], + "variation_price_overrides": [], + "meta_data": { + "type": "Workshop" + }, + }, + format='json' + ) + assert resp.status_code == 201 + assert not subevent.active + assert subevent.meta_values.filter( + property__name=meta_prop.name, value="Workshop" + ).exists() + + resp = token_client.post( + '/api/v1/organizers/{}/events/{}/subevents/'.format(organizer.slug, event.slug), + { + "name": { + "de": "Demo Subevent 2020 Test", + "en": "Demo Subevent 2020 Test" + }, + "active": False, + "date_from": "2017-12-27T10:00:00Z", + "date_to": "2017-12-28T10:00:00Z", + "date_admission": None, + "presale_start": None, + "presale_end": None, + "location": None, + "item_price_overrides": [], + "variation_price_overrides": [], + "meta_data": { + "foo": "bar" + }, + }, + format='json' + ) + assert resp.status_code == 400 + assert resp.content.decode() == '{"meta_data":["Meta data property \'foo\' does not exist."]}' + + resp = token_client.post( + '/api/v1/organizers/{}/events/{}/subevents/'.format(organizer.slug, event.slug), + { + "name": { + "de": "Demo Subevent 2020 Test", + "en": "Demo Subevent 2020 Test" + }, + "active": False, + "date_from": "2017-12-27T10:00:00Z", + "date_to": "2017-12-28T10:00:00Z", + "date_admission": None, + "presale_start": None, + "presale_end": None, + "location": None, + "item_price_overrides": [ + { + "item": item.pk, + "price": "23.42" + } + ], + "variation_price_overrides": [], + "meta_data": { + "type": "Workshop" + }, + }, + format='json' + ) + assert resp.status_code == 201 + assert item.default_price == Decimal('23.00') + assert event.subevents.get(id=resp.data['id']).item_price_overrides[item.pk] == Decimal('23.42') + + resp = token_client.post( + '/api/v1/organizers/{}/events/{}/subevents/'.format(organizer.slug, event.slug), + { + "name": { + "de": "Demo Subevent 2020 Test", + "en": "Demo Subevent 2020 Test" + }, + "active": False, + "date_from": "2017-12-27T10:00:00Z", + "date_to": "2017-12-28T10:00:00Z", + "date_admission": None, + "presale_start": None, + "presale_end": None, + "location": None, + "item_price_overrides": [ + { + "item": 555, + "price": "23.42" + } + ], + "variation_price_overrides": [], + "meta_data": { + "type": "Workshop" + }, + }, + format='json' + ) + assert resp.status_code == 400 + assert resp.content.decode() == '{"item_price_overrides":[{"item":["Invalid pk \\"555\\" - object does not exist."]}]}' + + +@pytest.mark.django_db +def test_subevent_update(token_client, organizer, event, subevent, item, item2, meta_prop, variations, variations2): + resp = token_client.patch( + '/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk), + { + "date_from": "2018-12-27T10:00:00Z", + "date_to": "2018-12-28T10:00:00Z", + }, + format='json' + ) + assert resp.status_code == 200 + subevent = event.subevents.get(id=subevent.id) + assert subevent.date_from == datetime(2018, 12, 27, 10, 0, tzinfo=UTC) + assert subevent.date_to == datetime(2018, 12, 28, 10, 0, tzinfo=UTC) + + resp = token_client.patch( + '/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk), + { + "date_from": "2017-12-27T10:00:00Z", + "date_to": "2017-12-26T10:00:00Z" + }, + format='json' + ) + assert resp.status_code == 400 + assert resp.content.decode() == '{"non_field_errors":["The event cannot end before it starts."]}' + + resp = token_client.patch( + '/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk), + { + "presale_start": "2017-12-27T10:00:00Z", + "presale_end": "2017-12-26T10:00:00Z" + }, + format='json' + ) + assert resp.status_code == 400 + assert resp.content.decode() == '{"non_field_errors":["The event\'s presale cannot end before it starts."]}' + + resp = token_client.patch( + '/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk), + { + "meta_data": { + meta_prop.name: "Conference" + } + }, + format='json' + ) + assert resp.status_code == 200 + assert organizer.events.get(slug=event.slug).subevents.get(id=resp.data['id']).meta_values.filter( + property__name=meta_prop.name, value="Conference" + ).exists() + + resp = token_client.patch( + '/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk), + { + "meta_data": { + } + }, + format='json' + ) + assert resp.status_code == 200 + assert not subevent.meta_values.filter( + property__name=meta_prop.name + ).exists() + + resp = token_client.patch( + '/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk), + { + "meta_data": { + "test": "test" + } + }, + format='json' + ) + assert resp.status_code == 400 + assert resp.content.decode() == '{"meta_data":["Meta data property \'test\' does not exist."]}' + + resp = token_client.patch( + '/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk), + { + "item_price_overrides": [ + { + "item": item.pk, + "price": "99.99" + } + ], + }, + format='json' + ) + assert resp.status_code == 200 + assert subevent.items.get(id=item.pk).default_price == Decimal('23.00') + assert subevent.item_price_overrides[item.pk] == Decimal('99.99') + + resp = token_client.patch( + '/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk), + { + "item_price_overrides": [ + { + "item": item.pk, + "price": "88.88" + } + ], + }, + format='json' + ) + assert resp.status_code == 200 + assert event.subevents.get(id=subevent.id).item_price_overrides[item.pk] == Decimal('88.88') + + resp = token_client.patch( + '/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk), + { + "item_price_overrides": [ + { + "item": item.pk, + "price": None + } + ], + }, + format='json' + ) + assert resp.status_code == 200 + assert item.pk not in event.subevents.get(id=subevent.id).item_price_overrides + + resp = token_client.patch( + '/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk), + { + "item_price_overrides": [ + { + "item": item.pk, + "price": "12.34" + } + ], + }, + format='json' + ) + assert resp.status_code == 200 + assert event.subevents.get(id=subevent.id).item_price_overrides[item.pk] == Decimal('12.34') + + resp = token_client.patch( + '/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk), + { + "item_price_overrides": [], + }, + format='json' + ) + assert resp.status_code == 200 + assert item.pk not in event.subevents.get(id=subevent.id).item_price_overrides + + resp = token_client.patch( + '/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk), + { + "item_price_overrides": [ + { + "item": 123, + "price": "99.99" + } + ], + }, + format='json' + ) + assert resp.status_code == 400 + assert resp.content.decode() == '{"item_price_overrides":[{"item":["Invalid pk \\"123\\" - object does not exist."]}]}' + + resp = token_client.patch( + '/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk), + { + "item_price_overrides": [ + { + "item": item2.id, + "price": "99.99" + } + ], + }, + format='json' + ) + assert resp.status_code == 400 + assert resp.content.decode() == '{"non_field_errors":["One or more items do not belong to this event."]}' + + resp = token_client.patch( + '/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk), + { + "variation_price_overrides": [ + { + "variation": variations[0].pk, + "price": "99.99" + } + ], + }, + format='json' + ) + assert resp.status_code == 200 + assert subevent.variations.get(id=variations[0].pk).default_price == Decimal('12.00') + assert subevent.var_price_overrides[variations[0].pk] == Decimal('99.99') + + resp = token_client.patch( + '/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk), + { + "variation_price_overrides": [ + { + "variation": variations[0].pk, + "price": "88.88" + } + ], + }, + format='json' + ) + assert resp.status_code == 200 + assert event.subevents.get(id=subevent.id).var_price_overrides[variations[0].pk] == Decimal('88.88') + + resp = token_client.patch( + '/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk), + { + "variation_price_overrides": [ + { + "variation": variations[0].pk, + "price": None + } + ], + }, + format='json' + ) + assert resp.status_code == 200 + assert variations[0].pk not in event.subevents.get(id=subevent.id).var_price_overrides + + resp = token_client.patch( + '/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk), + { + "variation_price_overrides": [ + { + "variation": variations[0].pk, + "price": "12.34" + } + ], + }, + format='json' + ) + assert resp.status_code == 200 + assert event.subevents.get(id=subevent.id).var_price_overrides[variations[0].pk] == Decimal('12.34') + + resp = token_client.patch( + '/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk), + { + "variation_price_overrides": [], + }, + format='json' + ) + assert resp.status_code == 200 + assert variations[0].pk not in event.subevents.get(id=subevent.id).var_price_overrides + + resp = token_client.patch( + '/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk), + { + "variation_price_overrides": [ + { + "variation": 123, + "price": "99.99" + } + ], + }, + format='json' + ) + assert resp.status_code == 400 + assert resp.content.decode() == '{"variation_price_overrides":[{"variation":["Invalid pk \\"123\\" - object does not exist."]}]}' + + resp = token_client.patch( + '/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk), + { + "variation_price_overrides": [ + { + "variation": variations2[0].pk, + "price": "99.99" + } + ], + }, + format='json' + ) + assert resp.status_code == 400 + assert resp.content.decode() == '{"non_field_errors":["One or more variations do not belong to this event."]}' + + @pytest.mark.django_db def test_subevent_detail(token_client, organizer, event, subevent): res = dict(TEST_SUBEVENT_RES) @@ -58,3 +536,21 @@ def test_subevent_detail(token_client, organizer, event, subevent): subevent.pk)) assert resp.status_code == 200 assert res == resp.data + + +@pytest.mark.django_db +def test_subevent_delete(token_client, organizer, event, subevent): + resp = token_client.delete('/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, + subevent.pk)) + assert resp.status_code == 204 + assert not event.subevents.filter(pk=subevent.id).exists() + + +@pytest.mark.django_db +def test_subevent_with_order_position_not_delete(token_client, organizer, event, subevent, item, order_position): + resp = token_client.delete('/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, + subevent.pk)) + assert resp.status_code == 403 + assert resp.content.decode() == '{"detail":"The sub-event can not be deleted as it has already been used in ' \ + 'orders. Please set \'active\' to false instead to hide it from users."}' + assert event.subevents.filter(pk=subevent.id).exists()