diff --git a/doc/api/resources/seats.rst b/doc/api/resources/seats.rst index d43a11ff37..b9c6e60cc0 100644 --- a/doc/api/resources/seats.rst +++ b/doc/api/resources/seats.rst @@ -101,7 +101,8 @@ Endpoints :query string seat_number: Only show seats with the given seat_number. :query string seat_label: Only show seats with the given seat_label. :query string seat_guid: Only show seats with the given seat_guid. - :query string blocked: Only show seats with the given blocked status. + :query boolean blocked: Only show seats with the given blocked status. + :query boolean is_available: Only show seats that are (not) currently available. :query string expand: If you pass ``"orderposition"``, ``"cartposition"``, or ``"voucher"``, the respective field will be shown as a nested value instead of just an ID. This requires permission to access that object. The nested objects are identical to the respective resources, except that order positions diff --git a/src/pretix/api/views/event.py b/src/pretix/api/views/event.py index ee31e67976..0a33f58e57 100644 --- a/src/pretix/api/views/event.py +++ b/src/pretix/api/views/event.py @@ -671,12 +671,31 @@ class EventSettingsView(views.APIView): return Response(s.data) +class SeatFilter(FilterSet): + is_available = django_filters.BooleanFilter(method="is_available_qs") + + def is_available_qs(self, queryset, name, value): + expr = ( + Q(orderposition_id__isnull=True, cartposition_id__isnull=True, voucher_id__isnull=True) + ) + if self.request.event.settings.seating_minimal_distance: + expr = expr & Q(has_closeby_taken=False) + if value: + return queryset.filter(expr) + else: + return queryset.exclude(expr) + + class Meta: + model = Seat + fields = ('zone_name', 'row_name', 'row_label', 'seat_number', 'seat_label', 'seat_guid', 'blocked',) + + 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',) + filter_backends = (DjangoFilterBackend, ) + filterset_class = SeatFilter def get_queryset(self): if self.request.event.has_subevents and 'subevent' in self.request.resolver_match.kwargs: @@ -684,9 +703,23 @@ class SeatViewSet(ConditionalListView, viewsets.ModelViewSet): 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) + qs = Seat.annotated( + event_id=self.request.event.id, + subevent=subevent, + qs=subevent.seats.all(), + annotate_ids=True, + minimal_distance=self.request.event.settings.seating_minimal_distance, + distance_only_within_row=self.request.event.settings.seating_distance_only_within_row, + ) 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) + qs = Seat.annotated( + event_id=self.request.event.id, + subevent=None, + qs=self.request.event.seats.all(), + annotate_ids=True, + minimal_distance=self.request.event.settings.seating_minimal_distance, + distance_only_within_row=self.request.event.settings.seating_distance_only_within_row, + ) else: raise NotFound('Please use the subevent-specific endpoint' if self.request.event.has_subevents else 'This event has no subevents') diff --git a/src/pretix/base/models/seating.py b/src/pretix/base/models/seating.py index 229f6ea53e..b96a034895 100644 --- a/src/pretix/base/models/seating.py +++ b/src/pretix/base/models/seating.py @@ -242,7 +242,11 @@ class Seat(models.Model): Power(F('y') - OuterRef('y'), Value(2), output_field=models.FloatField()) ) ).filter( - Q(has_order=True) | Q(has_cart=True) | Q(has_voucher=True), + ( + (Q(orderposition_id__isnull=False) | Q(cartposition_id__isnull=False) | Q(voucher_id__isnull=False)) + if annotate_ids else + (Q(has_order=True) | Q(has_cart=True) | Q(has_voucher=True)) + ), distance__lt=minimal_distance ** 2 ) if distance_only_within_row: diff --git a/src/tests/api/test_events.py b/src/tests/api/test_events.py index 7d11a54c91..163b367026 100644 --- a/src/tests/api/test_events.py +++ b/src/tests/api/test_events.py @@ -1589,7 +1589,9 @@ def test_event_block_unblock_seat(token_client, organizer, event, seatingplan, i @pytest.mark.django_db -def test_event_expand_seat_querycount(token_client, organizer, event, seatingplan, item): +def test_event_expand_seat_filter_and_querycount(token_client, organizer, event, seatingplan, item): + event.settings.seating_minimal_distance = 2 + resp = token_client.patch( '/api/v1/organizers/{}/events/{}/'.format(organizer.slug, event.slug), { @@ -1603,9 +1605,9 @@ def test_event_expand_seat_querycount(token_client, organizer, event, seatingpla assert resp.status_code == 200 event.refresh_from_db() - with assert_num_queries(9): + with assert_num_queries(12): resp = token_client.get('/api/v1/organizers/{}/events/{}/seats/' - '?expand=orderposition&expand=cartposition&expand=voucher' + '?expand=orderposition&expand=cartposition&expand=voucher&is_available=true' .format(organizer.slug, event.slug)) assert resp.status_code == 200 assert len(resp.data['results']) == 3 @@ -1613,24 +1615,31 @@ def test_event_expand_seat_querycount(token_client, organizer, event, seatingpla with scope(organizer=organizer): v0 = event.vouchers.create(item=item, seat=event.seats.get(seat_guid='0-0')) - with assert_num_queries(10): + with assert_num_queries(13): resp = token_client.get('/api/v1/organizers/{}/events/{}/seats/' - '?expand=orderposition&expand=cartposition&expand=voucher' + '?expand=orderposition&expand=cartposition&expand=voucher&is_available=false' .format(organizer.slug, event.slug)) assert resp.status_code == 200 + assert len(resp.data['results']) == 1 assert resp.data['results'][0]['voucher']['id'] == v0.pk - assert resp.data['results'][1]['voucher'] is None - assert resp.data['results'][2]['voucher'] is None + + with assert_num_queries(12): + resp = token_client.get('/api/v1/organizers/{}/events/{}/seats/' + '?expand=orderposition&expand=cartposition&expand=voucher&is_available=true' + .format(organizer.slug, event.slug)) + assert resp.status_code == 200 + assert len(resp.data['results']) == 2 with scope(organizer=organizer): v1 = event.vouchers.create(item=item, seat=event.seats.get(seat_guid='0-1')) v2 = event.vouchers.create(item=item, seat=event.seats.get(seat_guid='0-2')) - with assert_num_queries(10): + with assert_num_queries(13): resp = token_client.get('/api/v1/organizers/{}/events/{}/seats/' - '?expand=orderposition&expand=cartposition&expand=voucher' + '?expand=orderposition&expand=cartposition&expand=voucher&is_available=false' .format(organizer.slug, event.slug)) assert resp.status_code == 200 + assert len(resp.data['results']) == 3 assert resp.data['results'][0]['voucher']['id'] == v0.pk assert resp.data['results'][1]['voucher']['id'] == v1.pk assert resp.data['results'][2]['voucher']['id'] == v2.pk