You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

351 lines
11 KiB

4 months ago
import datetime
import warnings
from collections import OrderedDict
import django
from django.conf import settings
from django.core.exceptions import FieldDoesNotExist, FieldError
from django.db import models
from django.db.models.constants import LOOKUP_SEP
from django.db.models.expressions import Expression
from django.db.models.fields.related import ForeignObjectRel, RelatedField
from django.utils import timezone
from django.utils.encoding import force_str
from django.utils.text import capfirst
from django.utils.translation import gettext as _
from .exceptions import FieldLookupError
def deprecate(msg, level_modifier=0):
warnings.warn(msg, MigrationNotice, stacklevel=3 + level_modifier)
class MigrationNotice(DeprecationWarning):
url = "https://django-filter.readthedocs.io/en/main/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):
"""
Try ``fn`` with the DB ``field_class`` by walking its
MRO until a result is found.
ex::
_try_dbfield(field_dict.get, models.CharField)
"""
# walk the mro, as field_class could be a derived model field.
for cls in field_class.mro():
# skip if cls is models.Field
if cls is models.Field:
continue
data = fn(cls)
if data:
return data
def get_all_model_fields(model):
opts = model._meta
return [
f.name
for f in sorted(opts.fields + opts.many_to_many)
if not isinstance(f, models.AutoField)
and not (getattr(f.remote_field, "parent_link", False))
]
def get_model_field(model, field_name):
"""
Get a ``model`` field, traversing relationships
in the ``field_name``.
ex::
f = get_model_field(Book, 'author__first_name')
"""
fields = get_field_parts(model, field_name)
return fields[-1] if fields else None
def get_field_parts(model, field_name):
"""
Get the field parts that represent the traversable relationships from the
base ``model`` to the final field, described by ``field_name``.
ex::
>>> parts = get_field_parts(Book, 'author__first_name')
>>> [p.verbose_name for p in parts]
['author', 'first name']
"""
parts = field_name.split(LOOKUP_SEP)
opts = model._meta
fields = []
# walk relationships
for name in parts:
try:
field = opts.get_field(name)
except FieldDoesNotExist:
return None
fields.append(field)
try:
if isinstance(field, RelatedField):
opts = field.remote_field.model._meta
elif isinstance(field, ForeignObjectRel):
opts = field.related_model._meta
except AttributeError:
# Lazy relationships are not resolved until registry is populated.
raise RuntimeError(
"Unable to resolve relationship `%s` for `%s`. Django is most "
"likely not initialized, and its apps registry not populated. "
"Ensure Django has finished setup before loading `FilterSet`s."
% (field_name, model._meta.label)
)
return fields
def resolve_field(model_field, lookup_expr):
"""
Resolves a ``lookup_expr`` into its final output field, given
the initial ``model_field``. The lookup expression should only contain
transforms and lookups, not intermediary model field parts.
Note:
This method is based on django.db.models.sql.query.Query.build_lookup
For more info on the lookup API:
https://docs.djangoproject.com/en/stable/ref/models/lookups/
"""
query = model_field.model._default_manager.all().query
lhs = Expression(model_field)
lookups = lookup_expr.split(LOOKUP_SEP)
assert len(lookups) > 0
try:
while lookups:
name = lookups[0]
args = (lhs, name)
# If there is just one part left, try first get_lookup() so
# that if the lhs supports both transform and lookup for the
# name, then lookup will be picked.
if len(lookups) == 1:
final_lookup = lhs.get_lookup(name)
if not final_lookup:
# We didn't find a lookup. We are going to interpret
# the name as transform, and do an Exact lookup against
# it.
lhs = query.try_transform(*args)
final_lookup = lhs.get_lookup("exact")
return lhs.output_field, final_lookup.lookup_name
lhs = query.try_transform(*args)
lookups = lookups[1:]
except FieldError as e:
raise FieldLookupError(model_field, lookup_expr) from e
def handle_timezone(value, is_dst=None):
if settings.USE_TZ and timezone.is_naive(value):
# On pre-5.x versions, the default is to use zoneinfo, but pytz
# is still available under USE_DEPRECATED_PYTZ, and is_dst is
# meaningful there. Under those versions we should only use is_dst
# if USE_DEPRECATED_PYTZ is present and True; otherwise, we will cause
# deprecation warnings, and we should not. See #1580.
#
# This can be removed once 4.2 is no longer supported upstream.
if django.VERSION < (5, 0) and settings.USE_DEPRECATED_PYTZ:
return timezone.make_aware(value, timezone.get_current_timezone(), is_dst)
return timezone.make_aware(value, timezone.get_current_timezone())
elif not settings.USE_TZ and timezone.is_aware(value):
return timezone.make_naive(value, datetime.timezone.utc)
return value
def verbose_field_name(model, field_name):
"""
Get the verbose name for a given ``field_name``. The ``field_name``
will be traversed across relationships. Returns '[invalid name]' for
any field name that cannot be traversed.
ex::
>>> verbose_field_name(Article, 'author__name')
'author name'
"""
if field_name is None:
return "[invalid name]"
parts = get_field_parts(model, field_name)
if not parts:
return "[invalid name]"
names = []
for part in parts:
if isinstance(part, ForeignObjectRel):
if part.related_name:
names.append(part.related_name.replace("_", " "))
else:
return "[invalid name]"
else:
names.append(force_str(part.verbose_name))
return " ".join(names)
def verbose_lookup_expr(lookup_expr):
"""
Get a verbose, more humanized expression for a given ``lookup_expr``.
Each part in the expression is looked up in the ``FILTERS_VERBOSE_LOOKUPS``
dictionary. Missing keys will simply default to itself.
ex::
>>> verbose_lookup_expr('year__lt')
'year is less than'
# with `FILTERS_VERBOSE_LOOKUPS = {}`
>>> verbose_lookup_expr('year__lt')
'year lt'
"""
from .conf import settings as app_settings
VERBOSE_LOOKUPS = app_settings.VERBOSE_LOOKUPS or {}
lookups = [
force_str(VERBOSE_LOOKUPS.get(lookup, _(lookup)))
for lookup in lookup_expr.split(LOOKUP_SEP)
]
return " ".join(lookups)
def label_for_filter(model, field_name, lookup_expr, exclude=False):
"""
Create a generic label suitable for a filter.
ex::
>>> label_for_filter(Article, 'author__name', 'in')
'auther name is in'
"""
name = verbose_field_name(model, field_name)
verbose_expression = [_("exclude"), name] if exclude else [name]
# iterable lookups indicate a LookupTypeField, which should not be verbose
if isinstance(lookup_expr, str):
verbose_expression += [verbose_lookup_expr(lookup_expr)]
verbose_expression = [force_str(part) for part in verbose_expression if part]
verbose_expression = capfirst(" ".join(verbose_expression))
return verbose_expression
def translate_validation(error_dict):
"""
Translate a Django ErrorDict into its DRF ValidationError.
"""
# it's necessary to lazily import the exception, as it can otherwise create
# an import loop when importing django_filters inside the project settings.
from rest_framework.exceptions import ErrorDetail, ValidationError
exc = OrderedDict(
(
key,
[
ErrorDetail(e.message % (e.params or ()), code=e.code)
for e in error_list
],
)
for key, error_list in error_dict.as_data().items()
)
return ValidationError(exc)

Powered by TurnKey Linux.