diff --git a/doc/api/resources/giftcards.rst b/doc/api/resources/giftcards.rst index 5091affc9..d7ebb6248 100644 --- a/doc/api/resources/giftcards.rst +++ b/doc/api/resources/giftcards.rst @@ -96,6 +96,8 @@ Endpoints :query integer page: The page number in case of a multi-page result set, default is 1 :query string secret: Only show gift cards with the given secret. + :query string value: Only show gift cards with the given value. + :query boolean expired: Filter for gift cards that are (not) expired. :query boolean testmode: Filter for gift cards that are (not) in test mode. :query boolean include_accepted: Also show gift cards issued by other organizers that are accepted by this organizer. :query string expand: If you pass ``"owner_ticket"``, the respective field will be shown as a nested value instead of just an ID. diff --git a/doc/api/resources/item_variations.rst b/doc/api/resources/item_variations.rst index 0ae071d89..b687f4c06 100644 --- a/doc/api/resources/item_variations.rst +++ b/doc/api/resources/item_variations.rst @@ -164,6 +164,7 @@ Endpoints } :query integer page: The page number in case of a multi-page result set, default is 1 + :query string search: Filter the list by the value of the variation (substring search). :query boolean active: If set to ``true`` or ``false``, only items with this value for the field ``active`` will be returned. :param organizer: The ``slug`` field of the organizer to fetch diff --git a/doc/api/resources/items.rst b/doc/api/resources/items.rst index 24defa7ba..071c1f7d1 100644 --- a/doc/api/resources/items.rst +++ b/doc/api/resources/items.rst @@ -392,6 +392,7 @@ Endpoints } :query integer page: The page number in case of a multi-page result set, default is 1 + :query string search: Filter the list by internal name or name of the item (substring search). :query boolean active: If set to ``true`` or ``false``, only items with this value for the field ``active`` will be returned. :query integer category: If set to the ID of a category, only items within that category will be returned. diff --git a/doc/api/resources/orders.rst b/doc/api/resources/orders.rst index d6d1671a2..d1b12e030 100644 --- a/doc/api/resources/orders.rst +++ b/doc/api/resources/orders.rst @@ -460,10 +460,13 @@ List of all orders :query datetime modified_since: Only return orders that have changed since the given date. Be careful: We only recommend using this in combination with ``testmode=false``, since test mode orders can vanish at any time and you will not notice it using this method. - :query datetime created_since: Only return orders that have been created since the given date. + :query datetime created_since: Only return orders that have been created since the given date (inclusive). + :query datetime created_before: Only return orders that have been created before the given date (exclusive). :query integer subevent: Only return orders with a position that contains this subevent ID. *Warning:* Result will also include orders if they contain mixed subevents, and it will even return orders where the subevent is only contained in a canceled position. :query datetime subevent_after: Only return orders that contain a ticket for a subevent taking place after the given date. This is an exclusive after, and it considers the **end** of the subevent (or its start, if the end is not set). :query datetime subevent_before: Only return orders that contain a ticket for a subevent taking place after the given date. This is an exclusive before, and it considers the **start** of the subevent. + :query string sales_channel: Only return orders with the given sales channel identifier (e.g. ``"web"``). + :query string payment_provider: Only return orders that contain a payment using the given payment provider. Note that this also searches for partial incomplete, or failed payments within the order and is not useful to get a sum of payment amounts without further processing. :query string exclude: Exclude a field from the output, e.g. ``fees`` or ``positions.downloads``. Can be used as a performance optimization. Can be passed multiple times. :query string include: Include only the given field in the output, e.g. ``fees`` or ``positions.downloads``. Can be used as a performance optimization. Can be passed multiple times. ``include`` is applied before ``exclude``, so ``exclude`` takes precedence. :param organizer: The ``slug`` field of the organizer to fetch diff --git a/src/pretix/api/views/item.py b/src/pretix/api/views/item.py index f904499b5..3ab4237c3 100644 --- a/src/pretix/api/views/item.py +++ b/src/pretix/api/views/item.py @@ -56,10 +56,17 @@ from pretix.base.models import ( ) from pretix.base.services.quotas import QuotaAvailability from pretix.helpers.dicts import merge_dicts +from pretix.helpers.i18n import i18ncomp with scopes_disabled(): class ItemFilter(FilterSet): tax_rate = django_filters.CharFilter(method='tax_rate_qs') + search = django_filters.CharFilter(method='search_qs') + + def search_qs(self, queryset, name, value): + return queryset.filter( + Q(internal_name__icontains=value) | Q(name__icontains=i18ncomp(value)) + ) def tax_rate_qs(self, queryset, name, value): if value in ("0", "None", "0.00"): @@ -71,6 +78,18 @@ with scopes_disabled(): model = Item fields = ['active', 'category', 'admission', 'tax_rate', 'free_price'] + class ItemVariationFilter(FilterSet): + search = django_filters.CharFilter(method='search_qs') + + def search_qs(self, queryset, name, value): + return queryset.filter( + Q(value__icontains=i18ncomp(value)) + ) + + class Meta: + model = ItemVariation + fields = ['active'] + class ItemViewSet(ConditionalListView, viewsets.ModelViewSet): serializer_class = ItemSerializer @@ -140,6 +159,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet): serializer_class = ItemVariationSerializer queryset = ItemVariation.objects.none() filter_backends = (DjangoFilterBackend, TotalOrderingFilter,) + filterset_class = ItemVariationFilter ordering_fields = ('id', 'position') ordering = ('id',) permission = None diff --git a/src/pretix/api/views/order.py b/src/pretix/api/views/order.py index 606f8cdd4..5ca5876c6 100644 --- a/src/pretix/api/views/order.py +++ b/src/pretix/api/views/order.py @@ -108,6 +108,7 @@ with scopes_disabled(): status = django_filters.CharFilter(field_name='status', lookup_expr='iexact') modified_since = django_filters.IsoDateTimeFilter(field_name='last_modified', lookup_expr='gte') created_since = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='gte') + created_before = django_filters.IsoDateTimeFilter(field_name='datetime', lookup_expr='lt') subevent_after = django_filters.IsoDateTimeFilter(method='subevent_after_qs') subevent_before = django_filters.IsoDateTimeFilter(method='subevent_before_qs') search = django_filters.CharFilter(method='search_qs') @@ -115,6 +116,8 @@ with scopes_disabled(): variation = django_filters.CharFilter(field_name='all_positions', lookup_expr='variation_id', distinct=True) subevent = django_filters.CharFilter(field_name='all_positions', lookup_expr='subevent_id', distinct=True) customer = django_filters.CharFilter(field_name='customer__identifier') + sales_channel = django_filters.CharFilter(field_name='sales_channel__identifier') + payment_provider = django_filters.CharFilter(method='provider_qs') class Meta: model = Order @@ -138,6 +141,11 @@ with scopes_disabled(): ) return qs + def provider_qs(self, qs, name, value): + return qs.filter(Exists( + OrderPayment.objects.filter(order=OuterRef('pk'), provider=value) + )) + def subevent_before_qs(self, qs, name, value): if getattr(self.request, 'event', None): subevents = self.request.event.subevents diff --git a/src/pretix/api/views/organizer.py b/src/pretix/api/views/organizer.py index 5f284ff6f..e8f948b69 100644 --- a/src/pretix/api/views/organizer.py +++ b/src/pretix/api/views/organizer.py @@ -24,10 +24,11 @@ from decimal import Decimal import django_filters from django.contrib.auth.hashers import make_password from django.db import transaction -from django.db.models import OuterRef, Subquery, Sum +from django.db.models import OuterRef, Q, Subquery, Sum from django.db.models.functions import Coalesce from django.shortcuts import get_object_or_404 from django.utils.functional import cached_property +from django.utils.timezone import now from django_filters.rest_framework import DjangoFilterBackend, FilterSet from django_scopes import scopes_disabled from rest_framework import mixins, serializers, status, views, viewsets @@ -136,11 +137,19 @@ class SeatingPlanViewSet(viewsets.ModelViewSet): with scopes_disabled(): class GiftCardFilter(FilterSet): secret = django_filters.CharFilter(field_name='secret', lookup_expr='iexact') + expired = django_filters.BooleanFilter(method='expired_qs') + value = django_filters.NumberFilter(field_name='cached_value') class Meta: model = GiftCard fields = ['secret', 'testmode'] + def expired_qs(self, qs, name, value): + if value: + return qs.filter(expires__isnull=False, expires__lt=now()) + else: + return qs.filter(Q(expires__isnull=True) | Q(expires__gte=now())) + class GiftCardViewSet(viewsets.ModelViewSet): serializer_class = GiftCardSerializer diff --git a/src/tests/api/test_giftcards.py b/src/tests/api/test_giftcards.py index 2df4c922f..3cfc664b9 100644 --- a/src/tests/api/test_giftcards.py +++ b/src/tests/api/test_giftcards.py @@ -86,6 +86,18 @@ def test_giftcard_list(token_client, organizer, event, giftcard, other_giftcard) assert resp.status_code == 200 assert 2 == len(resp.data['results']) + resp = token_client.get('/api/v1/organizers/{}/giftcards/?expired=false'.format(organizer.slug)) + assert 1 == len(resp.data['results']) + resp = token_client.get('/api/v1/organizers/{}/giftcards/?expired=true'.format(organizer.slug)) + assert 0 == len(resp.data['results']) + + resp = token_client.get('/api/v1/organizers/{}/giftcards/?value=23.00'.format(organizer.slug)) + assert 1 == len(resp.data['results']) + resp = token_client.get('/api/v1/organizers/{}/giftcards/?value=23'.format(organizer.slug)) + assert 1 == len(resp.data['results']) + resp = token_client.get('/api/v1/organizers/{}/giftcards/?value=24'.format(organizer.slug)) + assert 0 == len(resp.data['results']) + @pytest.mark.django_db def test_giftcard_detail(token_client, organizer, event, giftcard): diff --git a/src/tests/api/test_items.py b/src/tests/api/test_items.py index e1d4224e5..5cd0d4b71 100644 --- a/src/tests/api/test_items.py +++ b/src/tests/api/test_items.py @@ -370,6 +370,13 @@ def test_item_list(token_client, organizer, event, team, item): assert resp.status_code == 200 assert [] == resp.data['results'] + resp = token_client.get('/api/v1/organizers/{}/events/{}/items/?search=Budget'.format(organizer.slug, event.slug)) + assert resp.status_code == 200 + assert [res] == resp.data['results'] + resp = token_client.get('/api/v1/organizers/{}/events/{}/items/?search=Free'.format(organizer.slug, event.slug)) + assert resp.status_code == 200 + assert [] == resp.data['results'] + @pytest.mark.django_db def test_item_detail(token_client, organizer, event, team, item): @@ -1412,6 +1419,21 @@ def test_variations_list(token_client, organizer, event, item, variation): assert res['position'] == resp.data['results'][0]['position'] assert res['price'] == resp.data['results'][0]['price'] + resp = token_client.get('/api/v1/organizers/{}/events/{}/items/{}/variations/?active=true'.format(organizer.slug, event.slug, item.pk)) + assert resp.status_code == 200 + assert res['value'] == resp.data['results'][0]['value'] + resp = token_client.get( + '/api/v1/organizers/{}/events/{}/items/{}/variations/?active=false'.format(organizer.slug, event.slug, item.pk)) + assert resp.status_code == 200 + assert [] == resp.data['results'] + + resp = token_client.get('/api/v1/organizers/{}/events/{}/items/{}/variations/?search=Child'.format(organizer.slug, event.slug, item.pk)) + assert resp.status_code == 200 + assert res['value'] == resp.data['results'][0]['value'] + resp = token_client.get('/api/v1/organizers/{}/events/{}/items/{}/variations/?search=Incorrect'.format(organizer.slug, event.slug, item.pk)) + assert resp.status_code == 200 + assert [] == resp.data['results'] + @pytest.mark.django_db def test_variations_detail(token_client, organizer, event, item, variation): diff --git a/src/tests/api/test_orders.py b/src/tests/api/test_orders.py index 0ed8ee28f..f2ebbbfa9 100644 --- a/src/tests/api/test_orders.py +++ b/src/tests/api/test_orders.py @@ -414,6 +414,16 @@ def test_order_list(token_client, organizer, event, order, item, taxrule, questi '/api/v1/organizers/{}/events/{}/orders/?email=foo@example.org'.format(organizer.slug, event.slug)) assert [] == resp.data['results'] + resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?payment_provider=banktransfer'.format(organizer.slug, event.slug)) + assert [res] == resp.data['results'] + resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?payment_provider=manual'.format(organizer.slug, event.slug)) + assert [] == resp.data['results'] + + resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?sales_channel=web'.format(organizer.slug, event.slug)) + assert [res] == resp.data['results'] + resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?sales_channel=bar'.format(organizer.slug, event.slug)) + assert [] == resp.data['results'] + resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?locale=en'.format(organizer.slug, event.slug)) assert [res] == resp.data['results'] resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?locale=de'.format(organizer.slug, event.slug)) @@ -434,6 +444,36 @@ def test_order_list(token_client, organizer, event, order, item, taxrule, questi )) assert [] == resp.data['results'] + resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?created_since={}'.format( + organizer.slug, event.slug, + (order.datetime - datetime.timedelta(hours=1)).isoformat().replace('+00:00', 'Z') + )) + assert [res] == resp.data['results'] + resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?created_since={}'.format( + organizer.slug, event.slug, order.datetime.isoformat().replace('+00:00', 'Z') + )) + assert [res] == resp.data['results'] + resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?created_since={}'.format( + organizer.slug, event.slug, + (order.datetime + datetime.timedelta(hours=1)).isoformat().replace('+00:00', 'Z') + )) + assert [] == resp.data['results'] + + resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?created_before={}'.format( + organizer.slug, event.slug, + (order.datetime - datetime.timedelta(hours=1)).isoformat().replace('+00:00', 'Z') + )) + assert [] == resp.data['results'] + resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?created_before={}'.format( + organizer.slug, event.slug, order.datetime.isoformat().replace('+00:00', 'Z') + )) + assert [] == resp.data['results'] + resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?created_before={}'.format( + organizer.slug, event.slug, + (order.datetime + datetime.timedelta(hours=1)).isoformat().replace('+00:00', 'Z') + )) + assert [res] == resp.data['results'] + resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?include_canceled_positions=false'.format(organizer.slug, event.slug)) assert resp.status_code == 200 assert len(resp.data['results'][0]['positions']) == 1