Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/django/contrib/admin/utils.py: 17%
317 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 datetime
2import decimal
3import json
4from collections import defaultdict
6from django.core.exceptions import FieldDoesNotExist
7from django.db import models, router
8from django.db.models.constants import LOOKUP_SEP
9from django.db.models.deletion import Collector
10from django.forms.utils import pretty_name
11from django.urls import NoReverseMatch, reverse
12from django.utils import formats, timezone
13from django.utils.html import format_html
14from django.utils.regex_helper import _lazy_re_compile
15from django.utils.text import capfirst
16from django.utils.translation import ngettext
17from django.utils.translation import override as translation_override
19QUOTE_MAP = {i: "_%02X" % i for i in b'":/_#?;@&=+$,"[]<>%\n\\'}
20UNQUOTE_MAP = {v: chr(k) for k, v in QUOTE_MAP.items()}
21UNQUOTE_RE = _lazy_re_compile("_(?:%s)" % "|".join([x[1:] for x in UNQUOTE_MAP]))
24class FieldIsAForeignKeyColumnName(Exception):
25 """A field is a foreign key attname, i.e. <FK>_id."""
27 pass
30def lookup_spawns_duplicates(opts, lookup_path):
31 """
32 Return True if the given lookup path spawns duplicates.
33 """
34 lookup_fields = lookup_path.split(LOOKUP_SEP)
35 # Go through the fields (following all relations) and look for an m2m.
36 for field_name in lookup_fields:
37 if field_name == "pk":
38 field_name = opts.pk.name
39 try:
40 field = opts.get_field(field_name)
41 except FieldDoesNotExist:
42 # Ignore query lookups.
43 continue
44 else:
45 if hasattr(field, "get_path_info"):
46 # This field is a relation; update opts to follow the relation.
47 path_info = field.get_path_info()
48 opts = path_info[-1].to_opts
49 if any(path.m2m for path in path_info):
50 # This field is a m2m relation so duplicates must be
51 # handled.
52 return True
53 return False
56def prepare_lookup_value(key, value):
57 """
58 Return a lookup value prepared to be used in queryset filtering.
59 """
60 # if key ends with __in, split parameter into separate values
61 if key.endswith("__in"):
62 value = value.split(",")
63 # if key ends with __isnull, special case '' and the string literals 'false' and '0'
64 elif key.endswith("__isnull"):
65 value = value.lower() not in ("", "false", "0")
66 return value
69def quote(s):
70 """
71 Ensure that primary key values do not confuse the admin URLs by escaping
72 any '/', '_' and ':' and similarly problematic characters.
73 Similar to urllib.parse.quote(), except that the quoting is slightly
74 different so that it doesn't get automatically unquoted by the web browser.
75 """
76 return s.translate(QUOTE_MAP) if isinstance(s, str) else s
79def unquote(s):
80 """Undo the effects of quote()."""
81 return UNQUOTE_RE.sub(lambda m: UNQUOTE_MAP[m[0]], s)
84def flatten(fields):
85 """
86 Return a list which is a single level of flattening of the original list.
87 """
88 flat = []
89 for field in fields:
90 if isinstance(field, (list, tuple)): 90 ↛ 91line 90 didn't jump to line 91, because the condition on line 90 was never true
91 flat.extend(field)
92 else:
93 flat.append(field)
94 return flat
97def flatten_fieldsets(fieldsets):
98 """Return a list of field names from an admin fieldsets structure."""
99 field_names = []
100 for name, opts in fieldsets:
101 field_names.extend(flatten(opts["fields"]))
102 return field_names
105def get_deleted_objects(objs, request, admin_site):
106 """
107 Find all objects related to ``objs`` that should also be deleted. ``objs``
108 must be a homogeneous iterable of objects (e.g. a QuerySet).
110 Return a nested list of strings suitable for display in the
111 template with the ``unordered_list`` filter.
112 """
113 try:
114 obj = objs[0]
115 except IndexError:
116 return [], {}, set(), []
117 else:
118 using = router.db_for_write(obj._meta.model)
119 collector = NestedObjects(using=using)
120 collector.collect(objs)
121 perms_needed = set()
123 def format_callback(obj):
124 model = obj.__class__
125 has_admin = model in admin_site._registry
126 opts = obj._meta
128 no_edit_link = "%s: %s" % (capfirst(opts.verbose_name), obj)
130 if has_admin:
131 if not admin_site._registry[model].has_delete_permission(request, obj):
132 perms_needed.add(opts.verbose_name)
133 try:
134 admin_url = reverse(
135 "%s:%s_%s_change"
136 % (admin_site.name, opts.app_label, opts.model_name),
137 None,
138 (quote(obj.pk),),
139 )
140 except NoReverseMatch:
141 # Change url doesn't exist -- don't display link to edit
142 return no_edit_link
144 # Display a link to the admin page.
145 return format_html(
146 '{}: <a href="{}">{}</a>', capfirst(opts.verbose_name), admin_url, obj
147 )
148 else:
149 # Don't display link to edit, because it either has no
150 # admin or is edited inline.
151 return no_edit_link
153 to_delete = collector.nested(format_callback)
155 protected = [format_callback(obj) for obj in collector.protected]
156 model_count = {
157 model._meta.verbose_name_plural: len(objs)
158 for model, objs in collector.model_objs.items()
159 }
161 return to_delete, model_count, perms_needed, protected
164class NestedObjects(Collector):
165 def __init__(self, *args, **kwargs):
166 super().__init__(*args, **kwargs)
167 self.edges = {} # {from_instance: [to_instances]}
168 self.protected = set()
169 self.model_objs = defaultdict(set)
171 def add_edge(self, source, target):
172 self.edges.setdefault(source, []).append(target)
174 def collect(self, objs, source=None, source_attr=None, **kwargs):
175 for obj in objs:
176 if source_attr and not source_attr.endswith("+"):
177 related_name = source_attr % {
178 "class": source._meta.model_name,
179 "app_label": source._meta.app_label,
180 }
181 self.add_edge(getattr(obj, related_name), obj)
182 else:
183 self.add_edge(None, obj)
184 self.model_objs[obj._meta.model].add(obj)
185 try:
186 return super().collect(objs, source_attr=source_attr, **kwargs)
187 except models.ProtectedError as e:
188 self.protected.update(e.protected_objects)
189 except models.RestrictedError as e:
190 self.protected.update(e.restricted_objects)
192 def related_objects(self, related_model, related_fields, objs):
193 qs = super().related_objects(related_model, related_fields, objs)
194 return qs.select_related(
195 *[related_field.name for related_field in related_fields]
196 )
198 def _nested(self, obj, seen, format_callback):
199 if obj in seen:
200 return []
201 seen.add(obj)
202 children = []
203 for child in self.edges.get(obj, ()):
204 children.extend(self._nested(child, seen, format_callback))
205 if format_callback:
206 ret = [format_callback(obj)]
207 else:
208 ret = [obj]
209 if children:
210 ret.append(children)
211 return ret
213 def nested(self, format_callback=None):
214 """
215 Return the graph as a nested list.
216 """
217 seen = set()
218 roots = []
219 for root in self.edges.get(None, ()):
220 roots.extend(self._nested(root, seen, format_callback))
221 return roots
223 def can_fast_delete(self, *args, **kwargs):
224 """
225 We always want to load the objects into memory so that we can display
226 them to the user in confirm page.
227 """
228 return False
231def model_format_dict(obj):
232 """
233 Return a `dict` with keys 'verbose_name' and 'verbose_name_plural',
234 typically for use with string formatting.
236 `obj` may be a `Model` instance, `Model` subclass, or `QuerySet` instance.
237 """
238 if isinstance(obj, (models.Model, models.base.ModelBase)):
239 opts = obj._meta
240 elif isinstance(obj, models.query.QuerySet):
241 opts = obj.model._meta
242 else:
243 opts = obj
244 return {
245 "verbose_name": opts.verbose_name,
246 "verbose_name_plural": opts.verbose_name_plural,
247 }
250def model_ngettext(obj, n=None):
251 """
252 Return the appropriate `verbose_name` or `verbose_name_plural` value for
253 `obj` depending on the count `n`.
255 `obj` may be a `Model` instance, `Model` subclass, or `QuerySet` instance.
256 If `obj` is a `QuerySet` instance, `n` is optional and the length of the
257 `QuerySet` is used.
258 """
259 if isinstance(obj, models.query.QuerySet):
260 if n is None:
261 n = obj.count()
262 obj = obj.model
263 d = model_format_dict(obj)
264 singular, plural = d["verbose_name"], d["verbose_name_plural"]
265 return ngettext(singular, plural, n or 0)
268def lookup_field(name, obj, model_admin=None):
269 opts = obj._meta
270 try:
271 f = _get_non_gfk_field(opts, name)
272 except (FieldDoesNotExist, FieldIsAForeignKeyColumnName):
273 # For non-field values, the value is either a method, property or
274 # returned via a callable.
275 if callable(name):
276 attr = name
277 value = attr(obj)
278 elif hasattr(model_admin, name) and name != "__str__":
279 attr = getattr(model_admin, name)
280 value = attr(obj)
281 else:
282 attr = getattr(obj, name)
283 if callable(attr):
284 value = attr()
285 else:
286 value = attr
287 f = None
288 else:
289 attr = None
290 value = getattr(obj, name)
291 return f, attr, value
294def _get_non_gfk_field(opts, name):
295 """
296 For historical reasons, the admin app relies on GenericForeignKeys as being
297 "not found" by get_field(). This could likely be cleaned up.
299 Reverse relations should also be excluded as these aren't attributes of the
300 model (rather something like `foo_set`).
301 """
302 field = opts.get_field(name)
303 if (
304 field.is_relation
305 and
306 # Generic foreign keys OR reverse relations
307 ((field.many_to_one and not field.related_model) or field.one_to_many)
308 ):
309 raise FieldDoesNotExist()
311 # Avoid coercing <FK>_id fields to FK
312 if (
313 field.is_relation
314 and not field.many_to_many
315 and hasattr(field, "attname")
316 and field.attname == name
317 ):
318 raise FieldIsAForeignKeyColumnName()
320 return field
323def label_for_field(name, model, model_admin=None, return_attr=False, form=None):
324 """
325 Return a sensible label for a field name. The name can be a callable,
326 property (but not created with @property decorator), or the name of an
327 object's attribute, as well as a model field. If return_attr is True, also
328 return the resolved attribute (which could be a callable). This will be
329 None if (and only if) the name refers to a field.
330 """
331 attr = None
332 try:
333 field = _get_non_gfk_field(model._meta, name)
334 try:
335 label = field.verbose_name
336 except AttributeError:
337 # field is likely a ForeignObjectRel
338 label = field.related_model._meta.verbose_name
339 except FieldDoesNotExist:
340 if name == "__str__":
341 label = str(model._meta.verbose_name)
342 attr = str
343 else:
344 if callable(name):
345 attr = name
346 elif hasattr(model_admin, name):
347 attr = getattr(model_admin, name)
348 elif hasattr(model, name):
349 attr = getattr(model, name)
350 elif form and name in form.fields:
351 attr = form.fields[name]
352 else:
353 message = "Unable to lookup '%s' on %s" % (
354 name,
355 model._meta.object_name,
356 )
357 if model_admin:
358 message += " or %s" % model_admin.__class__.__name__
359 if form:
360 message += " or %s" % form.__class__.__name__
361 raise AttributeError(message)
363 if hasattr(attr, "short_description"):
364 label = attr.short_description
365 elif (
366 isinstance(attr, property)
367 and hasattr(attr, "fget")
368 and hasattr(attr.fget, "short_description")
369 ):
370 label = attr.fget.short_description
371 elif callable(attr):
372 if attr.__name__ == "<lambda>":
373 label = "--"
374 else:
375 label = pretty_name(attr.__name__)
376 else:
377 label = pretty_name(name)
378 except FieldIsAForeignKeyColumnName:
379 label = pretty_name(name)
380 attr = name
382 if return_attr:
383 return (label, attr)
384 else:
385 return label
388def help_text_for_field(name, model):
389 help_text = ""
390 try:
391 field = _get_non_gfk_field(model._meta, name)
392 except (FieldDoesNotExist, FieldIsAForeignKeyColumnName):
393 pass
394 else:
395 if hasattr(field, "help_text"):
396 help_text = field.help_text
397 return help_text
400def display_for_field(value, field, empty_value_display):
401 from django.contrib.admin.templatetags.admin_list import _boolean_icon
403 if getattr(field, "flatchoices", None):
404 return dict(field.flatchoices).get(value, empty_value_display)
405 # BooleanField needs special-case null-handling, so it comes before the
406 # general null test.
407 elif isinstance(field, models.BooleanField):
408 return _boolean_icon(value)
409 elif value is None:
410 return empty_value_display
411 elif isinstance(field, models.DateTimeField):
412 return formats.localize(timezone.template_localtime(value))
413 elif isinstance(field, (models.DateField, models.TimeField)):
414 return formats.localize(value)
415 elif isinstance(field, models.DecimalField):
416 return formats.number_format(value, field.decimal_places)
417 elif isinstance(field, (models.IntegerField, models.FloatField)):
418 return formats.number_format(value)
419 elif isinstance(field, models.FileField) and value:
420 return format_html('<a href="{}">{}</a>', value.url, value)
421 elif isinstance(field, models.JSONField) and value:
422 try:
423 return json.dumps(value, ensure_ascii=False, cls=field.encoder)
424 except TypeError:
425 return display_for_value(value, empty_value_display)
426 else:
427 return display_for_value(value, empty_value_display)
430def display_for_value(value, empty_value_display, boolean=False):
431 from django.contrib.admin.templatetags.admin_list import _boolean_icon
433 if boolean:
434 return _boolean_icon(value)
435 elif value is None:
436 return empty_value_display
437 elif isinstance(value, bool):
438 return str(value)
439 elif isinstance(value, datetime.datetime):
440 return formats.localize(timezone.template_localtime(value))
441 elif isinstance(value, (datetime.date, datetime.time)):
442 return formats.localize(value)
443 elif isinstance(value, (int, decimal.Decimal, float)):
444 return formats.number_format(value)
445 elif isinstance(value, (list, tuple)):
446 return ", ".join(str(v) for v in value)
447 else:
448 return str(value)
451class NotRelationField(Exception):
452 pass
455def get_model_from_relation(field):
456 if hasattr(field, "get_path_info"):
457 return field.get_path_info()[-1].to_opts.model
458 else:
459 raise NotRelationField
462def reverse_field_path(model, path):
463 """Create a reversed field path.
465 E.g. Given (Order, "user__groups"),
466 return (Group, "user__order").
468 Final field must be a related model, not a data field.
469 """
470 reversed_path = []
471 parent = model
472 pieces = path.split(LOOKUP_SEP)
473 for piece in pieces:
474 field = parent._meta.get_field(piece)
475 # skip trailing data field if extant:
476 if len(reversed_path) == len(pieces) - 1: # final iteration
477 try:
478 get_model_from_relation(field)
479 except NotRelationField:
480 break
482 # Field should point to another model
483 if field.is_relation and not (field.auto_created and not field.concrete):
484 related_name = field.related_query_name()
485 parent = field.remote_field.model
486 else:
487 related_name = field.field.name
488 parent = field.related_model
489 reversed_path.insert(0, related_name)
490 return (parent, LOOKUP_SEP.join(reversed_path))
493def get_fields_from_path(model, path):
494 """Return list of Fields given path relative to model.
496 e.g. (ModelX, "user__groups__name") -> [
497 <django.db.models.fields.related.ForeignKey object at 0x...>,
498 <django.db.models.fields.related.ManyToManyField object at 0x...>,
499 <django.db.models.fields.CharField object at 0x...>,
500 ]
501 """
502 pieces = path.split(LOOKUP_SEP)
503 fields = []
504 for piece in pieces:
505 if fields: 505 ↛ 506line 505 didn't jump to line 506, because the condition on line 505 was never true
506 parent = get_model_from_relation(fields[-1])
507 else:
508 parent = model
509 fields.append(parent._meta.get_field(piece))
510 return fields
513def construct_change_message(form, formsets, add):
514 """
515 Construct a JSON structure describing changes from a changed object.
516 Translations are deactivated so that strings are stored untranslated.
517 Translation happens later on LogEntry access.
518 """
519 # Evaluating `form.changed_data` prior to disabling translations is required
520 # to avoid fields affected by localization from being included incorrectly,
521 # e.g. where date formats differ such as MM/DD/YYYY vs DD/MM/YYYY.
522 changed_data = form.changed_data
523 with translation_override(None):
524 # Deactivate translations while fetching verbose_name for form
525 # field labels and using `field_name`, if verbose_name is not provided.
526 # Translations will happen later on LogEntry access.
527 changed_field_labels = _get_changed_field_labels_from_form(form, changed_data)
529 change_message = []
530 if add:
531 change_message.append({"added": {}})
532 elif form.changed_data:
533 change_message.append({"changed": {"fields": changed_field_labels}})
534 if formsets:
535 with translation_override(None):
536 for formset in formsets:
537 for added_object in formset.new_objects:
538 change_message.append(
539 {
540 "added": {
541 "name": str(added_object._meta.verbose_name),
542 "object": str(added_object),
543 }
544 }
545 )
546 for changed_object, changed_fields in formset.changed_objects:
547 change_message.append(
548 {
549 "changed": {
550 "name": str(changed_object._meta.verbose_name),
551 "object": str(changed_object),
552 "fields": _get_changed_field_labels_from_form(
553 formset.forms[0], changed_fields
554 ),
555 }
556 }
557 )
558 for deleted_object in formset.deleted_objects:
559 change_message.append(
560 {
561 "deleted": {
562 "name": str(deleted_object._meta.verbose_name),
563 "object": str(deleted_object),
564 }
565 }
566 )
567 return change_message
570def _get_changed_field_labels_from_form(form, changed_data):
571 changed_field_labels = []
572 for field_name in changed_data:
573 try:
574 verbose_field_name = form.fields[field_name].label or field_name
575 except KeyError:
576 verbose_field_name = field_name
577 changed_field_labels.append(str(verbose_field_name))
578 return changed_field_labels