Skip to content

Commit 9b40a58

Browse files
author
Ryan P Kilby
committed
Merge pull request #66 from rpkilby/df-update
django-filter update
2 parents 84d0676 + 6c9f79e commit 9b40a58

File tree

12 files changed

+626
-284
lines changed

12 files changed

+626
-284
lines changed

CHANGELOG.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
11
Unreleased
22
----------
33

4+
This release is tied to a major update of django-filter (more details in #66),
5+
which fixes how lookup expressions are resolved. 'in', 'range', and 'isnull'
6+
lookups no longer require special handling by django-rest-framework-filters.
7+
This has the following effects:
8+
9+
* Deprecates ArrayDecimalField/InSetNumberFilter
10+
* Deprecates ArrayCharField/InSetCharFilter
11+
* Deprecates FilterSet.fix_filter_field
12+
* Deprecates ALL_LOOKUPS in favor of '__all__' constant.
13+
* AllLookupsFilter now generates only valid lookup expressions.
14+
15+
* #2 'range' lookup types do not work
16+
* #15 Date lookup types do not work (year, day, ...)
17+
* #16 'in' lookup types do not work
418
* #64 Fix browsable API filter form
519
* #69 Fix compatibility with base django-filter `FilterSet`s
620
* #70 Refactor related filter handling, fixing some edge cases

rest_framework_filters/fields.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
2+
import warnings
13
from django import forms
4+
25
from django_filters.widgets import BooleanWidget
36

47

@@ -7,6 +10,14 @@ class BooleanField(forms.BooleanField):
710

811

912
class ArrayDecimalField(forms.DecimalField):
13+
def __init__(self, *args, **kwargs):
14+
super(ArrayDecimalField, self).__init__(*args, **kwargs)
15+
warnings.warn(
16+
'ArrayDecimalField is deprecated and no longer necessary. See: '
17+
'https://github.com/philipn/django-rest-framework-filters/issues/62',
18+
DeprecationWarning, stacklevel=3
19+
)
20+
1021
def clean(self, value):
1122
if value is None:
1223
return None
@@ -18,6 +29,14 @@ def clean(self, value):
1829

1930

2031
class ArrayCharField(forms.CharField):
32+
def __init__(self, *args, **kwargs):
33+
super(ArrayCharField, self).__init__(*args, **kwargs)
34+
warnings.warn(
35+
'ArrayCharField is deprecated and no longer necessary. See: '
36+
'https://github.com/philipn/django-rest-framework-filters/issues/62',
37+
DeprecationWarning, stacklevel=3
38+
)
39+
2140
def clean(self, value):
2241
if value is None:
2342
return None

rest_framework_filters/filters.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
from __future__ import absolute_import
22
from __future__ import unicode_literals
33

4+
import warnings
45
from django.utils import six
56

67
from django_filters.filters import *
7-
from django_filters.filters import LOOKUP_TYPES
88

99
from . import fields
1010

11-
ALL_LOOKUPS = LOOKUP_TYPES
11+
12+
class ALL_LOOKUPS(object):
13+
pass
1214

1315

1416
def _import_class(path):
@@ -49,17 +51,36 @@ class AllLookupsFilter(Filter):
4951
###################################################
5052
# Fixed-up versions of some of the default filters
5153
###################################################
54+
55+
# This class is necessary, as directly django-filter's BooleanFilter
56+
# is using the incorrect form widget.
5257
class BooleanFilter(BooleanFilter):
5358
field_class = fields.BooleanField
5459

5560

5661
class InSetNumberFilter(Filter):
5762
field_class = fields.ArrayDecimalField
5863

64+
def __init__(self, *args, **kwargs):
65+
super(InSetNumberFilter, self).__init__(*args, **kwargs)
66+
warnings.warn(
67+
'InSetNumberFilter is deprecated and no longer necessary. See: '
68+
'https://github.com/philipn/django-rest-framework-filters/issues/62',
69+
DeprecationWarning, stacklevel=2
70+
)
71+
5972

6073
class InSetCharFilter(Filter):
6174
field_class = fields.ArrayCharField
6275

76+
def __init__(self, *args, **kwargs):
77+
super(InSetCharFilter, self).__init__(*args, **kwargs)
78+
warnings.warn(
79+
'InSetCharFilter is deprecated and no longer necessary. See: '
80+
'https://github.com/philipn/django-rest-framework-filters/issues/62',
81+
DeprecationWarning, stacklevel=2
82+
)
83+
6384

6485
class MethodFilter(Filter):
6586
"""

rest_framework_filters/filterset.py

Lines changed: 55 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,34 @@
1010
from django.db.models.fields.related import ForeignObjectRel
1111
from django.utils import six
1212

13-
import django_filters
14-
import django_filters.filters
1513
from django_filters import filterset
1614

1715
from . import filters
16+
from . import utils
17+
18+
19+
def _base(f):
20+
f._base = True
21+
return f
22+
23+
24+
def _get_fix_filter_field(cls):
25+
method = getattr(cls, 'fix_filter_field')
26+
if not getattr(method, '_base', False):
27+
warnings.warn(
28+
'fix_filter_field is deprecated and no longer necessary. See: '
29+
'https://github.com/philipn/django-rest-framework-filters/issues/62',
30+
DeprecationWarning, stacklevel=2
31+
)
32+
return cls.fix_filter_field
1833

1934

2035
class FilterSetMetaclass(filterset.FilterSetMetaclass):
2136
def __new__(cls, name, bases, attrs):
37+
cls.convert__all__(attrs)
38+
2239
new_class = super(FilterSetMetaclass, cls).__new__(cls, name, bases, attrs)
40+
fix_filter_field = _get_fix_filter_field(new_class)
2341

2442
# Populate our FilterSet fields with all the possible
2543
# filters for the AllLookupsFilter field.
@@ -28,24 +46,25 @@ def __new__(cls, name, bases, attrs):
2846
model = new_class._meta.model
2947
field = filterset.get_model_field(model, filter_.name)
3048

31-
for lookup_type in django_filters.filters.LOOKUP_TYPES:
49+
for lookup_expr in utils.lookups_for_field(field):
3250
if isinstance(field, ForeignObjectRel):
3351
f = new_class.filter_for_reverse_field(field, filter_.name)
3452
else:
35-
f = new_class.filter_for_field(field, filter_.name)
36-
f.lookup_type = lookup_type
37-
f = new_class.fix_filter_field(f)
53+
f = new_class.filter_for_field(field, filter_.name, lookup_expr)
54+
f = fix_filter_field(f)
3855

3956
# compute filter name
40-
filter_name = name
57+
filter_name = LOOKUP_SEP.join([name, lookup_expr])
58+
4159
# Don't add "exact" to filter names
42-
if lookup_type != 'exact':
43-
filter_name = LOOKUP_SEP.join([name, lookup_type])
60+
_exact = LOOKUP_SEP + 'exact'
61+
if filter_name.endswith(_exact):
62+
filter_name = filter_name[:-len(_exact)]
4463

4564
new_class.base_filters[filter_name] = f
4665

4766
elif name not in new_class.declared_filters:
48-
new_class.base_filters[name] = new_class.fix_filter_field(filter_)
67+
new_class.base_filters[name] = fix_filter_field(filter_)
4968

5069
return new_class
5170

@@ -61,9 +80,33 @@ def related_filters(self):
6180
])
6281
return self._related_filters
6382

83+
@staticmethod
84+
def convert__all__(attrs):
85+
"""
86+
Extract Meta.fields and convert any fields w/ `__all__`
87+
to a declared AllLookupsFilter.
88+
"""
89+
meta = attrs.get('Meta', None)
90+
fields = getattr(meta, 'fields', None)
91+
92+
if isinstance(fields, dict):
93+
for name, lookups in six.iteritems(fields.copy()):
94+
if lookups == filters.ALL_LOOKUPS:
95+
warnings.warn(
96+
"ALL_LOOKUPS has been deprecated in favor of '__all__'. See: "
97+
"https://github.com/philipn/django-rest-framework-filters/issues/62",
98+
DeprecationWarning, stacklevel=2
99+
)
100+
lookups = '__all__'
101+
102+
if lookups == '__all__':
103+
del fields[name]
104+
attrs[name] = filters.AllLookupsFilter()
105+
64106

65107
class FilterSet(six.with_metaclass(FilterSetMetaclass, filterset.FilterSet)):
66108
filter_overrides = {
109+
# uses API-friendly django_filters.BooleanWidget
67110
models.BooleanField: {
68111
'filter_class': filters.BooleanFilter,
69112
},
@@ -91,7 +134,7 @@ def __init__(self, *args, **kwargs):
91134
# Add an 'isnull' filter to allow checking if the relation is empty.
92135
filter_name = "%s%sisnull" % (filter_.name, LOOKUP_SEP)
93136
if filter_name not in self.filters:
94-
self.filters[filter_name] = filters.BooleanFilter(name=filter_.name, lookup_type='isnull')
137+
self.filters[filter_name] = filters.BooleanFilter(name=filter_.name, lookup_expr='isnull')
95138

96139
elif isinstance(filter_, filters.MethodFilter):
97140
filter_.resolve_action()
@@ -276,15 +319,6 @@ def qs(self):
276319
return qs
277320

278321
@classmethod
322+
@_base
279323
def fix_filter_field(cls, f):
280-
"""
281-
Fix the filter field based on the lookup type.
282-
"""
283-
lookup_type = f.lookup_type
284-
if lookup_type == 'isnull':
285-
return filters.BooleanFilter(name=f.name, lookup_type='isnull')
286-
if lookup_type == 'in' and type(f) == filters.NumberFilter:
287-
return filters.InSetNumberFilter(name=f.name, lookup_type='in')
288-
if lookup_type == 'in' and type(f) == filters.CharFilter:
289-
return filters.InSetCharFilter(name=f.name, lookup_type='in')
290324
return f

rest_framework_filters/utils.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
2+
from collections import OrderedDict
3+
4+
from django.db.models.constants import LOOKUP_SEP
5+
from django.db.models.lookups import Transform
6+
from django.utils import six
7+
8+
9+
def lookups_for_field(model_field):
10+
"""
11+
Generates a list of all possible lookup expressions for a model field.
12+
"""
13+
lookups = []
14+
15+
for expr, lookup in six.iteritems(class_lookups(model_field)):
16+
if issubclass(lookup, Transform):
17+
lookups += [
18+
LOOKUP_SEP.join([expr, transform]) for transform
19+
in lookups_for_field(lookup(model_field).output_field)
20+
]
21+
else:
22+
lookups.append(expr)
23+
24+
return lookups
25+
26+
27+
def class_lookups(model_field):
28+
"""
29+
Get a compiled set of class_lookups for a model field.
30+
"""
31+
field_class = model_field.__class__
32+
class_lookups = OrderedDict()
33+
34+
# traverse MRO in reverse, as this puts standard
35+
# lookups before subclass transforms/lookups
36+
for cls in field_class.mro()[::-1]:
37+
if hasattr(cls, 'class_lookups'):
38+
class_lookups.update(getattr(cls, 'class_lookups'))
39+
40+
return class_lookups

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def get_package_data(package):
4343
zip_safe=False,
4444
install_requires=[
4545
'djangorestframework',
46-
'django-filter>=0.12.0',
46+
'django-filter>=0.13.0',
4747
],
4848
classifiers=[
4949
'Development Status :: 5 - Production/Stable',

0 commit comments

Comments
 (0)