Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/django_filters/utils.py: 56%
143 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
1import warnings
2from collections import OrderedDict
4from django.conf import settings
5from django.core.exceptions import FieldDoesNotExist, FieldError
6from django.db import models
7from django.db.models.constants import LOOKUP_SEP
8from django.db.models.expressions import Expression
9from django.db.models.fields.related import ForeignObjectRel, RelatedField
10from django.utils import timezone
11from django.utils.encoding import force_str
12from django.utils.text import capfirst
13from django.utils.translation import gettext as _
15from .exceptions import FieldLookupError
18def deprecate(msg, level_modifier=0):
19 warnings.warn(msg, MigrationNotice, stacklevel=3 + level_modifier)
22class MigrationNotice(DeprecationWarning):
23 url = "https://django-filter.readthedocs.io/en/main/guide/migration.html"
25 def __init__(self, message):
26 super().__init__("%s See: %s" % (message, self.url))
29class RenameAttributesBase(type):
30 """
31 Handles the deprecation paths when renaming an attribute.
33 It does the following:
34 - Defines accessors that redirect to the renamed attributes.
35 - Complain whenever an old attribute is accessed.
37 This is conceptually based on `django.utils.deprecation.RenameMethodsBase`.
38 """
40 renamed_attributes = ()
42 def __new__(metacls, name, bases, attrs):
43 # remove old attributes before creating class
44 old_names = [r[0] for r in metacls.renamed_attributes]
45 old_names = [name for name in old_names if name in attrs]
46 old_attrs = {name: attrs.pop(name) for name in old_names}
48 # get a handle to any accessors defined on the class
49 cls_getattr = attrs.pop("__getattr__", None)
50 cls_setattr = attrs.pop("__setattr__", None)
52 new_class = super().__new__(metacls, name, bases, attrs)
54 def __getattr__(self, name):
55 name = type(self).get_name(name)
56 if cls_getattr is not None:
57 return cls_getattr(self, name)
58 elif hasattr(super(new_class, self), "__getattr__"):
59 return super(new_class, self).__getattr__(name)
60 return self.__getattribute__(name)
62 def __setattr__(self, name, value):
63 name = type(self).get_name(name)
64 if cls_setattr is not None:
65 return cls_setattr(self, name, value)
66 return super(new_class, self).__setattr__(name, value)
68 new_class.__getattr__ = __getattr__
69 new_class.__setattr__ = __setattr__
71 # set renamed attributes
72 for name, value in old_attrs.items():
73 setattr(new_class, name, value)
75 return new_class
77 def get_name(metacls, name):
78 """
79 Get the real attribute name. If the attribute has been renamed,
80 the new name will be returned and a deprecation warning issued.
81 """
82 for renamed_attribute in metacls.renamed_attributes:
83 old_name, new_name, deprecation_warning = renamed_attribute
85 if old_name == name:
86 warnings.warn(
87 "`%s.%s` attribute should be renamed `%s`."
88 % (metacls.__name__, old_name, new_name),
89 deprecation_warning,
90 3,
91 )
92 return new_name
94 return name
96 def __getattr__(metacls, name):
97 return super().__getattribute__(metacls.get_name(name))
99 def __setattr__(metacls, name, value):
100 return super().__setattr__(metacls.get_name(name), value)
103def try_dbfield(fn, field_class):
104 """
105 Try ``fn`` with the DB ``field_class`` by walking its
106 MRO until a result is found.
108 ex::
109 _try_dbfield(field_dict.get, models.CharField)
111 """
112 # walk the mro, as field_class could be a derived model field.
113 for cls in field_class.mro(): 113 ↛ exitline 113 didn't return from function 'try_dbfield', because the loop on line 113 didn't complete
114 # skip if cls is models.Field
115 if cls is models.Field: 115 ↛ 116line 115 didn't jump to line 116, because the condition on line 115 was never true
116 continue
118 data = fn(cls)
119 if data:
120 return data
123def get_all_model_fields(model):
124 opts = model._meta
126 return [
127 f.name
128 for f in sorted(opts.fields + opts.many_to_many)
129 if not isinstance(f, models.AutoField)
130 and not (getattr(f.remote_field, "parent_link", False))
131 ]
134def get_model_field(model, field_name):
135 """
136 Get a ``model`` field, traversing relationships
137 in the ``field_name``.
139 ex::
141 f = get_model_field(Book, 'author__first_name')
143 """
144 fields = get_field_parts(model, field_name)
145 return fields[-1] if fields else None
148def get_field_parts(model, field_name):
149 """
150 Get the field parts that represent the traversable relationships from the
151 base ``model`` to the final field, described by ``field_name``.
153 ex::
155 >>> parts = get_field_parts(Book, 'author__first_name')
156 >>> [p.verbose_name for p in parts]
157 ['author', 'first name']
159 """
160 parts = field_name.split(LOOKUP_SEP)
161 opts = model._meta
162 fields = []
164 # walk relationships
165 for name in parts:
166 try:
167 field = opts.get_field(name)
168 except FieldDoesNotExist:
169 return None
171 fields.append(field)
172 try:
173 if isinstance(field, RelatedField):
174 opts = field.remote_field.model._meta
175 elif isinstance(field, ForeignObjectRel):
176 opts = field.related_model._meta
177 except AttributeError:
178 # Lazy relationships are not resolved until registry is populated.
179 raise RuntimeError(
180 "Unable to resolve relationship `%s` for `%s`. Django is most "
181 "likely not initialized, and its apps registry not populated. "
182 "Ensure Django has finished setup before loading `FilterSet`s."
183 % (field_name, model._meta.label)
184 )
186 return fields
189def resolve_field(model_field, lookup_expr):
190 """
191 Resolves a ``lookup_expr`` into its final output field, given
192 the initial ``model_field``. The lookup expression should only contain
193 transforms and lookups, not intermediary model field parts.
195 Note:
196 This method is based on django.db.models.sql.query.Query.build_lookup
198 For more info on the lookup API:
199 https://docs.djangoproject.com/en/stable/ref/models/lookups/
201 """
202 query = model_field.model._default_manager.all().query
203 lhs = Expression(model_field)
204 lookups = lookup_expr.split(LOOKUP_SEP)
206 assert len(lookups) > 0
208 try:
209 while lookups: 209 ↛ exitline 209 didn't return from function 'resolve_field', because the condition on line 209 was never false
210 name = lookups[0]
211 args = (lhs, name)
212 # If there is just one part left, try first get_lookup() so
213 # that if the lhs supports both transform and lookup for the
214 # name, then lookup will be picked.
215 if len(lookups) == 1: 215 ↛ 224line 215 didn't jump to line 224, because the condition on line 215 was never false
216 final_lookup = lhs.get_lookup(name)
217 if not final_lookup: 217 ↛ 221line 217 didn't jump to line 221, because the condition on line 217 was never true
218 # We didn't find a lookup. We are going to interpret
219 # the name as transform, and do an Exact lookup against
220 # it.
221 lhs = query.try_transform(*args)
222 final_lookup = lhs.get_lookup("exact")
223 return lhs.output_field, final_lookup.lookup_name
224 lhs = query.try_transform(*args)
225 lookups = lookups[1:]
226 except FieldError as e:
227 raise FieldLookupError(model_field, lookup_expr) from e
230def handle_timezone(value, is_dst=None):
231 if settings.USE_TZ and timezone.is_naive(value):
232 return timezone.make_aware(value, timezone.get_current_timezone(), is_dst)
233 elif not settings.USE_TZ and timezone.is_aware(value):
234 return timezone.make_naive(value, timezone.utc)
235 return value
238def verbose_field_name(model, field_name):
239 """
240 Get the verbose name for a given ``field_name``. The ``field_name``
241 will be traversed across relationships. Returns '[invalid name]' for
242 any field name that cannot be traversed.
244 ex::
246 >>> verbose_field_name(Article, 'author__name')
247 'author name'
249 """
250 if field_name is None: 250 ↛ 251line 250 didn't jump to line 251, because the condition on line 250 was never true
251 return "[invalid name]"
253 parts = get_field_parts(model, field_name)
254 if not parts:
255 return "[invalid name]"
257 names = []
258 for part in parts:
259 if isinstance(part, ForeignObjectRel): 259 ↛ 260line 259 didn't jump to line 260, because the condition on line 259 was never true
260 if part.related_name:
261 names.append(part.related_name.replace("_", " "))
262 else:
263 return "[invalid name]"
264 else:
265 names.append(force_str(part.verbose_name))
267 return " ".join(names)
270def verbose_lookup_expr(lookup_expr):
271 """
272 Get a verbose, more humanized expression for a given ``lookup_expr``.
273 Each part in the expression is looked up in the ``FILTERS_VERBOSE_LOOKUPS``
274 dictionary. Missing keys will simply default to itself.
276 ex::
278 >>> verbose_lookup_expr('year__lt')
279 'year is less than'
281 # with `FILTERS_VERBOSE_LOOKUPS = {}`
282 >>> verbose_lookup_expr('year__lt')
283 'year lt'
285 """
286 from .conf import settings as app_settings
288 VERBOSE_LOOKUPS = app_settings.VERBOSE_LOOKUPS or {}
289 lookups = [
290 force_str(VERBOSE_LOOKUPS.get(lookup, _(lookup)))
291 for lookup in lookup_expr.split(LOOKUP_SEP)
292 ]
294 return " ".join(lookups)
297def label_for_filter(model, field_name, lookup_expr, exclude=False):
298 """
299 Create a generic label suitable for a filter.
301 ex::
303 >>> label_for_filter(Article, 'author__name', 'in')
304 'auther name is in'
306 """
307 name = verbose_field_name(model, field_name)
308 verbose_expression = [_("exclude"), name] if exclude else [name]
310 # iterable lookups indicate a LookupTypeField, which should not be verbose
311 if isinstance(lookup_expr, str): 311 ↛ 314line 311 didn't jump to line 314, because the condition on line 311 was never false
312 verbose_expression += [verbose_lookup_expr(lookup_expr)]
314 verbose_expression = [force_str(part) for part in verbose_expression if part]
315 verbose_expression = capfirst(" ".join(verbose_expression))
317 return verbose_expression
320def translate_validation(error_dict):
321 """
322 Translate a Django ErrorDict into its DRF ValidationError.
323 """
324 # it's necessary to lazily import the exception, as it can otherwise create
325 # an import loop when importing django_filters inside the project settings.
326 from rest_framework.exceptions import ErrorDetail, ValidationError
328 exc = OrderedDict(
329 (
330 key,
331 [
332 ErrorDetail(e.message % (e.params or ()), code=e.code)
333 for e in error_list
334 ],
335 )
336 for key, error_list in error_dict.as_data().items()
337 )
339 return ValidationError(exc)