Skip to content

Commit 85ba7c4

Browse files
author
Ryan P Kilby
committed
Refactor filter generation
1 parent 49a1e16 commit 85ba7c4

File tree

1 file changed

+100
-79
lines changed

1 file changed

+100
-79
lines changed

django_filters/filterset.py

Lines changed: 100 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -20,53 +20,20 @@
2020
from .utils import try_dbfield, get_all_model_fields, get_model_field, resolve_field
2121

2222

23-
def filters_for_model(model, fields=None, exclude=None, filter_for_field=None,
24-
filter_for_reverse_field=None):
25-
field_dict = OrderedDict()
26-
27-
# Setting exclude with no fields implies all other fields.
28-
if exclude is not None and fields is None:
29-
fields = ALL_FIELDS
30-
31-
# All implies all db fields associated with a filter_class.
32-
if fields == ALL_FIELDS:
33-
fields = get_all_model_fields(model)
34-
35-
# Loop through the list of fields.
36-
for f in fields:
37-
# Skip the field if excluded.
38-
if exclude is not None and f in exclude:
39-
continue
40-
field = get_model_field(model, f)
41-
# Do nothing if the field doesn't exist.
42-
if field is None:
43-
field_dict[f] = None
44-
continue
45-
if isinstance(field, ForeignObjectRel):
46-
filter_ = filter_for_reverse_field(field, f)
47-
if filter_:
48-
field_dict[f] = filter_
49-
# If fields is a dictionary, it must contain lists.
50-
elif isinstance(fields, dict):
51-
# Create a filter for each lookup type.
52-
for lookup_expr in fields[f]:
53-
filter_ = filter_for_field(field, f, lookup_expr)
54-
55-
if filter_:
56-
filter_name = LOOKUP_SEP.join([f, lookup_expr])
57-
58-
# Don't add "exact" to filter names
59-
_exact = LOOKUP_SEP + 'exact'
60-
if filter_name.endswith(_exact):
61-
filter_name = filter_name[:-len(_exact)]
62-
63-
field_dict[filter_name] = filter_
64-
# If fields is a list, it contains strings.
65-
else:
66-
filter_ = filter_for_field(field, f)
67-
if filter_:
68-
field_dict[f] = filter_
69-
return field_dict
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])
30+
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
7037

7138

7239
def get_full_clean_override(together):
@@ -111,34 +78,12 @@ def __init__(self, options=None):
11178

11279
class FilterSetMetaclass(type):
11380
def __new__(cls, name, bases, attrs):
114-
try:
115-
parents = [b for b in bases if issubclass(b, FilterSet)]
116-
except NameError:
117-
# We are defining FilterSet itself here
118-
parents = None
119-
declared_filters = cls.get_declared_filters(bases, attrs)
120-
new_class = super(
121-
FilterSetMetaclass, cls).__new__(cls, name, bases, attrs)
122-
123-
if not parents:
124-
return new_class
125-
126-
opts = new_class._meta = FilterSetOptions(
127-
getattr(new_class, 'Meta', None))
128-
129-
if opts.model and (opts.fields is not None or opts.exclude is not None):
130-
filters = new_class.filters_for_model(opts.model, opts)
131-
filters.update(declared_filters)
132-
else:
133-
filters = declared_filters
81+
attrs['declared_filters'] = cls.get_declared_filters(bases, attrs)
13482

135-
not_defined = next((k for k, v in filters.items() if v is None), False)
136-
if not_defined:
137-
raise TypeError("Meta.fields contains a field that isn't defined "
138-
"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()
13986

140-
new_class.declared_filters = declared_filters
141-
new_class.base_filters = filters
14287
return new_class
14388

14489
@classmethod
@@ -285,12 +230,88 @@ def form(self):
285230
return self._form
286231

287232
@classmethod
288-
def filters_for_model(cls, model, opts):
289-
return filters_for_model(
290-
model, opts.fields, opts.exclude,
291-
cls.filter_for_field,
292-
cls.filter_for_reverse_field
293-
)
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
294315

295316
@classmethod
296317
def filter_for_field(cls, f, name, lookup_expr='exact'):

0 commit comments

Comments
 (0)