From ee951a7448ec57f8190824c6e52bf2b5f5cb6a45 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Mon, 24 Sep 2018 12:59:44 +0200 Subject: [PATCH] API: Add subevent list on organizer level --- doc/api/resources/subevents.rst | 71 +++++++++++++++++++++++++++++ src/pretix/api/serializers/event.py | 4 +- src/pretix/api/urls.py | 1 + src/pretix/api/views/__init__.py | 3 ++ src/pretix/api/views/event.py | 49 ++++++++++++++++++-- src/tests/api/test_subevents.py | 18 ++++++++ 6 files changed, 142 insertions(+), 4 deletions(-) diff --git a/doc/api/resources/subevents.rst b/doc/api/resources/subevents.rst index 80fa4717da..b0734a2d23 100644 --- a/doc/api/resources/subevents.rst +++ b/doc/api/resources/subevents.rst @@ -17,6 +17,7 @@ Field Type Description ===================================== ========================== ======================================================= id integer Internal ID of the sub-event name multi-lingual string The sub-event's full name +event string The slug of the parent event active boolean If ``true``, the sub-event ticket shop is publicly available. date_from datetime The sub-event's start date @@ -40,6 +41,10 @@ meta_data dict Values set for The ``meta_data`` field has been added. +.. versionchanged:: 2.1 + + The ``event`` field has been added, together with filters on the list of dates and an organizer-level list. + Endpoints --------- @@ -72,6 +77,7 @@ Endpoints { "id": 1, "name": {"en": "First Sample Conference"}, + "event": "sampleconf", "active": false, "date_from": "2017-12-27T10:00:00Z", "date_to": null, @@ -92,6 +98,10 @@ Endpoints } :query page: The page number in case of a multi-page result set, default is 1 + :query active: If set to ``true``/``false``, only events with a matching value of ``active`` are returned. + :query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned. + :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 :statuscode 200: no error @@ -121,6 +131,7 @@ Endpoints { "id": 1, "name": {"en": "First Sample Conference"}, + "event": "sampleconf", "active": false, "date_from": "2017-12-27T10:00:00Z", "date_to": null, @@ -144,3 +155,63 @@ 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 it. + +.. 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. + + **Example request**: + + .. sourcecode:: http + + GET /api/v1/organizers/bigevents/subevents/ 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 + + { + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "id": 1, + "name": {"en": "First Sample Conference"}, + "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": "12.00" + } + ], + "variation_price_overrides": [], + "meta_data": {} + } + ] + } + + :query page: The page number in case of a multi-page result set, default is 1 + :query active: If set to ``true``/``false``, only events with a matching value of ``active`` are returned. + :query event__live: If set to ``true``/``false``, only events with a matching value of ``live`` on the parent event are returned. + :query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned. + :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 + :statuscode 200: no error + :statuscode 401: Authentication failure + :statuscode 403: The requested organizer does not exist **or** you have no permission to view it. diff --git a/src/pretix/api/serializers/event.py b/src/pretix/api/serializers/event.py index 6480e7ed81..6511869b0a 100644 --- a/src/pretix/api/serializers/event.py +++ b/src/pretix/api/serializers/event.py @@ -4,6 +4,7 @@ from django.utils.functional import cached_property from django.utils.translation import ugettext as _ from django_countries.serializers import CountryFieldMixin from rest_framework.fields import Field +from rest_framework.relations import SlugRelatedField from pretix.api.serializers.i18n import I18nAwareModelSerializer from pretix.base.models import Event, TaxRule @@ -190,12 +191,13 @@ class SubEventItemVariationSerializer(I18nAwareModelSerializer): class SubEventSerializer(I18nAwareModelSerializer): item_price_overrides = SubEventItemSerializer(source='subeventitem_set', many=True) variation_price_overrides = SubEventItemVariationSerializer(source='subeventitemvariation_set', many=True) + event = SlugRelatedField(slug_field='slug', read_only=True) meta_data = MetaDataField(source='*') class Meta: model = SubEvent fields = ('id', 'name', 'date_from', 'date_to', 'active', 'date_admission', - 'presale_start', 'presale_end', 'location', + 'presale_start', 'presale_end', 'location', 'event', 'item_price_overrides', 'variation_price_overrides', 'meta_data') diff --git a/src/pretix/api/urls.py b/src/pretix/api/urls.py index cd084c461f..a59bf9a68f 100644 --- a/src/pretix/api/urls.py +++ b/src/pretix/api/urls.py @@ -15,6 +15,7 @@ router.register(r'organizers', organizer.OrganizerViewSet) orga_router = routers.DefaultRouter() orga_router.register(r'events', event.EventViewSet) +orga_router.register(r'subevents', event.SubEventViewSet) event_router = routers.DefaultRouter() event_router.register(r'subevents', event.SubEventViewSet) diff --git a/src/pretix/api/views/__init__.py b/src/pretix/api/views/__init__.py index bd6c5ea37a..c26f6ef1ec 100644 --- a/src/pretix/api/views/__init__.py +++ b/src/pretix/api/views/__init__.py @@ -37,6 +37,9 @@ class ConditionalListView: if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE') if if_unmodified_since: if_unmodified_since = parse_http_date_safe(if_unmodified_since) + if not hasattr(request, 'event'): + return super().list(request, **kwargs) + lmd = request.event.logentry_set.filter( content_type__model=self.queryset.model._meta.model_name, content_type__app_label=self.queryset.model._meta.app_label, diff --git a/src/pretix/api/views/event.py b/src/pretix/api/views/event.py index cf8bd8a629..9f429f44a0 100644 --- a/src/pretix/api/views/event.py +++ b/src/pretix/api/views/event.py @@ -74,7 +74,7 @@ class EventViewSet(viewsets.ModelViewSet): def get_queryset(self): if isinstance(self.request.auth, TeamAPIToken): - qs = self.request.auth.team.get_events_with_any_permission() + qs = self.request.auth.get_events_with_any_permission() elif self.request.user.is_authenticated: qs = self.request.user.get_events_with_any_permission(self.request).filter( organizer=self.request.organizer @@ -179,9 +179,40 @@ class CloneEventViewSet(viewsets.ModelViewSet): class SubEventFilter(FilterSet): + is_past = django_filters.rest_framework.BooleanFilter(method='is_past_qs') + is_future = django_filters.rest_framework.BooleanFilter(method='is_future_qs') + ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs') + class Meta: model = SubEvent - fields = ['active'] + fields = ['active', 'event__live'] + + def ends_after_qs(self, queryset, name, value): + expr = Q( + Q(Q(date_to__isnull=True) & Q(date_from__gte=value)) + | Q(Q(date_to__isnull=False) & Q(date_to__gte=value)) + ) + return queryset.filter(expr) + + def is_past_qs(self, queryset, name, value): + expr = Q( + Q(Q(date_to__isnull=True) & Q(date_from__lt=now())) + | Q(Q(date_to__isnull=False) & Q(date_to__lt=now())) + ) + if value: + return queryset.filter(expr) + else: + return queryset.exclude(expr) + + def is_future_qs(self, queryset, name, value): + expr = Q( + Q(Q(date_to__isnull=True) & Q(date_from__gte=now())) + | Q(Q(date_to__isnull=False) & Q(date_to__gte=now())) + ) + if value: + return queryset.filter(expr) + else: + return queryset.exclude(expr) class SubEventViewSet(ConditionalListView, viewsets.ReadOnlyModelViewSet): @@ -191,7 +222,19 @@ class SubEventViewSet(ConditionalListView, viewsets.ReadOnlyModelViewSet): filterset_class = SubEventFilter def get_queryset(self): - return self.request.event.subevents.prefetch_related( + if getattr(self.request, 'event', None): + qs = self.request.event.subevents + elif isinstance(self.request.auth, TeamAPIToken): + qs = SubEvent.objects.filter( + event__organizer=self.request.organizer, + event__in=self.request.auth.get_events_with_any_permission() + ) + elif self.request.user.is_authenticated: + qs = SubEvent.objects.filter( + event__organizer=self.request.organizer, + event__in=self.request.user.get_events_with_any_permission() + ) + return qs.prefetch_related( 'subeventitem_set', 'subeventitemvariation_set' ) diff --git a/src/tests/api/test_subevents.py b/src/tests/api/test_subevents.py index e058fc1fbd..70cb25cd02 100644 --- a/src/tests/api/test_subevents.py +++ b/src/tests/api/test_subevents.py @@ -2,6 +2,7 @@ import pytest TEST_SUBEVENT_RES = { 'active': False, + 'event': 'dummy', 'presale_start': None, 'date_to': None, 'date_admission': None, @@ -23,6 +24,9 @@ def test_subevent_list(token_client, organizer, event, subevent): resp = token_client.get('/api/v1/organizers/{}/events/{}/subevents/'.format(organizer.slug, event.slug)) assert resp.status_code == 200 assert [res] == resp.data['results'] + resp = token_client.get('/api/v1/organizers/{}/subevents/'.format(organizer.slug)) + assert resp.status_code == 200 + assert [res] == resp.data['results'] resp = token_client.get( '/api/v1/organizers/{}/events/{}/subevents/?active=false'.format(organizer.slug, event.slug)) @@ -31,6 +35,20 @@ def test_subevent_list(token_client, organizer, event, subevent): '/api/v1/organizers/{}/events/{}/subevents/?active=true'.format(organizer.slug, event.slug)) assert [] == resp.data['results'] + resp = token_client.get( + '/api/v1/organizers/{}/events/{}/subevents/?event__live=false'.format(organizer.slug, event.slug)) + assert [res] == resp.data['results'] + resp = token_client.get( + '/api/v1/organizers/{}/events/{}/subevents/?event__live=true'.format(organizer.slug, event.slug)) + assert [] == resp.data['results'] + + resp = token_client.get( + '/api/v1/organizers/{}/events/{}/subevents/?ends_after=2017-12-27T09:59:59Z'.format(organizer.slug, event.slug)) + assert [res] == resp.data['results'] + resp = token_client.get( + '/api/v1/organizers/{}/events/{}/subevents/?ends_after=2017-12-27T10:01:01Z'.format(organizer.slug, event.slug)) + assert [] == resp.data['results'] + @pytest.mark.django_db def test_subevent_detail(token_client, organizer, event, subevent):