Refactor filter generation

This commit is contained in:
Ryan P Kilby 2016-11-06 21:18:14 -05:00
parent 49a1e16932
commit 85ba7c4560
1 changed files with 98 additions and 77 deletions

View File

@ -20,53 +20,20 @@ from .filters import (Filter, CharFilter, BooleanFilter, BaseInFilter, BaseRange
from .utils import try_dbfield, get_all_model_fields, get_model_field, resolve_field
def filters_for_model(model, fields=None, exclude=None, filter_for_field=None,
filter_for_reverse_field=None):
field_dict = OrderedDict()
def get_filter_name(field_name, lookup_expr):
"""
Combine a field name and lookup expression into a usable filter name.
Exact lookups are the implicit default, so "exact" is stripped from the
end of the filter name.
"""
filter_name = LOOKUP_SEP.join([field_name, lookup_expr])
# Setting exclude with no fields implies all other fields.
if exclude is not None and fields is None:
fields = ALL_FIELDS
# This also works with transformed exact lookups, such as 'date__exact'
_exact = LOOKUP_SEP + 'exact'
if filter_name.endswith(_exact):
filter_name = filter_name[:-len(_exact)]
# All implies all db fields associated with a filter_class.
if fields == ALL_FIELDS:
fields = get_all_model_fields(model)
# Loop through the list of fields.
for f in fields:
# Skip the field if excluded.
if exclude is not None and f in exclude:
continue
field = get_model_field(model, f)
# Do nothing if the field doesn't exist.
if field is None:
field_dict[f] = None
continue
if isinstance(field, ForeignObjectRel):
filter_ = filter_for_reverse_field(field, f)
if filter_:
field_dict[f] = filter_
# If fields is a dictionary, it must contain lists.
elif isinstance(fields, dict):
# Create a filter for each lookup type.
for lookup_expr in fields[f]:
filter_ = filter_for_field(field, f, lookup_expr)
if filter_:
filter_name = LOOKUP_SEP.join([f, lookup_expr])
# Don't add "exact" to filter names
_exact = LOOKUP_SEP + 'exact'
if filter_name.endswith(_exact):
filter_name = filter_name[:-len(_exact)]
field_dict[filter_name] = filter_
# If fields is a list, it contains strings.
else:
filter_ = filter_for_field(field, f)
if filter_:
field_dict[f] = filter_
return field_dict
return filter_name
def get_full_clean_override(together):
@ -111,34 +78,12 @@ class FilterSetOptions(object):
class FilterSetMetaclass(type):
def __new__(cls, name, bases, attrs):
try:
parents = [b for b in bases if issubclass(b, FilterSet)]
except NameError:
# We are defining FilterSet itself here
parents = None
declared_filters = cls.get_declared_filters(bases, attrs)
new_class = super(
FilterSetMetaclass, cls).__new__(cls, name, bases, attrs)
attrs['declared_filters'] = cls.get_declared_filters(bases, attrs)
if not parents:
return new_class
new_class = super(FilterSetMetaclass, cls).__new__(cls, name, bases, attrs)
new_class._meta = FilterSetOptions(getattr(new_class, 'Meta', None))
new_class.base_filters = new_class.get_filters()
opts = new_class._meta = FilterSetOptions(
getattr(new_class, 'Meta', None))
if opts.model and (opts.fields is not None or opts.exclude is not None):
filters = new_class.filters_for_model(opts.model, opts)
filters.update(declared_filters)
else:
filters = declared_filters
not_defined = next((k for k, v in filters.items() if v is None), False)
if not_defined:
raise TypeError("Meta.fields contains a field that isn't defined "
"on this FilterSet: {}".format(not_defined))
new_class.declared_filters = declared_filters
new_class.base_filters = filters
return new_class
@classmethod
@ -285,12 +230,88 @@ class BaseFilterSet(object):
return self._form
@classmethod
def filters_for_model(cls, model, opts):
return filters_for_model(
model, opts.fields, opts.exclude,
cls.filter_for_field,
cls.filter_for_reverse_field
)
def get_fields(cls):
"""
Resolve the 'fields' argument that should be used for generating filters on the
filterset. This is 'Meta.fields' sans the fields in 'Meta.exclude'.
"""
model = cls._meta.model
fields = cls._meta.fields
exclude = cls._meta.exclude
assert not (fields is None and exclude is None), \
"Setting 'Meta.model' without either 'Meta.fields' or 'Meta.exclude' " \
"has been deprecated since 0.15.0 and is now disallowed. Add an explicit" \
"'Meta.fields' or 'Meta.exclude' to the %s class." % cls.__name__
# Setting exclude with no fields implies all other fields.
if exclude is not None and fields is None:
fields = ALL_FIELDS
# Resolve ALL_FIELDS into all fields for the filterset's model.
if fields == ALL_FIELDS:
fields = get_all_model_fields(model)
# Remove excluded fields
exclude = exclude or []
if not isinstance(fields, dict):
fields = [(f, ['exact']) for f in fields if f not in exclude]
else:
fields = [(f, lookups) for f, lookups in fields.items() if f not in exclude]
return OrderedDict(fields)
@classmethod
def get_filters(cls):
"""
Get all filters for the filterset. This is the combination of declared and
generated filters.
"""
# No model specified - skip filter generation
if not cls._meta.model:
return cls.declared_filters.copy()
# Determine the filters that should be included on the filterset.
filters = OrderedDict()
fields = cls.get_fields()
undefined = []
for field_name, lookups in fields.items():
field = get_model_field(cls._meta.model, field_name)
# warn if the field doesn't exist.
if field is None:
undefined.append(field_name)
# ForeignObjectRel does not support non-exact lookups
if isinstance(field, ForeignObjectRel):
filters[field_name] = cls.filter_for_reverse_field(field, field_name)
continue
for lookup_expr in lookups:
filter_name = get_filter_name(field_name, lookup_expr)
# If the filter is explicitly declared on the class, skip generation
if filter_name in cls.declared_filters:
filters[filter_name] = cls.declared_filters[filter_name]
continue
if field is not None:
filters[filter_name] = cls.filter_for_field(field, field_name, lookup_expr)
# filter out declared filters
undefined = [f for f in undefined if f not in cls.declared_filters]
if undefined:
raise TypeError(
"'Meta.fields' contains fields that are not defined on this FilterSet: "
"%s" % ', '.join(undefined)
)
# Add in declared filters. This is necessary since we don't enforce adding
# declared filters to the 'Meta.fields' option
filters.update(cls.declared_filters)
return filters
@classmethod
def filter_for_field(cls, f, name, lookup_expr='exact'):