Skip to content

Commit 20b0e53

Browse files
author
Victor
authored
Merge pull request #1 from devind-team/develop
Basic functionality
2 parents 7584bbb + 9eba1de commit 20b0e53

22 files changed

+1612
-68
lines changed

.flake8

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
[flake8]
2+
max-line-length = 100
23
per-file-ignores =
34
__init__.py:F401
45
connection_field.py:A002
5-
ignore = ANN101,ANN102,D107
6+
ignore = ANN002,ANN003,ANN101,ANN102,D106,D107
67

78
import-order-style = pycharm
89
dictionaries=en_US,python,technical,django

.pre-commit-config.yaml

-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ repos:
2525
- flake8-comprehensions
2626
- flake8-eradicate
2727
- flake8-simplify
28-
- flake8-spellcheck
2928
- pep8-naming
3029
- flake8-use-fstring
3130
- flake8-annotations

README.md

+133-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,133 @@
1-
# graphene-django-filter
2-
Advanced filters for Graphene.
1+
# Graphene-Django-Filter
2+
[![CI](https://github.com/devind-team/graphene-django-filter/workflows/CI/badge.svg)](https://github.com/devind-team/graphene-django-filter/actions) [![PyPI version](https://badge.fury.io/py/graphene-django-filter.svg)](https://badge.fury.io/py/graphene-django-filter)
3+
4+
This package contains advanced filters for [graphene-django](https://github.com/graphql-python/graphene-django). The standard filtering feature in Graphene-Django relies on the Django-Filter library and therefore provides a flat API without the ability to use `and` and `or` expressions. This library makes the API nested and adds the `and` and `or` composition by extension of the `DjangoFilterConnectionField` field and the `FilterSet` class.
5+
# Requirements
6+
* Python (3.6, 3.7, 3.8, 3.9, 3.10)
7+
* Graphene-Django (2.15)
8+
# Features
9+
## Nested API with the ability to use `and` and `or` expressions
10+
To use, simply replace all `DjangoFilterConnectionField` fields with `AdvancedDjangoFilterConnectionField` fields in your queries. Also, if you create custom FilterSets, replace the inheritance from the `FilterSet` class with the inheritance from the `AdvancedFilterSet` class. For example, the following task query exposes an old flat API.
11+
```python
12+
import graphene
13+
from django_filters import FilterSet
14+
from graphene_django import DjangoObjectType
15+
from graphene_django.filter import DjangoFilterConnectionField
16+
17+
class TaskFilter(FilterSet)
18+
class Meta:
19+
model = Task
20+
fields = {
21+
'name': ('exact', 'contains'),
22+
'user__email': ('exact', 'contains'),
23+
'user__last_name': ('exact', 'contains'),
24+
}
25+
26+
class UserType(DjangoObjectType):
27+
class Meta:
28+
model = User
29+
interfaces = (graphene.relay.Node,)
30+
fields = '__all__'
31+
32+
class TaskType(DjangoObjectType):
33+
user = graphene.Field(UserType)
34+
35+
class Meta:
36+
model = Task
37+
interfaces = (graphene.relay.Node,)
38+
fields = '__all__'
39+
filterset_class = TaskFilter
40+
41+
class Query(graphene.ObjectType):
42+
tasks = DjangoFilterConnectionField(TaskType)
43+
```
44+
The flat API in which all filters are applied using the "and" operator looks like this.
45+
```graphql
46+
{
47+
tasks(
48+
name_Contains: "important"
49+
user_Email_Contains: "john"
50+
user_LastName: "Dou"
51+
){
52+
edges {
53+
node {
54+
id
55+
name
56+
}
57+
}
58+
}
59+
}
60+
```
61+
After replacing the field class with the `AdvancedDjangoFilterConnectionField` and the `FilterSet` class with the `AdvancedFilterSet` the API becomes nested with support for `and` and `or` expressions.
62+
```python
63+
from graphene_django_filter import AdvancedDjangoFilterConnectionField, AdvancedFilterSet
64+
65+
class TaskFilter(AdvancedFilterSet)
66+
class Meta:
67+
model = Task
68+
fields = {
69+
'name': ('exact', 'contains'),
70+
'user__email': ('exact', 'contains'),
71+
'user__last_name': ('exact', 'contains'),
72+
}
73+
74+
class Query(graphene.ObjectType):
75+
tasks = AdvancedDjangoFilterConnectionField(TaskType)
76+
```
77+
For example, the following query returns tasks whose names contain the word "important" or the user's email address contains the word "john" and the user's last name is "Dou". Note that the operators are applied to lookups such as `contains`, `exact`, etc. at the last level of nesting.
78+
```graphql
79+
{
80+
tasks(
81+
filter: {
82+
or: [
83+
{name: {contains: "important"}}
84+
and: [
85+
{user: email: {contains: "john"}}
86+
{user: lastName: {exact: "Dou"}}
87+
]
88+
]
89+
}
90+
){
91+
edges {
92+
node {
93+
id
94+
name
95+
}
96+
}
97+
}
98+
}
99+
```
100+
The same result can be achieved with an alternative query structure because within the same object the `and` operator is always used.
101+
```graphql
102+
{
103+
tasks(
104+
filter: {
105+
or: [
106+
{name: {contains: "important"}}
107+
{
108+
user: {
109+
email: {contains: "john"}
110+
lastName: {exact: "Dou"}
111+
}
112+
}
113+
]
114+
}
115+
){
116+
edges {
117+
node {
118+
id
119+
name
120+
}
121+
}
122+
}
123+
}
124+
```
125+
The filter input type has the following structure.
126+
```graphql
127+
input FilterInputType {
128+
and: [FilterInputType]
129+
or: [FilterInputType]
130+
...FieldLookups
131+
}
132+
```
133+
For more examples, see tests.

graphene_django_filter/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22

33
__version__ = '0.3.0'
44

5-
from .connection_field import DjangoFilterConnectionField
5+
from .connection_field import AdvancedDjangoFilterConnectionField
6+
from .filterset import AdvancedFilterSet
+82-7
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,88 @@
1-
"""DjangoFilterConnectionField module.
1+
"""`AdvancedDjangoFilterConnectionField` class module.
22
3-
Use DjangoFilterConnectionField class from this
4-
module instead of graphene_django one.
3+
Use the `AdvancedDjangoFilterConnectionField` class from this
4+
module instead of the `DjangoFilterConnectionField` from graphene-django.
55
"""
66

7-
from graphene_django import DjangoConnectionField
7+
from typing import Any, Dict, Iterable, Optional, Type
88

9+
import graphene
10+
from django.core.exceptions import ValidationError
11+
from django.db import models
12+
from django_filters import FilterSet
13+
from graphene_django import DjangoObjectType
14+
from graphene_django.filter import DjangoFilterConnectionField
915

10-
class DjangoFilterConnectionField(DjangoConnectionField):
11-
"""Allow to use advanced filters provided by this library."""
16+
from .filterset import AdvancedFilterSet, tree_input_type_to_data
17+
from .filterset_factories import get_filterset_class
18+
from .input_type_factories import get_filtering_args_from_filterset
1219

13-
pass
20+
21+
class AdvancedDjangoFilterConnectionField(DjangoFilterConnectionField):
22+
"""Allow you to use advanced filters provided by this library."""
23+
24+
def __init__(
25+
self,
26+
type: Type[DjangoObjectType],
27+
fields: Optional[Dict[str, list]] = None,
28+
order_by: Any = None,
29+
extra_filter_meta: Optional[dict] = None,
30+
filterset_class: Optional[Type[AdvancedFilterSet]] = None,
31+
*args,
32+
**kwargs
33+
) -> None:
34+
assert filterset_class is None or issubclass(filterset_class, AdvancedFilterSet), \
35+
'Use the `AdvancedFilterSet` class with the `AdvancedDjangoFilterConnectionField`'
36+
super().__init__(
37+
type,
38+
fields,
39+
order_by,
40+
extra_filter_meta,
41+
filterset_class,
42+
*args,
43+
**kwargs
44+
)
45+
46+
@property
47+
def filterset_class(self) -> Type[AdvancedFilterSet]:
48+
"""Return a AdvancedFilterSet instead of a FilterSet."""
49+
if not self._filterset_class:
50+
fields = self._fields or self.node_type._meta.filter_fields
51+
meta = {'model': self.model, 'fields': fields}
52+
if self._extra_filter_meta:
53+
meta.update(self._extra_filter_meta)
54+
filterset_class = self._provided_filterset_class or (
55+
self.node_type._meta.filterset_class
56+
)
57+
self._filterset_class = get_filterset_class(filterset_class, **meta)
58+
return self._filterset_class
59+
60+
@property
61+
def filtering_args(self) -> dict:
62+
"""Return filtering args from the filterset."""
63+
if not self._filtering_args:
64+
self._filtering_args = get_filtering_args_from_filterset(
65+
self.filterset_class, self.node_type,
66+
)
67+
return self._filtering_args
68+
69+
@classmethod
70+
def resolve_queryset(
71+
cls,
72+
connection: object,
73+
iterable: Iterable,
74+
info: graphene.ResolveInfo,
75+
args: Dict[str, Any],
76+
filtering_args: Dict[str, graphene.InputField],
77+
filterset_class: Type[FilterSet],
78+
) -> models.QuerySet:
79+
"""Return a filtered QuerySet."""
80+
qs = super(DjangoFilterConnectionField, cls).resolve_queryset(
81+
connection, iterable, info, args,
82+
)
83+
filterset = filterset_class(
84+
data=tree_input_type_to_data(args['filter']), queryset=qs, request=info.context,
85+
)
86+
if filterset.form.is_valid():
87+
return filterset.qs
88+
raise ValidationError(filterset.form.errors.as_json())

0 commit comments

Comments
 (0)