API: Allow querying invoices with multiple order codes (Z#23158921) (#4332)

This commit is contained in:
Raphael Michel
2024-07-26 16:32:29 +02:00
committed by GitHub
parent a692940397
commit 17f1d571b0
4 changed files with 102 additions and 5 deletions

View File

@@ -217,6 +217,9 @@ List of all invoices
:query boolean is_cancellation: If set to ``true`` or ``false``, only invoices with this value for the field :query boolean is_cancellation: If set to ``true`` or ``false``, only invoices with this value for the field
``is_cancellation`` will be returned. ``is_cancellation`` will be returned.
:query string order: If set, only invoices belonging to the order with the given order code will be returned. :query string order: If set, only invoices belonging to the order with the given order code will be returned.
This parameter may be given multiple times. In this case, all invoices matching one of the inputs will be returned.
:query string number: If set, only invoices with the given invoice number will be returned.
This parameter may be given multiple times. In this case, all invoices matching one of the inputs will be returned.
:query string refers: If set, only invoices referring to the given invoice will be returned. :query string refers: If set, only invoices referring to the given invoice will be returned.
:query string locale: If set, only invoices with the given locale will be returned. :query string locale: If set, only invoices with the given locale will be returned.
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``date`` and :query string ordering: Manually set the ordering of results. Valid fields to be used are ``date`` and

82
src/pretix/api/filters.py Normal file
View File

@@ -0,0 +1,82 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from django import forms
from django.core.exceptions import ValidationError
from django.db.models import Q
from django.db.models.constants import LOOKUP_SEP
from django.forms import MultipleChoiceField
from django_filters import Filter
from django_filters.conf import settings
class MultipleCharField(forms.CharField):
widget = forms.MultipleHiddenInput
def to_python(self, value):
if not value:
return []
elif not isinstance(value, (list, tuple)):
raise ValidationError(
MultipleChoiceField.default_error_messages["invalid_list"], code="invalid_list"
)
return [str(val) for val in value]
class MultipleCharFilter(Filter):
"""
This filter performs OR(by default) or AND(using conjoined=True) query
on the selected inputs.
"""
field_class = MultipleCharField
def __init__(self, *args, **kwargs):
self.conjoined = kwargs.pop("conjoined", False)
super().__init__(*args, **kwargs)
def filter(self, qs, value):
if not value:
# Even though not a noop, no point filtering if empty.
return qs
if not self.conjoined:
q = Q()
for v in set(value):
predicate = self.get_filter_predicate(v)
if self.conjoined:
qs = self.get_method(qs)(**predicate)
else:
q |= Q(**predicate)
if not self.conjoined:
qs = self.get_method(qs)(q)
return qs.distinct() if self.distinct else qs
def get_filter_predicate(self, v):
name = self.field_name
if name and self.lookup_expr != settings.DEFAULT_LOOKUP_EXPR:
name = LOOKUP_SEP.join([name, self.lookup_expr])
try:
return {name: getattr(v, self.field.to_field_name)}
except (AttributeError, TypeError):
return {name: v}

View File

@@ -49,6 +49,7 @@ from rest_framework.mixins import CreateModelMixin
from rest_framework.permissions import SAFE_METHODS from rest_framework.permissions import SAFE_METHODS
from rest_framework.response import Response from rest_framework.response import Response
from pretix.api.filters import MultipleCharFilter
from pretix.api.models import OAuthAccessToken from pretix.api.models import OAuthAccessToken
from pretix.api.pagination import TotalOrderingFilter from pretix.api.pagination import TotalOrderingFilter
from pretix.api.serializers.order import ( from pretix.api.serializers.order import (
@@ -1825,17 +1826,14 @@ class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
with scopes_disabled(): with scopes_disabled():
class InvoiceFilter(FilterSet): class InvoiceFilter(FilterSet):
refers = django_filters.CharFilter(method='refers_qs') refers = django_filters.CharFilter(method='refers_qs')
number = django_filters.CharFilter(method='nr_qs') number = MultipleCharFilter(field_name='nr', lookup_expr='iexact')
order = django_filters.CharFilter(field_name='order', lookup_expr='code__iexact') order = MultipleCharFilter(field_name='order', lookup_expr='code__iexact')
def refers_qs(self, queryset, name, value): def refers_qs(self, queryset, name, value):
return queryset.annotate( return queryset.annotate(
refers_nr=Concat('refers__prefix', 'refers__invoice_no') refers_nr=Concat('refers__prefix', 'refers__invoice_no')
).filter(refers_nr__iexact=value) ).filter(refers_nr__iexact=value)
def nr_qs(self, queryset, name, value):
return queryset.filter(nr__iexact=value)
class Meta: class Meta:
model = Invoice model = Invoice
fields = ['order', 'number', 'is_cancellation', 'refers', 'locale'] fields = ['order', 'number', 'is_cancellation', 'refers', 'locale']

View File

@@ -311,6 +311,20 @@ def test_invoice_list(token_client, organizer, event, order, item, invoice):
assert [] == resp.data['results'] assert [] == resp.data['results']
@pytest.mark.django_db
def test_invoice_list_multi_filter(token_client, organizer, event, order, order2, item, invoice, invoice2):
order2.event = event
order2.save()
invoice2.event = event
invoice2.save()
resp = token_client.get('/api/v1/organizers/{}/events/{}/invoices/?order=FOO'.format(organizer.slug, event.slug))
assert len(resp.data['results']) == 1
resp = token_client.get('/api/v1/organizers/{}/events/{}/invoices/?order=BAR'.format(organizer.slug, event.slug))
assert len(resp.data['results']) == 1
resp = token_client.get('/api/v1/organizers/{}/events/{}/invoices/?order=FOO&order=BAR'.format(organizer.slug, event.slug))
assert len(resp.data['results']) == 2
@pytest.mark.django_db @pytest.mark.django_db
def test_organizer_level(token_client, organizer, team, event, event2, invoice, invoice2): def test_organizer_level(token_client, organizer, team, event, event2, invoice, invoice2):
resp = token_client.get('/api/v1/organizers/{}/invoices/'.format(organizer.slug)) resp = token_client.get('/api/v1/organizers/{}/invoices/'.format(organizer.slug))