Skip to content

Commit b11dfa2

Browse files
author
Ryan P Kilby
authored
Fix declared filters overwritten by AllLookupsFilter (#123)
* Add failing test for declared filter * Use filters_for_model to create all/related looups
1 parent 6777c7d commit b11dfa2

File tree

2 files changed

+61
-40
lines changed

2 files changed

+61
-40
lines changed

rest_framework_filters/filterset.py

Lines changed: 30 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import warnings
77

88
from django.db.models.constants import LOOKUP_SEP
9-
from django.db.models.fields.related import ForeignObjectRel
109
from django.utils import six
1110

1211
from django_filters import filterset, rest_framework
@@ -35,7 +34,7 @@ class FilterSetMetaclass(filterset.FilterSetMetaclass):
3534
def __new__(cls, name, bases, attrs):
3635
new_class = super(FilterSetMetaclass, cls).__new__(cls, name, bases, attrs)
3736
fix_filter_field = _get_fix_filter_field(new_class)
38-
opts = new_class._meta
37+
opts = copy.deepcopy(new_class._meta)
3938

4039
# order_by is not compatible.
4140
if opts.order_by:
@@ -51,38 +50,36 @@ def __new__(cls, name, bases, attrs):
5150
if not opts.model:
5251
return new_class
5352

54-
# Populate our FilterSet fields with all the possible
55-
# filters for the AllLookupsFilter field.
53+
# Determine declared filters and filters to generate lookups from. Declared
54+
# filters have precedence over generated filters and should not be overwritten.
55+
declared_filters, lookups_filters = OrderedDict(), OrderedDict()
56+
for name, f in six.iteritems(new_class.declared_filters):
57+
if isinstance(f, (filters.AllLookupsFilter, filters.RelatedFilter)):
58+
lookups_filters[name] = f
59+
60+
# `AllLookupsFilter` is an exception, as it should be overwritten
61+
if not isinstance(f, filters.AllLookupsFilter):
62+
declared_filters[name] = f
63+
64+
# generate filters for AllLookups/Related filters
65+
# name is the parameter name on the filterset, f.name is the model field's name
66+
for name, f in six.iteritems(lookups_filters):
67+
opts.fields = {f.name: f.lookups or []}
68+
new_filters = new_class.filters_for_model(opts.model, opts)
69+
70+
# filters_for_model generate param names from the model field name
71+
# replace model field name with the parameter name from the filerset
72+
new_class.base_filters.update(OrderedDict(
73+
(param.replace(f.name, name, 1), v)
74+
for param, v in six.iteritems(new_filters)
75+
))
76+
77+
# re-apply declared filters (sans `AllLookupsFilter`s)
78+
new_class.base_filters.update(declared_filters)
79+
80+
# TODO: remove with deprecations
5681
for name, filter_ in six.iteritems(new_class.base_filters.copy()):
57-
if isinstance(filter_, (filters.AllLookupsFilter, filters.RelatedFilter)):
58-
field = filterset.get_model_field(opts.model, filter_.name)
59-
60-
lookups = filter_.lookups or []
61-
if lookups == '__all__':
62-
lookups = utils.lookups_for_field(field)
63-
64-
for lookup_expr in lookups:
65-
if isinstance(filter_, filters.RelatedFilter) and lookup_expr == 'exact':
66-
# Don't replace the RelatedFilter
67-
continue
68-
69-
if isinstance(field, ForeignObjectRel):
70-
f = new_class.filter_for_reverse_field(field, filter_.name)
71-
else:
72-
f = new_class.filter_for_field(field, filter_.name, lookup_expr)
73-
f = fix_filter_field(f)
74-
75-
# compute filter name
76-
filter_name = LOOKUP_SEP.join([name, lookup_expr])
77-
78-
# Don't add "exact" to filter names
79-
_exact = LOOKUP_SEP + 'exact'
80-
if filter_name.endswith(_exact):
81-
filter_name = filter_name[:-len(_exact)]
82-
83-
new_class.base_filters[filter_name] = f
84-
85-
elif name not in new_class.declared_filters:
82+
if name not in new_class.declared_filters:
8683
new_class.base_filters[name] = fix_filter_field(filter_)
8784

8885
return new_class

tests/test_filterset.py

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,21 @@
2525

2626
class LookupsFilterTests(TestCase):
2727
"""
28-
Test basic filter construction for `AllLookupsFilter`, '__all__',
29-
and `RelatedFilter.lookups`.
28+
Test basic filter construction for `AllLookupsFilter`, '__all__', and `RelatedFilter.lookups`.
3029
"""
3130

31+
def test_alllookupsfilter_meta_fields_unmodified(self):
32+
f = []
33+
34+
class F(FilterSet):
35+
id = filters.AllLookupsFilter()
36+
37+
class Meta:
38+
model = Note
39+
fields = f
40+
41+
self.assertIs(F._meta.fields, f)
42+
3243
def test_alllookupsfilter_replaced(self):
3344
# See: https://github.com/philipn/django-rest-framework-filters/issues/118
3445
class F(FilterSet):
@@ -91,16 +102,29 @@ class Meta:
91102

92103
def test_declared_filter_persistence_with__all__(self):
93104
# ensure that __all__ does not overwrite declared filters.
105+
f = filters.Filter()
106+
94107
class F(FilterSet):
95-
name = filters.ChoiceFilter(lookup_expr='iexact')
108+
name = f
96109

97110
class Meta:
98111
model = Person
99-
fields = {
100-
'name': '__all__',
101-
}
112+
fields = {'name': '__all__'}
113+
114+
self.assertIs(F.base_filters['name'], f)
115+
116+
def test_declared_filter_persistence_with_alllookupsfilter(self):
117+
# ensure that AllLookupsFilter does not overwrite declared filters.
118+
f = filters.Filter()
119+
120+
class F(FilterSet):
121+
id = filters.AllLookupsFilter()
122+
id__in = f
123+
124+
class Meta:
125+
model = Note
102126

103-
self.assertIsInstance(F.base_filters['name'], filters.ChoiceFilter)
127+
self.assertIs(F.base_filters['id__in'], f)
104128

105129

106130
class GetFilterNameTests(TestCase):

0 commit comments

Comments
 (0)