mirror of
https://github.com/pretix/pretix.git
synced 2026-05-03 14:54:04 +00:00
Add API endpoint /seats to event (Z#23159536) (#4321)
* add API endpoint /seats to event
* fix logging
* add Seat annotations
* add seats endpoint for subevents
* return ids of occupying objects instead of boolean flags
* wip
* include orderposition instead of order in seat info
* add API documentation
* Apply suggestions from code review
Co-authored-by: Raphael Michel <michel@rami.io>
* Apply suggestions from code review
* Clarify API docs
* add api examples
* add test cases
* require can_view_orders permission for retrieving seats
* improve permission handling
* Revert "improve permission handling"
This reverts commit f32b532cc6.
* improve permission handling (minimal version)
* formatting
* add permission tests
* fix bug
* update permission checks
* Apply suggestions from code review
Co-authored-by: Raphael Michel <michel@rami.io>
* add tests for permission checks
* add tests for expand=voucher and expand=cartposition
* remove unused parameter
* test query count
* codestyle
---------
Co-authored-by: Raphael Michel <michel@rami.io>
This commit is contained in:
@@ -35,7 +35,7 @@
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.exceptions import PermissionDenied, ValidationError
|
||||
from django.db import transaction
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.functional import cached_property
|
||||
@@ -52,7 +52,8 @@ from pretix.api.serializers import (
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.settings import SettingsSerializer
|
||||
from pretix.base.models import (
|
||||
Device, Event, SalesChannel, TaxRule, TeamAPIToken,
|
||||
CartPosition, Device, Event, OrderPosition, SalesChannel, Seat, TaxRule,
|
||||
TeamAPIToken, Voucher,
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.items import (
|
||||
@@ -970,3 +971,77 @@ class ItemMetaPropertiesSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = ItemMetaProperty
|
||||
fields = ('id', 'name', 'default', 'required', 'allowed_values')
|
||||
|
||||
|
||||
def prefetch_by_id(items, qs, id_attr, target_attr):
|
||||
"""
|
||||
Prefetches a related object on each item in the given list of items by searching by id or another
|
||||
unique field. The id value is read from the attribute on item specified in `id_attr`, searched on queryset `qs` by
|
||||
the primary key, and the resulting prefetched model object is stored into `target_attr` on the item.
|
||||
"""
|
||||
ids = [getattr(item, id_attr) for item in items if getattr(item, id_attr)]
|
||||
if ids:
|
||||
result = qs.in_bulk(id_list=ids)
|
||||
for item in items:
|
||||
setattr(item, target_attr, result.get(getattr(item, id_attr)))
|
||||
|
||||
|
||||
class SeatSerializer(I18nAwareModelSerializer):
|
||||
orderposition = serializers.IntegerField(source='orderposition_id')
|
||||
cartposition = serializers.IntegerField(source='cartposition_id')
|
||||
voucher = serializers.IntegerField(source='voucher_id')
|
||||
|
||||
class Meta:
|
||||
model = Seat
|
||||
read_only_fields = (
|
||||
'id', 'subevent', 'zone_name', 'row_name', 'row_label',
|
||||
'seat_number', 'seat_label', 'seat_guid', 'product',
|
||||
'orderposition', 'cartposition', 'voucher',
|
||||
)
|
||||
fields = (
|
||||
'id', 'subevent', 'zone_name', 'row_name', 'row_label',
|
||||
'seat_number', 'seat_label', 'seat_guid', 'product', 'blocked',
|
||||
'orderposition', 'cartposition', 'voucher',
|
||||
)
|
||||
|
||||
def prefetch_expanded_data(self, items, request, expand_fields):
|
||||
if 'orderposition' in expand_fields:
|
||||
if 'can_view_orders' not in request.eventpermset:
|
||||
raise PermissionDenied('can_view_orders permission required for expand=orderposition')
|
||||
prefetch_by_id(items, OrderPosition.objects.prefetch_related('order'), 'orderposition_id', 'orderposition')
|
||||
if 'cartposition' in expand_fields:
|
||||
if 'can_view_orders' not in request.eventpermset:
|
||||
raise PermissionDenied('can_view_orders permission required for expand=cartposition')
|
||||
prefetch_by_id(items, CartPosition.objects, 'cartposition_id', 'cartposition')
|
||||
if 'voucher' in expand_fields:
|
||||
if 'can_view_vouchers' not in request.eventpermset:
|
||||
raise PermissionDenied('can_view_vouchers permission required for expand=voucher')
|
||||
prefetch_by_id(items, Voucher.objects, 'voucher_id', 'voucher')
|
||||
|
||||
def __init__(self, instance, *args, **kwargs):
|
||||
if not kwargs.get('data'):
|
||||
self.prefetch_expanded_data(instance if hasattr(instance, '__iter__') else [instance],
|
||||
kwargs['context']['request'],
|
||||
kwargs['context']['expand_fields'])
|
||||
|
||||
super().__init__(instance, *args, **kwargs)
|
||||
|
||||
if 'orderposition' in self.context['expand_fields']:
|
||||
from pretix.api.serializers.media import (
|
||||
NestedOrderPositionSerializer,
|
||||
)
|
||||
self.fields['orderposition'] = NestedOrderPositionSerializer(read_only=True, context=self.context['order_context'])
|
||||
try:
|
||||
del self.fields['orderposition'].fields['seat']
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
if 'cartposition' in self.context['expand_fields']:
|
||||
from pretix.api.serializers.cart import CartPositionSerializer
|
||||
self.fields['cartposition'] = CartPositionSerializer(read_only=True)
|
||||
del self.fields['cartposition'].fields['seat']
|
||||
|
||||
if 'voucher' in self.context['expand_fields']:
|
||||
from pretix.api.serializers.voucher import VoucherSerializer
|
||||
self.fields['voucher'] = VoucherSerializer(read_only=True)
|
||||
del self.fields['voucher'].fields['seat']
|
||||
|
||||
@@ -87,6 +87,7 @@ event_router.register(r'invoices', order.InvoiceViewSet)
|
||||
event_router.register(r'revokedsecrets', order.RevokedSecretViewSet, basename='revokedsecrets')
|
||||
event_router.register(r'blockedsecrets', order.BlockedSecretViewSet, basename='blockedsecrets')
|
||||
event_router.register(r'taxrules', event.TaxRuleViewSet)
|
||||
event_router.register(r'seats', event.SeatViewSet)
|
||||
event_router.register(r'waitinglistentries', waitinglist.WaitingListViewSet)
|
||||
event_router.register(r'checkinlists', checkin.CheckinListViewSet)
|
||||
event_router.register(r'cartpositions', cart.CartPositionViewSet)
|
||||
@@ -95,6 +96,9 @@ event_router.register(r'exporters', exporters.EventExportersViewSet, basename='e
|
||||
event_router.register(r'shredders', shredders.EventShreddersViewSet, basename='shredders')
|
||||
event_router.register(r'item_meta_properties', event.ItemMetaPropertiesViewSet)
|
||||
|
||||
subevent_router = routers.DefaultRouter()
|
||||
subevent_router.register(r'seats', event.SeatViewSet)
|
||||
|
||||
checkinlist_router = routers.DefaultRouter()
|
||||
checkinlist_router.register(r'positions', checkin.CheckinListPositionViewSet, basename='checkinlistpos')
|
||||
|
||||
@@ -132,6 +136,7 @@ urlpatterns = [
|
||||
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/settings/$', event.EventSettingsView.as_view(),
|
||||
name="event.settings"),
|
||||
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/', include(event_router.urls)),
|
||||
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/subevents/(?P<subevent>\d+)/', include(subevent_router.urls)),
|
||||
re_path(r'^organizers/(?P<organizer>[^/]+)/teams/(?P<team>[^/]+)/', include(team_router.urls)),
|
||||
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/items/(?P<item>[^/]+)/', include(item_router.urls)),
|
||||
re_path(r'^organizers/(?P<organizer>[^/]+)/events/(?P<event>[^/]+)/questions/(?P<question>[^/]+)/',
|
||||
|
||||
@@ -40,7 +40,9 @@ from django.utils.timezone import now
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from django_scopes import scopes_disabled
|
||||
from rest_framework import serializers, views, viewsets
|
||||
from rest_framework.exceptions import PermissionDenied, ValidationError
|
||||
from rest_framework.exceptions import (
|
||||
NotFound, PermissionDenied, ValidationError,
|
||||
)
|
||||
from rest_framework.generics import get_object_or_404
|
||||
from rest_framework.response import Response
|
||||
|
||||
@@ -48,12 +50,12 @@ from pretix.api.auth.permission import EventCRUDPermission
|
||||
from pretix.api.pagination import TotalOrderingFilter
|
||||
from pretix.api.serializers.event import (
|
||||
CloneEventSerializer, DeviceEventSettingsSerializer, EventSerializer,
|
||||
EventSettingsSerializer, ItemMetaPropertiesSerializer, SubEventSerializer,
|
||||
TaxRuleSerializer,
|
||||
EventSettingsSerializer, ItemMetaPropertiesSerializer, SeatSerializer,
|
||||
SubEventSerializer, TaxRuleSerializer,
|
||||
)
|
||||
from pretix.api.views import ConditionalListView
|
||||
from pretix.base.models import (
|
||||
CartPosition, Device, Event, ItemMetaProperty, SeatCategoryMapping,
|
||||
CartPosition, Device, Event, ItemMetaProperty, Seat, SeatCategoryMapping,
|
||||
TaxRule, TeamAPIToken,
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
@@ -667,3 +669,44 @@ class EventSettingsView(views.APIView):
|
||||
'request': request
|
||||
})
|
||||
return Response(s.data)
|
||||
|
||||
|
||||
class SeatViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
serializer_class = SeatSerializer
|
||||
queryset = Seat.objects.none()
|
||||
write_permission = 'can_change_event_settings'
|
||||
filter_backends = (DjangoFilterBackend,)
|
||||
filterset_fields = ('zone_name', 'row_name', 'row_label', 'seat_number', 'seat_label', 'seat_guid', 'blocked',)
|
||||
|
||||
def get_queryset(self):
|
||||
if self.request.event.has_subevents and 'subevent' in self.request.resolver_match.kwargs:
|
||||
try:
|
||||
subevent = self.request.event.subevents.get(pk=self.request.resolver_match.kwargs['subevent'])
|
||||
except SubEvent.DoesNotExist:
|
||||
raise NotFound('Subevent not found')
|
||||
qs = Seat.annotated(event_id=self.request.event.id, subevent=subevent, qs=subevent.seats.all(), annotate_ids=True)
|
||||
elif not self.request.event.has_subevents and 'subevent' not in self.request.resolver_match.kwargs:
|
||||
qs = Seat.annotated(event_id=self.request.event.id, subevent=None, qs=self.request.event.seats.all(), annotate_ids=True)
|
||||
else:
|
||||
raise NotFound('Please use the subevent-specific endpoint' if self.request.event.has_subevents
|
||||
else 'This event has no subevents')
|
||||
|
||||
return qs
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['expand_fields'] = self.request.query_params.getlist('expand')
|
||||
ctx['order_context'] = {
|
||||
'event': self.request.event,
|
||||
'pdf_data': None,
|
||||
}
|
||||
return ctx
|
||||
|
||||
def perform_update(self, serializer):
|
||||
super().perform_update(serializer)
|
||||
serializer.instance.event.log_action(
|
||||
"pretix.event.seats.blocks.changed",
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={"seats": [serializer.instance.pk]},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user