Skip to content

Commit 998ed89

Browse files
tcleonardThomas Leonard
and
Thomas Leonard
authored
feat: add TypedFilter which allow to explicitly give a filter input GraphQL type (#1142)
Co-authored-by: Thomas Leonard <[email protected]>
1 parent 6f1389c commit 998ed89

11 files changed

+408
-154
lines changed

docs/filtering.rst

+39-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ You will need to install it manually, which can be done as follows:
1616
1717
# You'll need to install django-filter
1818
pip install django-filter>=2
19-
19+
2020
After installing ``django-filter`` you'll need to add the application in the ``settings.py`` file:
2121

2222
.. code:: python
@@ -271,3 +271,41 @@ with this set up, you can now filter events by tags:
271271
name
272272
}
273273
}
274+
275+
276+
`TypedFilter`
277+
-------------
278+
279+
Sometimes the automatic detection of the filter input type is not satisfactory for what you are trying to achieve.
280+
You can then explicitly specify the input type you want for your filter by using a `TypedFilter`:
281+
282+
.. code:: python
283+
284+
from django.db import models
285+
from django_filters import FilterSet, OrderingFilter
286+
import graphene
287+
from graphene_django.filter import TypedFilter
288+
289+
class Event(models.Model):
290+
name = models.CharField(max_length=50)
291+
292+
class EventFilterSet(FilterSet):
293+
class Meta:
294+
model = Event
295+
fields = {
296+
"name": ["exact", "contains"],
297+
}
298+
299+
only_first = TypedFilter(input_type=graphene.Boolean, method="only_first_filter")
300+
301+
def only_first_filter(self, queryset, _name, value):
302+
if value:
303+
return queryset[:1]
304+
else:
305+
return queryset
306+
307+
class EventType(DjangoObjectType):
308+
class Meta:
309+
model = Event
310+
interfaces = (Node,)
311+
filterset_class = EventFilterSet

graphene_django/filter/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
GlobalIDMultipleChoiceFilter,
1616
ListFilter,
1717
RangeFilter,
18+
TypedFilter,
1819
)
1920

2021
__all__ = [
@@ -24,4 +25,5 @@
2425
"ArrayFilter",
2526
"ListFilter",
2627
"RangeFilter",
28+
"TypedFilter",
2729
]

graphene_django/filter/filters.py

-101
This file was deleted.
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import warnings
2+
from ...utils import DJANGO_FILTER_INSTALLED
3+
4+
if not DJANGO_FILTER_INSTALLED:
5+
warnings.warn(
6+
"Use of django filtering requires the django-filter package "
7+
"be installed. You can do so using `pip install django-filter`",
8+
ImportWarning,
9+
)
10+
else:
11+
from .array_filter import ArrayFilter
12+
from .global_id_filter import GlobalIDFilter, GlobalIDMultipleChoiceFilter
13+
from .list_filter import ListFilter
14+
from .range_filter import RangeFilter
15+
from .typed_filter import TypedFilter
16+
17+
__all__ = [
18+
"DjangoFilterConnectionField",
19+
"GlobalIDFilter",
20+
"GlobalIDMultipleChoiceFilter",
21+
"ArrayFilter",
22+
"ListFilter",
23+
"RangeFilter",
24+
"TypedFilter",
25+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from django_filters.constants import EMPTY_VALUES
2+
3+
from .typed_filter import TypedFilter
4+
5+
6+
class ArrayFilter(TypedFilter):
7+
"""
8+
Filter made for PostgreSQL ArrayField.
9+
"""
10+
11+
def filter(self, qs, value):
12+
"""
13+
Override the default filter class to check first whether the list is
14+
empty or not.
15+
This needs to be done as in this case we expect to get the filter applied with
16+
an empty list since it's a valid value but django_filter consider an empty list
17+
to be an empty input value (see `EMPTY_VALUES`) meaning that
18+
the filter does not need to be applied (hence returning the original
19+
queryset).
20+
"""
21+
if value in EMPTY_VALUES and value != []:
22+
return qs
23+
if self.distinct:
24+
qs = qs.distinct()
25+
lookup = "%s__%s" % (self.field_name, self.lookup_expr)
26+
qs = self.get_method(qs)(**{lookup: value})
27+
return qs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from django_filters import Filter, MultipleChoiceFilter
2+
3+
from graphql_relay.node.node import from_global_id
4+
5+
from ...forms import GlobalIDFormField, GlobalIDMultipleChoiceField
6+
7+
8+
class GlobalIDFilter(Filter):
9+
"""
10+
Filter for Relay global ID.
11+
"""
12+
13+
field_class = GlobalIDFormField
14+
15+
def filter(self, qs, value):
16+
""" Convert the filter value to a primary key before filtering """
17+
_id = None
18+
if value is not None:
19+
_, _id = from_global_id(value)
20+
return super(GlobalIDFilter, self).filter(qs, _id)
21+
22+
23+
class GlobalIDMultipleChoiceFilter(MultipleChoiceFilter):
24+
field_class = GlobalIDMultipleChoiceField
25+
26+
def filter(self, qs, value):
27+
gids = [from_global_id(v)[1] for v in value]
28+
return super(GlobalIDMultipleChoiceFilter, self).filter(qs, gids)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from .typed_filter import TypedFilter
2+
3+
4+
class ListFilter(TypedFilter):
5+
"""
6+
Filter that takes a list of value as input.
7+
It is for example used for `__in` filters.
8+
"""
9+
10+
def filter(self, qs, value):
11+
"""
12+
Override the default filter class to check first whether the list is
13+
empty or not.
14+
This needs to be done as in this case we expect to get an empty output
15+
(if not an exclude filter) but django_filter consider an empty list
16+
to be an empty input value (see `EMPTY_VALUES`) meaning that
17+
the filter does not need to be applied (hence returning the original
18+
queryset).
19+
"""
20+
if value is not None and len(value) == 0:
21+
if self.exclude:
22+
return qs
23+
else:
24+
return qs.none()
25+
else:
26+
return super(ListFilter, self).filter(qs, value)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from django.core.exceptions import ValidationError
2+
from django.forms import Field
3+
4+
from .typed_filter import TypedFilter
5+
6+
7+
def validate_range(value):
8+
"""
9+
Validator for range filter input: the list of value must be of length 2.
10+
Note that validators are only run if the value is not empty.
11+
"""
12+
if len(value) != 2:
13+
raise ValidationError(
14+
"Invalid range specified: it needs to contain 2 values.", code="invalid"
15+
)
16+
17+
18+
class RangeField(Field):
19+
default_validators = [validate_range]
20+
empty_values = [None]
21+
22+
23+
class RangeFilter(TypedFilter):
24+
field_class = RangeField
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from django_filters import Filter
2+
3+
from graphene.types.utils import get_type
4+
5+
6+
class TypedFilter(Filter):
7+
"""
8+
Filter class for which the input GraphQL type can explicitly be provided.
9+
If it is not provided, when building the schema, it will try to guess
10+
it from the field.
11+
"""
12+
13+
def __init__(self, input_type=None, *args, **kwargs):
14+
self._input_type = input_type
15+
super(TypedFilter, self).__init__(*args, **kwargs)
16+
17+
@property
18+
def input_type(self):
19+
input_type = get_type(self._input_type)
20+
if input_type is not None:
21+
if not callable(getattr(input_type, "get_type", None)):
22+
raise ValueError(
23+
"Wrong `input_type` for {}: it only accepts graphene types, got {}".format(
24+
self.__class__.__name__, input_type
25+
)
26+
)
27+
return input_type

0 commit comments

Comments
 (0)