diff --git a/django_filters/filterset.py b/django_filters/filterset.py index 1b8a2b9..5711772 100644 --- a/django_filters/filterset.py +++ b/django_filters/filterset.py @@ -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'):