Skip to content

Commit 074aaef

Browse files
author
Ryan P Kilby
committed
Update AllLookupsFilter handling
This includes deprecating ALL_LOOKUPS for '__all__', as the former was an array of lookup_types. Given the new lookup expression handling, this is no longer compatible. '__all__' is just a Meta.fields shorthand for declaring an AllLookupsFilter
1 parent 3031145 commit 074aaef

File tree

6 files changed

+165
-4
lines changed

6 files changed

+165
-4
lines changed

rest_framework_filters/filters.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@
44
from django.utils import six
55

66
from django_filters.filters import *
7-
from django_filters.filters import LOOKUP_TYPES
87

98
from . import fields
109

11-
ALL_LOOKUPS = LOOKUP_TYPES
10+
11+
class ALL_LOOKUPS(object):
12+
pass
1213

1314

1415
def _import_class(path):

rest_framework_filters/filterset.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from django_filters import filterset
1414

1515
from . import filters
16+
from . import utils
1617

1718

1819
def _base(f):
@@ -33,6 +34,8 @@ def _get_fix_filter_field(cls):
3334

3435
class FilterSetMetaclass(filterset.FilterSetMetaclass):
3536
def __new__(cls, name, bases, attrs):
37+
cls.convert__all__(attrs)
38+
3639
new_class = super(FilterSetMetaclass, cls).__new__(cls, name, bases, attrs)
3740
fix_filter_field = _get_fix_filter_field(new_class)
3841

@@ -43,7 +46,7 @@ def __new__(cls, name, bases, attrs):
4346
model = new_class._meta.model
4447
field = filterset.get_model_field(model, filter_.name)
4548

46-
for lookup_expr in filters.LOOKUP_TYPES:
49+
for lookup_expr in utils.lookups_for_field(field):
4750
if isinstance(field, ForeignObjectRel):
4851
f = new_class.filter_for_reverse_field(field, filter_.name)
4952
else:
@@ -77,6 +80,29 @@ def related_filters(self):
7780
])
7881
return self._related_filters
7982

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+
80106

81107
class FilterSet(six.with_metaclass(FilterSetMetaclass, filterset.FilterSet)):
82108
filter_overrides = {

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

tests/test_deprecations.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
from django.test import TestCase
44

55
from rest_framework_filters import FilterSet
6+
from rest_framework_filters import filters
67

8+
from .testapp.models import User
79
from .testapp.filters import UserFilter
810

911

@@ -57,3 +59,43 @@ class F(FilterSet):
5759
pass
5860

5961
self.assertEqual(len(w), 0)
62+
63+
64+
class AllLookupsDeprecationTests(TestCase):
65+
66+
def test_ALL_LOOKUPS_notification(self):
67+
with warnings.catch_warnings(record=True) as w:
68+
warnings.simplefilter("always")
69+
70+
class F(FilterSet):
71+
class Meta:
72+
model = User
73+
fields = {
74+
'last_login': filters.ALL_LOOKUPS,
75+
}
76+
77+
self.assertEqual(len(w), 1)
78+
79+
def test_no_notification_for__all__(self):
80+
with warnings.catch_warnings(record=True) as w:
81+
warnings.simplefilter("always")
82+
83+
class F(FilterSet):
84+
class Meta:
85+
model = User
86+
fields = {
87+
'last_login': '__all__',
88+
}
89+
90+
self.assertEqual(len(w), 0)
91+
92+
def test_no_notification_for_fields_list(self):
93+
with warnings.catch_warnings(record=True) as w:
94+
warnings.simplefilter("always")
95+
96+
class F(FilterSet):
97+
class Meta:
98+
model = User
99+
fields = ['last_login']
100+
101+
self.assertEqual(len(w), 0)

tests/test_utils.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
2+
import unittest
3+
import django
4+
from django.test import TestCase
5+
6+
from rest_framework_filters import utils
7+
8+
from .testapp.models import Person
9+
10+
11+
class LookupsForFieldTests(TestCase):
12+
def test_standard_field(self):
13+
model_field = Person._meta.get_field('name')
14+
lookups = utils.lookups_for_field(model_field)
15+
16+
self.assertIn('exact', lookups)
17+
if django.VERSION >= (1, 9):
18+
self.assertNotIn('year', lookups)
19+
self.assertNotIn('date', lookups)
20+
else:
21+
self.assertIn('year', lookups)
22+
23+
@unittest.skipIf(django.VERSION < (1, 9), "version does not support transformed lookup expressions")
24+
def test_transformed_field(self):
25+
model_field = Person._meta.get_field('datetime_joined')
26+
lookups = utils.lookups_for_field(model_field)
27+
28+
self.assertIn('exact', lookups)
29+
self.assertIn('year__exact', lookups)
30+
self.assertIn('date__year__exact', lookups)
31+
32+
33+
class ClassLookupsTests(TestCase):
34+
def test_standard_field(self):
35+
model_field = Person._meta.get_field('name')
36+
class_lookups = utils.class_lookups(model_field)
37+
38+
self.assertIn('exact', class_lookups)
39+
if django.VERSION >= (1, 9):
40+
self.assertNotIn('year', class_lookups)
41+
self.assertNotIn('date', class_lookups)
42+
else:
43+
self.assertIn('year', class_lookups)
44+
45+
@unittest.skipIf(django.VERSION < (1, 9), "version does not support transformed lookup expressions")
46+
def test_transformed_field(self):
47+
model_field = Person._meta.get_field('datetime_joined')
48+
class_lookups = utils.class_lookups(model_field)
49+
50+
self.assertIn('exact', class_lookups)
51+
self.assertIn('year', class_lookups)
52+
self.assertIn('date', class_lookups)

tests/testapp/filters.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,4 +223,4 @@ class BlogPostOverrideFilter(FilterSet):
223223

224224
class Meta:
225225
model = BlogPost
226-
fields = {'publish_date': filters.ALL_LOOKUPS, }
226+
fields = {'publish_date': '__all__', }

0 commit comments

Comments
 (0)