diff --git a/doc/api/resources/events.rst b/doc/api/resources/events.rst index 25b641f6b7..b8395bc92e 100644 --- a/doc/api/resources/events.rst +++ b/doc/api/resources/events.rst @@ -80,6 +80,10 @@ Endpoints The events resource can now be filtered by meta data attributes. +.. versionchanged:: 4.0 + + The ``clone_from`` parameter has been added to the event creation endpoint. + .. http:get:: /api/v1/organizers/(organizer)/events/ Returns a list of all events within a given organizer the authenticated user/token has access to. @@ -321,6 +325,9 @@ Endpoints } :param organizer: The ``slug`` field of the organizer of the event to create. + :query clone_from: Set to ``event_slug`` to clone data (settings, products, …) from an event with this slug in the + same organizer or to ``organizer_slug/event_slug`` to clone from an event within a different + organizer. :statuscode 201: no error :statuscode 400: The event could not be created due to invalid submitted data. :statuscode 401: Authentication failure @@ -335,7 +342,8 @@ Endpoints If the ``plugins``, ``has_subevents`` and/or ``is_public`` fields are present in the post body this will determine their value. Otherwise their value will be copied from the existing event. - Please note that you can only copy from events under the same organizer. + Please note that you can only copy from events under the same organizer this way. Use the ``clone_from`` parameter + when creating a new event for this instead. Permission required: "Can create events" diff --git a/src/pretix/api/views/event.py b/src/pretix/api/views/event.py index f097a3a87b..d93b67ed5e 100644 --- a/src/pretix/api/views/event.py +++ b/src/pretix/api/views/event.py @@ -39,7 +39,7 @@ from django.utils.timezone import now from django_filters.rest_framework import DjangoFilterBackend, FilterSet from django_scopes import scopes_disabled from rest_framework import filters, serializers, views, viewsets -from rest_framework.exceptions import PermissionDenied +from rest_framework.exceptions import PermissionDenied, ValidationError from rest_framework.response import Response from pretix.api.auth.permission import EventCRUDPermission @@ -120,6 +120,13 @@ class EventViewSet(viewsets.ModelViewSet): ordering_fields = ('date_from', 'slug') filterset_class = EventFilter + def get_copy_from_queryset(self): + if isinstance(self.request.auth, (TeamAPIToken, Device)): + return self.request.auth.get_events_with_any_permission() + elif self.request.user.is_authenticated: + return self.request.user.get_events_with_any_permission(self.request) + return Event.objects.none() + def get_queryset(self): if isinstance(self.request.auth, (TeamAPIToken, Device)): qs = self.request.auth.get_events_with_any_permission() @@ -173,8 +180,45 @@ class EventViewSet(viewsets.ModelViewSet): ) def perform_create(self, serializer): - serializer.save(organizer=self.request.organizer) - serializer.instance.set_defaults() + copy_from = None + if 'clone_from' in self.request.GET: + src = self.request.GET.get('clone_from') + try: + if '/' in src: + copy_from = self.get_copy_from_queryset().get( + organizer__slug=src.split('/')[0], + slug=src.split('/')[1] + ) + else: + copy_from = self.get_copy_from_queryset().get( + organizer=self.request.organizer, + slug=src + ) + except Event.DoesNotExist: + raise ValidationError('Event to copy from was not found') + + print(copy_from, self.request.GET) + new_event = serializer.save(organizer=self.request.organizer) + + if copy_from: + new_event.copy_data_from(copy_from) + + if 'plugins' in serializer.validated_data: + new_event.set_active_plugins(serializer.validated_data['plugins']) + if 'is_public' in serializer.validated_data: + new_event.is_public = serializer.validated_data['is_public'] + if 'testmode' in serializer.validated_data: + new_event.testmode = serializer.validated_data['testmode'] + if 'sales_channels' in serializer.validated_data: + new_event.sales_channels = serializer.validated_data['sales_channels'] + if 'has_subevents' in serializer.validated_data: + new_event.has_subevents = serializer.validated_data['has_subevents'] + new_event.save() + if 'timezone' in serializer.validated_data: + new_event.settings.timezone = serializer.validated_data['timezone'] + else: + serializer.instance.set_defaults() + serializer.instance.log_action( 'pretix.event.added', user=self.request.user, diff --git a/src/tests/api/test_events.py b/src/tests/api/test_events.py index d8bdd1df9f..d33fc83751 100644 --- a/src/tests/api/test_events.py +++ b/src/tests/api/test_events.py @@ -40,12 +40,13 @@ from unittest import mock import pytest from django.conf import settings from django.core.files.base import ContentFile +from django.utils.timezone import now from django_countries.fields import Country from django_scopes import scopes_disabled from pytz import UTC from pretix.base.models import ( - Event, InvoiceAddress, Order, OrderPosition, SeatingPlan, + Event, InvoiceAddress, Order, OrderPosition, Organizer, SeatingPlan, ) from pretix.base.models.orders import OrderFee from pretix.testutils.mock import mocker_context @@ -351,9 +352,13 @@ def test_event_create(team, token_client, organizer, event, meta_prop): @pytest.mark.django_db -def test_event_create_with_clone(token_client, organizer, event, meta_prop): +@pytest.mark.parametrize("urlstyle", [ + '/api/v1/organizers/{}/events/{}/clone/', + '/api/v1/organizers/{}/events/?clone_from={}', +]) +def test_event_create_with_clone(token_client, organizer, event, meta_prop, urlstyle): resp = token_client.post( - '/api/v1/organizers/{}/events/{}/clone/'.format(organizer.slug, event.slug), + urlstyle.format(organizer.slug, event.slug), { "name": { "de": "Demo Konference 2020 Test", @@ -393,7 +398,7 @@ def test_event_create_with_clone(token_client, organizer, event, meta_prop): assert cloned_event.settings.timezone == "Europe/Vienna" resp = token_client.post( - '/api/v1/organizers/{}/events/{}/clone/'.format(organizer.slug, event.slug), + urlstyle.format(organizer.slug, event.slug), { "name": { "de": "Demo Konference 2020 Test", @@ -425,7 +430,7 @@ def test_event_create_with_clone(token_client, organizer, event, meta_prop): ).exists() resp = token_client.post( - '/api/v1/organizers/{}/events/{}/clone/'.format(organizer.slug, event.slug), + urlstyle.format(organizer.slug, event.slug), { "name": { "de": "Demo Konference 2020 Test", @@ -451,6 +456,93 @@ def test_event_create_with_clone(token_client, organizer, event, meta_prop): assert cloned_event.plugins == "" +@pytest.mark.django_db +def test_event_create_with_clone_unknown_source(user, user_client, organizer, event): + with scopes_disabled(): + target_org = Organizer.objects.create(name='Dummy', slug='dummy2') + target_org.events.create(slug='bar', name='bar', date_from=now()) + resp = user_client.post( + '/api/v1/organizers/{}/events/?clone_from={}/{}'.format(organizer.slug, 'dummy2', 'bar'), + { + "name": { + "de": "Demo Konference 2020 Test", + "en": "Demo Conference 2020 Test" + }, + "live": False, + "testmode": True, + "currency": "EUR", + "date_from": "2018-12-27T10:00:00Z", + "date_to": "2018-12-28T10:00:00Z", + "date_admission": None, + "is_public": False, + "presale_start": None, + "presale_end": None, + "location": None, + "slug": "2030", + "plugins": [ + "pretix.plugins.ticketoutputpdf" + ], + "timezone": "Europe/Vienna" + }, + format='json' + ) + assert resp.status_code == 400 + + +@pytest.mark.django_db +def test_event_create_with_clone_across_organizers(user, user_client, organizer, event, taxrule): + with scopes_disabled(): + target_org = Organizer.objects.create(name='Dummy', slug='dummy2') + team = target_org.teams.create( + name="Test-Team", + 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_change_organizer_settings=True + ) + team.members.add(user) + + resp = user_client.post( + '/api/v1/organizers/{}/events/?clone_from={}/{}'.format(target_org.slug, organizer.slug, event.slug), + { + "name": { + "de": "Demo Konference 2020 Test", + "en": "Demo Conference 2020 Test" + }, + "live": False, + "testmode": True, + "currency": "EUR", + "date_from": "2018-12-27T10:00:00Z", + "date_to": "2018-12-28T10:00:00Z", + "date_admission": None, + "is_public": False, + "presale_start": None, + "presale_end": None, + "location": None, + "slug": "2030", + "plugins": [ + "pretix.plugins.ticketoutputpdf" + ], + "timezone": "Europe/Vienna" + }, + format='json' + ) + assert resp.status_code == 201 + with scopes_disabled(): + cloned_event = Event.objects.get(organizer=target_org.pk, slug='2030') + assert cloned_event.plugins == 'pretix.plugins.ticketoutputpdf' + assert cloned_event.is_public is False + assert cloned_event.testmode + assert cloned_event.settings.timezone == "Europe/Vienna" + assert cloned_event.tax_rules.exists() + + @pytest.mark.django_db def test_event_put_with_clone(token_client, organizer, event, meta_prop): resp = token_client.put(