Skip to content

Commit 4d02cdf

Browse files
author
Carlton Gibson
authored
Merge pull request carltongibson#550 from rpkilby/rework-filter-generation
Rework filter generation
2 parents 4dd64e5 + 85ba7c4 commit 4d02cdf

File tree

4 files changed

+156
-117
lines changed

4 files changed

+156
-117
lines changed

django_filters/filterset.py

+123-105
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
from django.db.models.constants import LOOKUP_SEP
1111
from django.db.models.fields.related import ForeignObjectRel
1212
from django.utils import six
13-
from django.utils.translation import ugettext as _
1413

1514
from .conf import settings
1615
from .compat import remote_field, remote_queryset
@@ -21,75 +20,20 @@
2120
from .utils import try_dbfield, get_all_model_fields, get_model_field, resolve_field
2221

2322

24-
def get_declared_filters(bases, attrs, with_base_filters=True):
25-
filters = []
26-
for filter_name, obj in list(attrs.items()):
27-
if isinstance(obj, Filter):
28-
obj = attrs.pop(filter_name)
29-
if getattr(obj, 'name', None) is None:
30-
obj.name = filter_name
31-
filters.append((filter_name, obj))
32-
filters.sort(key=lambda x: x[1].creation_counter)
33-
34-
if with_base_filters:
35-
for base in bases[::-1]:
36-
if hasattr(base, 'base_filters'):
37-
filters = list(base.base_filters.items()) + filters
38-
else:
39-
for base in bases[::-1]:
40-
if hasattr(base, 'declared_filters'):
41-
filters = list(base.declared_filters.items()) + filters
23+
def get_filter_name(field_name, lookup_expr):
24+
"""
25+
Combine a field name and lookup expression into a usable filter name.
26+
Exact lookups are the implicit default, so "exact" is stripped from the
27+
end of the filter name.
28+
"""
29+
filter_name = LOOKUP_SEP.join([field_name, lookup_expr])
4230

43-
return OrderedDict(filters)
44-
45-
46-
def filters_for_model(model, fields=None, exclude=None, filter_for_field=None,
47-
filter_for_reverse_field=None):
48-
field_dict = OrderedDict()
49-
50-
# Setting exclude with no fields implies all other fields.
51-
if exclude is not None and fields is None:
52-
fields = ALL_FIELDS
53-
54-
# All implies all db fields associated with a filter_class.
55-
if fields == ALL_FIELDS:
56-
fields = get_all_model_fields(model)
57-
58-
# Loop through the list of fields.
59-
for f in fields:
60-
# Skip the field if excluded.
61-
if exclude is not None and f in exclude:
62-
continue
63-
field = get_model_field(model, f)
64-
# Do nothing if the field doesn't exist.
65-
if field is None:
66-
field_dict[f] = None
67-
continue
68-
if isinstance(field, ForeignObjectRel):
69-
filter_ = filter_for_reverse_field(field, f)
70-
if filter_:
71-
field_dict[f] = filter_
72-
# If fields is a dictionary, it must contain lists.
73-
elif isinstance(fields, dict):
74-
# Create a filter for each lookup type.
75-
for lookup_expr in fields[f]:
76-
filter_ = filter_for_field(field, f, lookup_expr)
77-
78-
if filter_:
79-
filter_name = LOOKUP_SEP.join([f, lookup_expr])
80-
81-
# Don't add "exact" to filter names
82-
_exact = LOOKUP_SEP + 'exact'
83-
if filter_name.endswith(_exact):
84-
filter_name = filter_name[:-len(_exact)]
85-
86-
field_dict[filter_name] = filter_
87-
# If fields is a list, it contains strings.
88-
else:
89-
filter_ = filter_for_field(field, f)
90-
if filter_:
91-
field_dict[f] = filter_
92-
return field_dict
31+
# This also works with transformed exact lookups, such as 'date__exact'
32+
_exact = LOOKUP_SEP + 'exact'
33+
if filter_name.endswith(_exact):
34+
filter_name = filter_name[:-len(_exact)]
35+
36+
return filter_name
9337

9438

9539
def get_full_clean_override(together):
@@ -134,36 +78,36 @@ def __init__(self, options=None):
13478

13579
class FilterSetMetaclass(type):
13680
def __new__(cls, name, bases, attrs):
137-
try:
138-
parents = [b for b in bases if issubclass(b, FilterSet)]
139-
except NameError:
140-
# We are defining FilterSet itself here
141-
parents = None
142-
declared_filters = get_declared_filters(bases, attrs, False)
143-
new_class = super(
144-
FilterSetMetaclass, cls).__new__(cls, name, bases, attrs)
145-
146-
if not parents:
147-
return new_class
148-
149-
opts = new_class._meta = FilterSetOptions(
150-
getattr(new_class, 'Meta', None))
151-
152-
if opts.model and (opts.fields is not None or opts.exclude is not None):
153-
filters = new_class.filters_for_model(opts.model, opts)
154-
filters.update(declared_filters)
155-
else:
156-
filters = declared_filters
81+
attrs['declared_filters'] = cls.get_declared_filters(bases, attrs)
15782

158-
not_defined = next((k for k, v in filters.items() if v is None), False)
159-
if not_defined:
160-
raise TypeError("Meta.fields contains a field that isn't defined "
161-
"on this FilterSet: {}".format(not_defined))
83+
new_class = super(FilterSetMetaclass, cls).__new__(cls, name, bases, attrs)
84+
new_class._meta = FilterSetOptions(getattr(new_class, 'Meta', None))
85+
new_class.base_filters = new_class.get_filters()
16286

163-
new_class.declared_filters = declared_filters
164-
new_class.base_filters = filters
16587
return new_class
16688

89+
@classmethod
90+
def get_declared_filters(cls, bases, attrs):
91+
filters = [
92+
(filter_name, attrs.pop(filter_name))
93+
for filter_name, obj in list(attrs.items())
94+
if isinstance(obj, Filter)
95+
]
96+
97+
# Default the `filter.name` to the attribute name on the filterset
98+
for filter_name, f in filters:
99+
if getattr(f, 'name', None) is None:
100+
f.name = filter_name
101+
102+
filters.sort(key=lambda x: x[1].creation_counter)
103+
104+
# merge declared filters from base classes
105+
for base in reversed(bases):
106+
if hasattr(base, 'declared_filters'):
107+
filters = list(base.declared_filters.items()) + filters
108+
109+
return OrderedDict(filters)
110+
167111

168112
FILTER_FOR_DBFIELD_DEFAULTS = {
169113
models.AutoField: {'filter_class': NumberFilter},
@@ -235,12 +179,10 @@ def __init__(self, data=None, queryset=None, prefix=None, strict=None, request=N
235179
self.request = request
236180

237181
self.filters = copy.deepcopy(self.base_filters)
238-
# propagate the model being used through the filters
182+
239183
for filter_ in self.filters.values():
184+
# propagate the model and filterset to the filters
240185
filter_.model = self._meta.model
241-
242-
# Apply the parent to the filters, this will allow the filters to access the filterset
243-
for filter_key, filter_ in six.iteritems(self.filters):
244186
filter_.parent = self
245187

246188
@property
@@ -288,12 +230,88 @@ def form(self):
288230
return self._form
289231

290232
@classmethod
291-
def filters_for_model(cls, model, opts):
292-
return filters_for_model(
293-
model, opts.fields, opts.exclude,
294-
cls.filter_for_field,
295-
cls.filter_for_reverse_field
296-
)
233+
def get_fields(cls):
234+
"""
235+
Resolve the 'fields' argument that should be used for generating filters on the
236+
filterset. This is 'Meta.fields' sans the fields in 'Meta.exclude'.
237+
"""
238+
model = cls._meta.model
239+
fields = cls._meta.fields
240+
exclude = cls._meta.exclude
241+
242+
assert not (fields is None and exclude is None), \
243+
"Setting 'Meta.model' without either 'Meta.fields' or 'Meta.exclude' " \
244+
"has been deprecated since 0.15.0 and is now disallowed. Add an explicit" \
245+
"'Meta.fields' or 'Meta.exclude' to the %s class." % cls.__name__
246+
247+
# Setting exclude with no fields implies all other fields.
248+
if exclude is not None and fields is None:
249+
fields = ALL_FIELDS
250+
251+
# Resolve ALL_FIELDS into all fields for the filterset's model.
252+
if fields == ALL_FIELDS:
253+
fields = get_all_model_fields(model)
254+
255+
# Remove excluded fields
256+
exclude = exclude or []
257+
if not isinstance(fields, dict):
258+
fields = [(f, ['exact']) for f in fields if f not in exclude]
259+
else:
260+
fields = [(f, lookups) for f, lookups in fields.items() if f not in exclude]
261+
262+
return OrderedDict(fields)
263+
264+
@classmethod
265+
def get_filters(cls):
266+
"""
267+
Get all filters for the filterset. This is the combination of declared and
268+
generated filters.
269+
"""
270+
271+
# No model specified - skip filter generation
272+
if not cls._meta.model:
273+
return cls.declared_filters.copy()
274+
275+
# Determine the filters that should be included on the filterset.
276+
filters = OrderedDict()
277+
fields = cls.get_fields()
278+
undefined = []
279+
280+
for field_name, lookups in fields.items():
281+
field = get_model_field(cls._meta.model, field_name)
282+
283+
# warn if the field doesn't exist.
284+
if field is None:
285+
undefined.append(field_name)
286+
287+
# ForeignObjectRel does not support non-exact lookups
288+
if isinstance(field, ForeignObjectRel):
289+
filters[field_name] = cls.filter_for_reverse_field(field, field_name)
290+
continue
291+
292+
for lookup_expr in lookups:
293+
filter_name = get_filter_name(field_name, lookup_expr)
294+
295+
# If the filter is explicitly declared on the class, skip generation
296+
if filter_name in cls.declared_filters:
297+
filters[filter_name] = cls.declared_filters[filter_name]
298+
continue
299+
300+
if field is not None:
301+
filters[filter_name] = cls.filter_for_field(field, field_name, lookup_expr)
302+
303+
# filter out declared filters
304+
undefined = [f for f in undefined if f not in cls.declared_filters]
305+
if undefined:
306+
raise TypeError(
307+
"'Meta.fields' contains fields that are not defined on this FilterSet: "
308+
"%s" % ', '.join(undefined)
309+
)
310+
311+
# Add in declared filters. This is necessary since we don't enforce adding
312+
# declared filters to the 'Meta.fields' option
313+
filters.update(cls.declared_filters)
314+
return filters
297315

298316
@classmethod
299317
def filter_for_field(cls, f, name, lookup_expr='exact'):

tests/test_conf.py

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class StrictnessTests(TestCase):
3939
class F(FilterSet):
4040
class Meta:
4141
model = User
42+
fields = []
4243

4344
def test_settings_default(self):
4445
self.assertEqual(self.F().strict, STRICTNESS.RETURN_NO_RESULTS)

tests/test_filtering.py

+1
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ class F(FilterSet):
209209

210210
class Meta:
211211
model = Article
212+
fields = ['author']
212213

213214
# sanity check to make sure the filter is setup correctly
214215
f = F({'author': '1'})

tests/test_filterset.py

+31-12
Original file line numberDiff line numberDiff line change
@@ -322,20 +322,21 @@ class Meta:
322322
['title', 'price', 'average_rating'])
323323

324324
def test_model_no_fields_or_exclude(self):
325-
class F(FilterSet):
326-
class Meta:
327-
model = Book
325+
with self.assertRaises(AssertionError) as excinfo:
326+
class F(FilterSet):
327+
class Meta:
328+
model = Book
328329

329-
self.assertEqual(len(F.declared_filters), 0)
330-
self.assertEqual(len(F.base_filters), 0)
331-
self.assertListEqual(list(F.base_filters), [])
330+
self.assertIn(
331+
"Setting 'Meta.model' without either 'Meta.fields' or 'Meta.exclude'",
332+
str(excinfo.exception)
333+
)
332334

333-
def test_model_exclude_is_none(self):
334-
# equivalent to unset fields/exclude
335+
def test_model_fields_empty(self):
335336
class F(FilterSet):
336337
class Meta:
337338
model = Book
338-
exclude = None
339+
fields = []
339340

340341
self.assertEqual(len(F.declared_filters), 0)
341342
self.assertEqual(len(F.base_filters), 0)
@@ -427,9 +428,12 @@ class F(FilterSet):
427428
class Meta:
428429
model = Book
429430
fields = ('username', 'price', 'other', 'another')
430-
self.assertEqual(excinfo.exception.args, (
431-
"Meta.fields contains a field that isn't defined "
432-
"on this FilterSet: other",))
431+
432+
self.assertEqual(
433+
str(excinfo.exception),
434+
"'Meta.fields' contains fields that are not defined on this FilterSet: "
435+
"other, another"
436+
)
433437

434438
def test_meta_fields_dictionary_containing_unknown(self):
435439
with self.assertRaises(TypeError):
@@ -528,6 +532,16 @@ class Meta:
528532

529533
self.assertEqual(list(F.base_filters.keys()), ['ip', 'mask'])
530534

535+
def test_custom_declared_field_no_warning(self):
536+
class F(FilterSet):
537+
mask = CharFilter()
538+
539+
class Meta:
540+
model = NetworkSetting
541+
fields = ['mask']
542+
543+
self.assertEqual(list(F.base_filters.keys()), ['mask'])
544+
531545
def test_filterset_for_proxy_model(self):
532546
class F(FilterSet):
533547
class Meta:
@@ -612,6 +626,7 @@ def test_settings_default(self):
612626
class F(FilterSet):
613627
class Meta:
614628
model = User
629+
fields = []
615630

616631
# Ensure default is not IGNORE
617632
self.assertEqual(F().strict, STRICTNESS.RETURN_NO_RESULTS)
@@ -624,6 +639,7 @@ def test_meta_value(self):
624639
class F(FilterSet):
625640
class Meta:
626641
model = User
642+
fields = []
627643
strict = STRICTNESS.IGNORE
628644

629645
self.assertEqual(F().strict, STRICTNESS.IGNORE)
@@ -632,6 +648,7 @@ def test_init_default(self):
632648
class F(FilterSet):
633649
class Meta:
634650
model = User
651+
fields = []
635652
strict = STRICTNESS.IGNORE
636653

637654
strict = STRICTNESS.RAISE_VALIDATION_ERROR
@@ -641,6 +658,7 @@ def test_legacy_value(self):
641658
class F(FilterSet):
642659
class Meta:
643660
model = User
661+
fields = []
644662

645663
self.assertEqual(F(strict=False).strict, STRICTNESS.IGNORE)
646664

@@ -770,6 +788,7 @@ class F(FilterSet):
770788

771789
class Meta:
772790
model = User
791+
fields = []
773792

774793
def filter_f(inner_self, qs, name, value):
775794
self.assertIsInstance(inner_self, F)

0 commit comments

Comments
 (0)