forked from CGM_Public/pretix_original
API: Allow querying invoices with multiple order codes (Z#23158921) (#4332)
This commit is contained in:
@@ -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
82
src/pretix/api/filters.py
Normal 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}
|
||||||
@@ -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']
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
Reference in New Issue
Block a user