230 lines
6.7 KiB
Python
230 lines
6.7 KiB
Python
import warnings
|
|
|
|
from django.conf import settings
|
|
from django.core.exceptions import 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 import FieldDoesNotExist
|
|
from django.db.models.fields.related import RelatedField, ForeignObjectRel
|
|
from django.utils import six, timezone
|
|
from django.utils.encoding import force_text
|
|
from django.utils.translation import ugettext as _
|
|
|
|
try:
|
|
from django.forms.utils import pretty_name
|
|
except ImportError: # Django 1.8
|
|
from django.forms.forms import pretty_name
|
|
|
|
from .compat import remote_field, remote_model
|
|
from .exceptions import FieldLookupError
|
|
|
|
|
|
def deprecate(msg, level_modifier=0):
|
|
warnings.warn(
|
|
"%s See: https://django-filter.readthedocs.io/en/latest/migration.html" % msg,
|
|
DeprecationWarning, stacklevel=3 + level_modifier)
|
|
|
|
|
|
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(remote_field(f), '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)
|
|
if isinstance(field, RelatedField):
|
|
opts = remote_model(field)._meta
|
|
elif isinstance(field, ForeignObjectRel):
|
|
opts = field.related_model._meta
|
|
|
|
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/1.9/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]
|
|
# 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(lhs, name, lookups)
|
|
final_lookup = lhs.get_lookup('exact')
|
|
return lhs.output_field, final_lookup.lookup_name
|
|
lhs = query.try_transform(lhs, name, lookups)
|
|
lookups = lookups[1:]
|
|
except FieldError as e:
|
|
six.raise_from(FieldLookupError(model_field, lookup_expr), e)
|
|
|
|
|
|
def handle_timezone(value):
|
|
if settings.USE_TZ and timezone.is_naive(value):
|
|
return timezone.make_aware(value, timezone.get_default_timezone())
|
|
elif not settings.USE_TZ and timezone.is_aware(value):
|
|
return timezone.make_naive(value, 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):
|
|
names.append(force_text(part.related_name))
|
|
else:
|
|
names.append(force_text(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_text(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, six.string_types):
|
|
verbose_expression += [verbose_lookup_expr(lookup_expr)]
|
|
|
|
verbose_expression = [force_text(part) for part in verbose_expression if part]
|
|
verbose_expression = pretty_name(' '.join(verbose_expression))
|
|
|
|
return verbose_expression
|