Skip to content

Commit 6a8d7ae

Browse files
committed
easy changes recommended by @sliverc review
1 parent 4f2b75b commit 6a8d7ae

File tree

4 files changed

+126
-149
lines changed

4 files changed

+126
-149
lines changed

example/requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@ pyparsing
1111
pytz
1212
six
1313
sqlparse
14-
14+
django-filter>=2.0

example/settings/dev.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,9 @@
2626
'polymorphic',
2727
'example',
2828
'debug_toolbar',
29+
'django_filters',
2930
]
3031

31-
try:
32-
import django_filters # noqa: 401
33-
INSTALLED_APPS += ['django_filters']
34-
except ImportError:
35-
pass
36-
3732
TEMPLATES = [
3833
{
3934
'BACKEND': 'django.template.backends.django.DjangoTemplates',

example/views.py

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import rest_framework_json_api.metadata
66
import rest_framework_json_api.parsers
77
import rest_framework_json_api.renderers
8+
from django_filters import rest_framework as filters
89
from rest_framework_json_api.pagination import PageNumberPagination
910
from rest_framework_json_api.utils import format_drf_errors
1011
from rest_framework_json_api.views import ModelViewSet, RelationshipView
@@ -102,23 +103,16 @@ class NonPaginatedEntryViewSet(EntryViewSet):
102103
'blog__name': rels,
103104
'blog__tagline': rels,
104105
}
105-
filter_fields = filterset_fields # django-filter<=1.11 (required for py27)
106+
filter_fields = filterset_fields # django-filter<=1.1 (required for py27)
106107

107108

108-
# While this example is used for testing with django-filter, leave the option of running it without.
109-
# The test cases will fail, but the app will run.
110-
try:
111-
from django_filters import rest_framework as filters
109+
class EntryFilter(filters.FilterSet):
110+
bname = filters.CharFilter(field_name="blog__name",
111+
lookup_expr="exact")
112112

113-
class EntryFilter(filters.FilterSet):
114-
bname = filters.CharFilter(field_name="blog__name",
115-
lookup_expr="exact")
116-
117-
class Meta:
118-
model = Entry
119-
fields = ['id', 'headline', 'body_text']
120-
except ImportError:
121-
EntryFilter = None
113+
class Meta:
114+
model = Entry
115+
fields = ['id', 'headline', 'body_text']
122116

123117

124118
class FiltersetEntryViewSet(EntryViewSet):
@@ -128,7 +122,7 @@ class FiltersetEntryViewSet(EntryViewSet):
128122
pagination_class = NoPagination
129123
filterset_fields = None
130124
filterset_class = EntryFilter
131-
filter_fields = filterset_fields # django-filter<=1.11
125+
filter_fields = filterset_fields # django-filter<=1.1
132126
filter_class = filterset_class
133127

134128

@@ -139,7 +133,7 @@ class NoFiltersetEntryViewSet(EntryViewSet):
139133
pagination_class = NoPagination
140134
filterset_fields = None
141135
filterset_class = None
142-
filter_fields = filterset_fields # django-filter<=1.11
136+
filter_fields = filterset_fields # django-filter<=1.1
143137
filter_class = filterset_class
144138

145139

Lines changed: 114 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -1,135 +1,123 @@
11
import re
2-
import warnings
32

43
from rest_framework.exceptions import ValidationError
54
from rest_framework.settings import api_settings
65

6+
from django_filters import VERSION
7+
from django_filters.rest_framework import DjangoFilterBackend
78
from rest_framework_json_api.utils import format_value
89

9-
# django-filter is an optional package. Generate a dummy class if it's missing.
10-
try:
11-
from django_filters.rest_framework import DjangoFilterBackend
12-
except ImportError:
13-
class JSONAPIDjangoFilter(object):
14-
15-
def __init__(self, *args, **kwargs):
16-
"""
17-
complain that they need django-filter
18-
TODO: should this be a warning or an exception?
19-
"""
20-
warnings.warn("must install django-filter package to use JSONAPIDjangoFilter")
21-
22-
def filter_queryset(self, request, queryset, view):
23-
"""
24-
do nothing
25-
"""
26-
return queryset
27-
28-
else:
29-
class JSONAPIDjangoFilter(DjangoFilterBackend):
10+
11+
class JSONAPIDjangoFilter(DjangoFilterBackend):
12+
"""
13+
A Django-style ORM filter implementation, using `django-filter`.
14+
15+
This is not part of the jsonapi standard per-se, other than the requirement
16+
to use the `filter` keyword: This is an optional implementation of style of
17+
filtering in which each filter is an ORM expression as implemented by
18+
DjangoFilterBackend and seems to be in alignment with an interpretation of
19+
http://jsonapi.org/recommendations/#filtering, including relationship
20+
chaining. It also returns a 400 error for invalid filters.
21+
22+
Filters can be:
23+
- A resource field equality test:
24+
`?filter[qty]=123`
25+
- Apply other [field lookup](https://docs.djangoproject.com/en/stable/ref/models/querysets/#field-lookups) # noqa: E501
26+
operators:
27+
`?filter[name.icontains]=bar` or `?filter[name.isnull]=true...`
28+
- Membership in a list of values:
29+
`?filter[name.in]=abc,123,zzz (name in ['abc','123','zzz'])`
30+
- Filters can be combined for intersection (AND):
31+
`?filter[qty]=123&filter[name.in]=abc,123,zzz&filter[...]`
32+
- A related resource path can be used:
33+
`?filter[inventory.item.partNum]=123456 (where `inventory.item` is the relationship path)`
34+
35+
If you are also using rest_framework.filters.SearchFilter you'll want to customize
36+
the name of the query parameter for searching to make sure it doesn't conflict
37+
with a field name defined in the filterset.
38+
The recommended value is: `search_param="filter[search]"` but just make sure it's
39+
`filter[<something>]` to comply with the jsonapi spec requirement to use the filter
40+
keyword. The default is "search" unless overriden but it's used here just to make sure
41+
we don't complain about it being an invalid filter.
42+
"""
43+
# TODO: find a better way to deal with search_param.
44+
search_param = api_settings.SEARCH_PARAM
45+
46+
# Make this regex check for 'filter' as well as 'filter[...]'
47+
# Leave other incorrect usages of 'filter' to JSONAPIQueryValidationFilter.
48+
# See http://jsonapi.org/format/#document-member-names for allowed characters
49+
# and http://jsonapi.org/format/#document-member-names-reserved-characters for reserved
50+
# characters (for use in paths, lists or as delimiters).
51+
# regex `\w` matches [a-zA-Z0-9_].
52+
# TODO: U+0080 and above allowed but not recommended. Leave them out for now.e
53+
# Also, ' ' (space) is allowed within a member name but not recommended.
54+
filter_regex = re.compile(r'^filter(?P<ldelim>\[?)(?P<assoc>[\w\.\-]*)(?P<rdelim>\]?$)')
55+
56+
def _validate_filter(self, keys, filterset_class):
57+
for k in keys:
58+
if ((not filterset_class) or (k not in filterset_class.base_filters)):
59+
raise ValidationError("invalid filter[{}]".format(k))
60+
61+
def get_filterset(self, request, queryset, view):
62+
"""
63+
Sometimes there's no filterset_class defined yet the client still
64+
requests a filter. Make sure they see an error too. This means
65+
we have to get_filterset_kwargs() even if there's no filterset_class.
66+
67+
TODO: .base_filters vs. .filters attr (not always present)
68+
"""
69+
filterset_class = self.get_filterset_class(view, queryset)
70+
kwargs = self.get_filterset_kwargs(request, queryset, view)
71+
self._validate_filter(kwargs.pop('filter_keys'), filterset_class)
72+
if filterset_class is None:
73+
return None
74+
return filterset_class(**kwargs)
75+
76+
def get_filterset_kwargs(self, request, queryset, view):
77+
"""
78+
Turns filter[<field>]=<value> into <field>=<value> which is what
79+
DjangoFilterBackend expects
3080
"""
31-
A Django-style ORM filter implementation, using `django-filter`.
32-
33-
This is not part of the jsonapi standard per-se, other than the requirement
34-
to use the `filter` keyword: This is an optional implementation of style of
35-
filtering in which each filter is an ORM expression as implemented by
36-
DjangoFilterBackend and seems to be in alignment with an interpretation of
37-
http://jsonapi.org/recommendations/#filtering, including relationship
38-
chaining. It also returns a 400 error for invalid filters.
39-
40-
Filters can be:
41-
- A resource field equality test:
42-
`?filter[qty]=123`
43-
- Apply other [field lookup](https://docs.djangoproject.com/en/stable/ref/models/querysets/#field-lookups) # noqa: E501
44-
operators:
45-
`?filter[name.icontains]=bar` or `?filter[name.isnull]=true...`
46-
- Membership in a list of values:
47-
`?filter[name.in]=abc,123,zzz (name in ['abc','123','zzz'])`
48-
- Filters can be combined for intersection (AND):
49-
`?filter[qty]=123&filter[name.in]=abc,123,zzz&filter[...]`
50-
- A related resource path can be used:
51-
`?filter[inventory.item.partNum]=123456 (where `inventory.item` is the relationship path)`
52-
53-
If you are also using rest_framework.filters.SearchFilter you'll want to customize
54-
the name of the query parameter for searching to make sure it doesn't conflict
55-
with a field name defined in the filterset.
56-
The recommended value is: `search_param="filter[search]"` but just make sure it's
57-
`filter[<something>]` to comply with the jsonapi spec requirement to use the filter
58-
keyword. The default is "search" unless overriden but it's used here just to make sure
59-
we don't complain about it being an invalid filter.
81+
filter_keys = []
82+
# rewrite filter[field] query params to make DjangoFilterBackend work.
83+
data = request.query_params.copy()
84+
for qp, val in data.items():
85+
m = self.filter_regex.match(qp)
86+
if m and (not m.groupdict()['assoc'] or
87+
m.groupdict()['ldelim'] != '[' or m.groupdict()['rdelim'] != ']'):
88+
raise ValidationError("invalid filter: {}".format(qp))
89+
if m and qp != self.search_param:
90+
if not val:
91+
raise ValidationError("missing {} test value".format(qp))
92+
# convert jsonapi relationship path to Django ORM's __ notation
93+
key = m.groupdict()['assoc'].replace('.', '__')
94+
# undo JSON_API_FORMAT_FIELD_NAMES conversion:
95+
key = format_value(key, 'underscore')
96+
data[key] = val
97+
filter_keys.append(key)
98+
del data[qp]
99+
return {
100+
'data': data,
101+
'queryset': queryset,
102+
'request': request,
103+
'filter_keys': filter_keys,
104+
}
105+
106+
def filter_queryset(self, request, queryset, view):
60107
"""
61-
# TODO: find a better way to deal with search_param.
62-
search_param = api_settings.SEARCH_PARAM
63-
64-
# Make this regex check for 'filter' as well as 'filter[...]'
65-
# Leave other incorrect usages of 'filter' to JSONAPIQueryValidationFilter.
66-
# See http://jsonapi.org/format/#document-member-names for allowed characters
67-
# and http://jsonapi.org/format/#document-member-names-reserved-characters for reserved
68-
# characters (for use in paths, lists or as delimiters).
69-
# regex `\w` matches [a-zA-Z0-9_].
70-
# TODO: U+0080 and above allowed but not recommended. Leave them out for now. Fix later?
71-
# Also, ' ' (space) is allowed within a member name but not recommended.
72-
filter_regex = re.compile(r'^filter(?P<ldelim>\[?)(?P<assoc>[\w\.\-]*)(?P<rdelim>\]?$)')
73-
74-
def validate_filter(self, keys, filterset_class):
75-
for k in keys:
76-
if ((not filterset_class) or (k not in filterset_class.base_filters)):
77-
raise ValidationError("invalid filter[{}]".format(k))
78-
79-
def get_filterset(self, request, queryset, view):
80-
"""
81-
Sometimes there's no filterset_class defined yet the client still
82-
requests a filter. Make sure they see an error too. This means
83-
we have to get_filterset_kwargs() even if there's no filterset_class.
84-
85-
TODO: .base_filters vs. .filters attr (not always present)
86-
"""
87-
filterset_class = self.get_filterset_class(view, queryset)
88-
kwargs = self.get_filterset_kwargs(request, queryset, view)
89-
self.validate_filter(self.filter_keys, filterset_class)
90-
if filterset_class is None:
91-
return None
92-
return filterset_class(**kwargs)
93-
94-
def get_filterset_kwargs(self, request, queryset, view):
95-
"""
96-
Turns filter[<field>]=<value> into <field>=<value> which is what
97-
DjangoFilterBackend expects
98-
"""
99-
self.filter_keys = []
100-
# rewrite filter[field] query params to make DjangoFilterBackend work.
101-
data = request.query_params.copy()
102-
for qp, val in data.items():
103-
m = self.filter_regex.match(qp)
104-
if m and (not m.groupdict()['assoc'] or
105-
m.groupdict()['ldelim'] != '[' or m.groupdict()['rdelim'] != ']'):
106-
raise ValidationError("invalid filter: {}".format(qp))
107-
if m and qp != self.search_param:
108-
if not val:
109-
raise ValidationError("missing {} test value".format(qp))
110-
# convert jsonapi relationship path to Django ORM's __ notation
111-
key = m.groupdict()['assoc'].replace('.', '__')
112-
# undo JSON_API_FORMAT_FIELD_NAMES conversion:
113-
key = format_value(key, 'underscore')
114-
data[key] = val
115-
self.filter_keys.append(key)
116-
del data[qp]
117-
return {
118-
'data': data,
119-
'queryset': queryset,
120-
'request': request,
121-
}
122-
123-
def filter_queryset(self, request, queryset, view):
124-
"""
125-
backwards compatibility to 1.1
126-
"""
127-
filter_class = self.get_filter_class(view, queryset)
128-
129-
kwargs = self.get_filterset_kwargs(request, queryset, view)
130-
self.validate_filter(self.filter_keys, filter_class)
131-
132-
if filter_class:
133-
return filter_class(kwargs['data'], queryset=queryset, request=request).qs
134-
135-
return queryset
108+
Backwards compatibility to 1.1 (required for Python 2.7)
109+
In 1.1 filter_queryset does not call get_filterset or get_filterset_kwargs.
110+
"""
111+
# TODO: remove when Python 2.7 support is deprecated
112+
if VERSION >= (2, 0, 0):
113+
return super(JSONAPIDjangoFilter, self).filter_queryset(request, queryset, view)
114+
115+
filter_class = self.get_filter_class(view, queryset)
116+
117+
kwargs = self.get_filterset_kwargs(request, queryset, view)
118+
self._validate_filter(kwargs.pop('filter_keys'), filter_class)
119+
120+
if filter_class:
121+
return filter_class(kwargs['data'], queryset=queryset, request=request).qs
122+
123+
return queryset

0 commit comments

Comments
 (0)