Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve view-related attribute name consistency #867

Merged
merged 6 commits into from
Jul 13, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 52 additions & 27 deletions django_filters/rest_framework/backends.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
import warnings

from django.template import loader
from django.utils.deprecation import RenameMethodsBase

from . import filters, filterset
from .. import compat, utils


class DjangoFilterBackend(object):
default_filter_set = filterset.FilterSet
# TODO: remove metaclass in 2.1
class RenameAttributes(utils.RenameAttributesBase, RenameMethodsBase):
renamed_attributes = (
('default_filter_set', 'filterset_base', utils.MigrationNotice),
)
renamed_methods = (
('get_filter_class', 'get_filterset_class', utils.MigrationNotice),
)


class DjangoFilterBackend(metaclass=RenameAttributes):
filterset_base = filterset.FilterSet
raise_exception = True

@property
Expand All @@ -16,55 +27,69 @@ def template(self):
return 'django_filters/rest_framework/crispy_form.html'
return 'django_filters/rest_framework/form.html'

def get_filter_class(self, view, queryset=None):
def get_filterset_class(self, view, queryset=None):
"""
Return the django-filters `FilterSet` used to filter the queryset.
Return the `FilterSet` class used to filter the queryset.
"""
filter_class = getattr(view, 'filter_class', None)
filter_fields = getattr(view, 'filter_fields', None)

if filter_class:
filter_model = filter_class._meta.model
filterset_class = getattr(view, 'filterset_class', None)
filterset_fields = getattr(view, 'filterset_fields', None)

# TODO: remove assertion in 2.1
if filterset_class is None and hasattr(view, 'filter_class'):
utils.deprecate(
"`%s.filter_class` attribute should be renamed `filterset_class`."
% view.__class__.__name__)
filterset_class = getattr(view, 'filter_class', None)

# TODO: remove assertion in 2.1
if filterset_fields is None and hasattr(view, 'filter_fields'):
utils.deprecate(
"`%s.filter_fields` attribute should be renamed `filterset_fields`."
% view.__class__.__name__)
filterset_fields = getattr(view, 'filter_fields', None)

if filterset_class:
filterset_model = filterset_class._meta.model

# FilterSets do not need to specify a Meta class
if filter_model and queryset is not None:
assert issubclass(queryset.model, filter_model), \
if filterset_model and queryset is not None:
assert issubclass(queryset.model, filterset_model), \
'FilterSet model %s does not match queryset model %s' % \
(filter_model, queryset.model)
(filterset_model, queryset.model)

return filter_class
return filterset_class

if filter_fields and queryset is not None:
MetaBase = getattr(self.default_filter_set, 'Meta', object)
if filterset_fields and queryset is not None:
MetaBase = getattr(self.filterset_base, 'Meta', object)

class AutoFilterSet(self.default_filter_set):
class AutoFilterSet(self.filterset_base):
class Meta(MetaBase):
model = queryset.model
fields = filter_fields
fields = filterset_fields

return AutoFilterSet

return None

def filter_queryset(self, request, queryset, view):
filter_class = self.get_filter_class(view, queryset)
filterset_class = self.get_filterset_class(view, queryset)

if filter_class:
filterset = filter_class(request.query_params, queryset=queryset, request=request)
if filterset_class:
filterset = filterset_class(request.query_params, queryset=queryset, request=request)
if not filterset.is_valid() and self.raise_exception:
raise utils.translate_validation(filterset.errors)
return filterset.qs
return queryset

def to_html(self, request, queryset, view):
filter_class = self.get_filter_class(view, queryset)
if not filter_class:
filterset_class = self.get_filterset_class(view, queryset)
if not filterset_class:
return None
filter_instance = filter_class(request.query_params, queryset=queryset, request=request)
filterset = filterset_class(request.query_params, queryset=queryset, request=request)

template = loader.get_template(self.template)
context = {
'filter': filter_instance
'filter': filterset
}

return template.render(context, request)
Expand Down Expand Up @@ -93,13 +118,13 @@ def get_schema_fields(self, view):
"{} is not compatible with schema generation".format(view.__class__)
)

filter_class = self.get_filter_class(view, queryset)
filterset_class = self.get_filterset_class(view, queryset)

return [] if not filter_class else [
return [] if not filterset_class else [
compat.coreapi.Field(
name=field_name,
required=field.extra['required'],
location='query',
schema=self.get_coreschema_field(field)
) for field_name, field in filter_class.base_filters.items()
) for field_name, field in filterset_class.base_filters.items()
]
81 changes: 78 additions & 3 deletions django_filters/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,84 @@


def deprecate(msg, level_modifier=0):
warnings.warn(
"%s See: https://django-filter.readthedocs.io/en/master/guide/migration.html" % msg,
DeprecationWarning, stacklevel=3 + level_modifier)
warnings.warn(msg, MigrationNotice, stacklevel=3 + level_modifier)


class MigrationNotice(DeprecationWarning):
url = 'https://django-filter.readthedocs.io/en/master/guide/migration.html'

def __init__(self, message):
super().__init__('%s See: %s' % (message, self.url))


class RenameAttributesBase(type):
"""
Handles the deprecation paths when renaming an attribute.

It does the following:
- Defines accessors that redirect to the renamed attributes.
- Complain whenever an old attribute is accessed.

This is conceptually based on `django.utils.deprecation.RenameMethodsBase`.
"""
renamed_attributes = ()

def __new__(metacls, name, bases, attrs):
# remove old attributes before creating class
old_names = [r[0] for r in metacls.renamed_attributes]
old_names = [name for name in old_names if name in attrs]
old_attrs = {name: attrs.pop(name) for name in old_names}

# get a handle to any accessors defined on the class
cls_getattr = attrs.pop('__getattr__', None)
cls_setattr = attrs.pop('__setattr__', None)

new_class = super().__new__(metacls, name, bases, attrs)

def __getattr__(self, name):
name = type(self).get_name(name)
if cls_getattr is not None:
return cls_getattr(self, name)
elif hasattr(super(new_class, self), '__getattr__'):
return super(new_class, self).__getattr__(name)
return self.__getattribute__(name)

def __setattr__(self, name, value):
name = type(self).get_name(name)
if cls_setattr is not None:
return cls_setattr(self, name, value)
return super(new_class, self).__setattr__(name, value)

new_class.__getattr__ = __getattr__
new_class.__setattr__ = __setattr__

# set renamed attributes
for name, value in old_attrs.items():
setattr(new_class, name, value)

return new_class

def get_name(metacls, name):
"""
Get the real attribute name. If the attribute has been renamed,
the new name will be returned and a deprecation warning issued.
"""
for renamed_attribute in metacls.renamed_attributes:
old_name, new_name, deprecation_warning = renamed_attribute

if old_name == name:
warnings.warn("`%s.%s` attribute should be renamed `%s`."
% (metacls.__name__, old_name, new_name),
deprecation_warning, 3)
return new_name

return name

def __getattr__(metacls, name):
return super().__getattribute__(metacls.get_name(name))

def __setattr__(metacls, name, value):
return super().__setattr__(metacls.get_name(name), value)


def try_dbfield(fn, field_class):
Expand Down
14 changes: 11 additions & 3 deletions django_filters/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,22 @@

from .constants import ALL_FIELDS
from .filterset import filterset_factory
from .utils import MigrationNotice, RenameAttributesBase


class FilterMixin(object):
# TODO: remove metaclass in 2.1
class FilterMixinRenames(RenameAttributesBase):
renamed_attributes = (
('filter_fields', 'filterset_fields', MigrationNotice),
)


class FilterMixin(metaclass=FilterMixinRenames):
"""
A mixin that provides a way to show and handle a FilterSet in a request.
"""
filterset_class = None
filter_fields = ALL_FIELDS
filterset_fields = ALL_FIELDS
strict = True

def get_filterset_class(self):
Expand All @@ -24,7 +32,7 @@ def get_filterset_class(self):
if self.filterset_class:
return self.filterset_class
elif self.model:
return filterset_factory(model=self.model, fields=self.filter_fields)
return filterset_factory(model=self.model, fields=self.filterset_fields)
else:
msg = "'%s' must define 'filterset_class' or 'model'"
raise ImproperlyConfigured(msg % self.__class__.__name__)
Expand Down
14 changes: 14 additions & 0 deletions docs/guide/migration.txt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,20 @@ The ``Filter.lookup_expr`` argument no longer accepts ``None`` or a list of
expressions. Use the :ref:`LookupChoiceFilter <lookup-choice-filter>` instead.


View attributes renamed (`#867`__)
----------------------------------
__ https://github.com/carltongibson/django-filter/pull/867

Several view-related attributes have been renamed to improve consistency with
other parts of the library. The following classes are affected:

* DRF ``ViewSet.filter_class`` => ``filterset_class``
* DRF ``ViewSet.filter_fields`` => ``filterset_fields``
* ``DjangoFilterBackend.default_filter_set`` => ``filterset_base``
* ``DjangoFilterBackend.get_filter_class()`` => ``get_filterset_class()``
* ``FilterMixin.filter_fields`` => ``filterset_fields``


FilterSet ``Meta.together`` option removed (`#791`__)
-----------------------------------------------------
__ https://github.com/carltongibson/django-filter/pull/791
Expand Down
Loading