2015-03-21 15:44:11 +00:00
|
|
|
from collections import defaultdict
|
|
|
|
|
|
2020-09-26 21:21:43 +00:00
|
|
|
from django.core.exceptions import FieldDoesNotExist
|
|
|
|
|
from django.db.models.constants import LOOKUP_SEP
|
2016-05-07 21:28:30 +00:00
|
|
|
from django.db.models.deletion import Collector, ProtectedError
|
2020-09-26 21:21:43 +00:00
|
|
|
from django.utils.encoding import force_bytes, force_str
|
2013-05-25 14:52:58 +00:00
|
|
|
|
2016-05-07 20:59:15 +00:00
|
|
|
|
2013-06-06 17:50:45 +00:00
|
|
|
def lookup_needs_distinct(opts, lookup_path):
|
|
|
|
|
"""
|
|
|
|
|
Returns True if 'distinct()' should be used to query the given lookup path.
|
|
|
|
|
|
|
|
|
|
This is adopted from the Django core. django-admin2 mandates that code
|
|
|
|
|
doesn't depend on imports from django.contrib.admin.
|
|
|
|
|
|
2016-05-07 21:26:48 +00:00
|
|
|
https://github.com/django/django/blob/1.9.6/django/contrib/admin/utils.py#L22
|
2013-06-06 17:50:45 +00:00
|
|
|
"""
|
2016-05-07 21:26:48 +00:00
|
|
|
|
2020-09-26 21:21:43 +00:00
|
|
|
lookup_fields = lookup_path.split(LOOKUP_SEP)
|
|
|
|
|
# Go through the fields (following all relations) and look for an m2m.
|
2016-05-07 21:26:48 +00:00
|
|
|
for field_name in lookup_fields:
|
2020-09-26 21:21:43 +00:00
|
|
|
if field_name == 'pk':
|
|
|
|
|
field_name = opts.pk.name
|
|
|
|
|
try:
|
|
|
|
|
field = opts.get_field(field_name)
|
|
|
|
|
except FieldDoesNotExist:
|
|
|
|
|
# Ignore query lookups.
|
|
|
|
|
continue
|
|
|
|
|
else:
|
|
|
|
|
if hasattr(field, 'get_path_info'):
|
|
|
|
|
# This field is a relation; update opts to follow the relation.
|
|
|
|
|
path_info = field.get_path_info()
|
|
|
|
|
opts = path_info[-1].to_opts
|
|
|
|
|
if any(path.m2m for path in path_info):
|
|
|
|
|
# This field is a m2m relation so distinct must be called.
|
|
|
|
|
return True
|
2016-05-07 21:26:48 +00:00
|
|
|
return False
|
2013-06-06 17:50:45 +00:00
|
|
|
|
2013-07-06 07:37:25 +00:00
|
|
|
|
2013-05-23 17:46:03 +00:00
|
|
|
def model_options(model):
|
|
|
|
|
"""
|
|
|
|
|
Wrapper for accessing model._meta. If this access point changes in core
|
|
|
|
|
Django, this function allows django-admin2 to address the change with
|
|
|
|
|
what should hopefully be less disruption to the rest of the code base.
|
2013-05-24 14:22:54 +00:00
|
|
|
|
|
|
|
|
Works on model classes and objects.
|
2013-05-23 17:46:03 +00:00
|
|
|
"""
|
|
|
|
|
return model._meta
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def admin2_urlname(view, action):
|
|
|
|
|
"""
|
|
|
|
|
Converts the view and the specified action into a valid namespaced URLConf name.
|
|
|
|
|
"""
|
|
|
|
|
return 'admin2:%s_%s_%s' % (view.app_label, view.model_name, action)
|
|
|
|
|
|
|
|
|
|
|
2013-07-06 09:14:06 +00:00
|
|
|
def model_verbose_name(model):
|
2013-05-23 17:46:03 +00:00
|
|
|
"""
|
|
|
|
|
Returns the verbose name of a model instance or class.
|
|
|
|
|
"""
|
2013-07-06 09:14:06 +00:00
|
|
|
return model_options(model).verbose_name
|
2013-05-23 17:46:03 +00:00
|
|
|
|
|
|
|
|
|
2013-07-06 09:14:06 +00:00
|
|
|
def model_verbose_name_plural(model):
|
2013-05-23 17:46:03 +00:00
|
|
|
"""
|
|
|
|
|
Returns the pluralized verbose name of a model instance or class.
|
|
|
|
|
"""
|
2013-07-06 09:14:06 +00:00
|
|
|
return model_options(model).verbose_name_plural
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def model_field_verbose_name(model, field_name):
|
|
|
|
|
"""
|
|
|
|
|
Returns the verbose name of a model field.
|
|
|
|
|
"""
|
|
|
|
|
meta = model_options(model)
|
2016-11-13 05:47:08 +00:00
|
|
|
field = meta.get_field(field_name)
|
2013-07-06 09:14:06 +00:00
|
|
|
return field.verbose_name
|
2013-05-26 00:55:43 +00:00
|
|
|
|
|
|
|
|
|
2013-07-06 09:23:26 +00:00
|
|
|
def model_method_verbose_name(model, method_name):
|
|
|
|
|
"""
|
|
|
|
|
Returns the verbose name / short description of a model field.
|
|
|
|
|
"""
|
|
|
|
|
method = getattr(model, method_name)
|
|
|
|
|
try:
|
|
|
|
|
return method.short_description
|
|
|
|
|
except AttributeError:
|
|
|
|
|
return method_name
|
|
|
|
|
|
|
|
|
|
|
2013-05-26 00:55:43 +00:00
|
|
|
def model_app_label(obj):
|
|
|
|
|
"""
|
|
|
|
|
Returns the app label of a model instance or class.
|
|
|
|
|
"""
|
2013-05-30 09:35:58 +00:00
|
|
|
return model_options(obj).app_label
|
2013-05-25 14:52:58 +00:00
|
|
|
|
|
|
|
|
|
2013-07-07 10:01:50 +00:00
|
|
|
def get_attr(obj, attr):
|
|
|
|
|
"""
|
|
|
|
|
Get the right value for the attribute. Handle special cases like callables
|
|
|
|
|
and the __str__ attribute.
|
|
|
|
|
"""
|
|
|
|
|
if attr == '__str__':
|
2016-05-07 20:59:15 +00:00
|
|
|
from builtins import str as text
|
|
|
|
|
value = text(obj)
|
2013-07-07 10:01:50 +00:00
|
|
|
else:
|
|
|
|
|
attribute = getattr(obj, attr)
|
|
|
|
|
value = attribute() if callable(attribute) else attribute
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
2013-05-25 14:52:58 +00:00
|
|
|
class NestedObjects(Collector):
|
2013-05-29 13:10:56 +00:00
|
|
|
"""
|
|
|
|
|
This is adopted from the Django core. django-admin2 mandates that code
|
|
|
|
|
doesn't depend on imports from django.contrib.admin.
|
|
|
|
|
|
2016-05-07 21:26:48 +00:00
|
|
|
https://github.com/django/django/blob/1.9.6/django/contrib/admin/utils.py#L171-L231
|
2013-05-29 13:10:56 +00:00
|
|
|
"""
|
2015-03-21 15:44:11 +00:00
|
|
|
|
2013-05-25 14:52:58 +00:00
|
|
|
def __init__(self, *args, **kwargs):
|
2021-10-17 09:13:22 +00:00
|
|
|
super().__init__(*args, **kwargs)
|
2013-05-30 09:35:58 +00:00
|
|
|
self.edges = {} # {from_instance: [to_instances]}
|
2013-05-25 14:52:58 +00:00
|
|
|
self.protected = set()
|
2016-05-07 21:26:48 +00:00
|
|
|
self.model_objs = defaultdict(set)
|
2013-05-25 14:52:58 +00:00
|
|
|
|
|
|
|
|
def add_edge(self, source, target):
|
|
|
|
|
self.edges.setdefault(source, []).append(target)
|
|
|
|
|
|
2015-03-21 15:44:11 +00:00
|
|
|
def collect(self, objs, source=None, source_attr=None, **kwargs):
|
2013-05-25 14:52:58 +00:00
|
|
|
for obj in objs:
|
2015-03-21 15:44:11 +00:00
|
|
|
if source_attr and not source_attr.endswith('+'):
|
|
|
|
|
related_name = source_attr % {
|
|
|
|
|
'class': source._meta.model_name,
|
|
|
|
|
'app_label': source._meta.app_label,
|
|
|
|
|
}
|
|
|
|
|
self.add_edge(getattr(obj, related_name), obj)
|
2013-05-25 14:52:58 +00:00
|
|
|
else:
|
|
|
|
|
self.add_edge(None, obj)
|
2016-05-07 21:26:48 +00:00
|
|
|
self.model_objs[obj._meta.model].add(obj)
|
2013-05-25 14:52:58 +00:00
|
|
|
try:
|
2021-10-17 09:13:22 +00:00
|
|
|
return super().collect(objs, source_attr=source_attr, **kwargs)
|
2016-05-07 21:28:30 +00:00
|
|
|
except ProtectedError as e:
|
2013-05-25 14:52:58 +00:00
|
|
|
self.protected.update(e.protected_objects)
|
|
|
|
|
|
2020-09-26 21:21:43 +00:00
|
|
|
def related_objects(self, *args):
|
|
|
|
|
# Django >= 3.1
|
|
|
|
|
if len(args) == 3:
|
|
|
|
|
related_model, related_fields, objs = args
|
|
|
|
|
qs = super().related_objects(related_model, related_fields, objs)
|
|
|
|
|
return qs.select_related(*[related_field.name for related_field in related_fields])
|
|
|
|
|
# Django < 3.1
|
|
|
|
|
elif len(args) == 2:
|
|
|
|
|
related, objs = args
|
|
|
|
|
qs = super(NestedObjects, self).related_objects(related, objs)
|
|
|
|
|
return qs.select_related(related.field.name)
|
2013-05-25 14:52:58 +00:00
|
|
|
|
|
|
|
|
def _nested(self, obj, seen, format_callback):
|
|
|
|
|
if obj in seen:
|
|
|
|
|
return []
|
|
|
|
|
seen.add(obj)
|
|
|
|
|
children = []
|
|
|
|
|
for child in self.edges.get(obj, ()):
|
|
|
|
|
children.extend(self._nested(child, seen, format_callback))
|
|
|
|
|
if format_callback:
|
|
|
|
|
ret = [format_callback(obj)]
|
|
|
|
|
else:
|
|
|
|
|
ret = [obj]
|
|
|
|
|
if children:
|
|
|
|
|
ret.append(children)
|
|
|
|
|
return ret
|
|
|
|
|
|
|
|
|
|
def nested(self, format_callback=None):
|
|
|
|
|
"""
|
|
|
|
|
Return the graph as a nested list.
|
|
|
|
|
"""
|
|
|
|
|
seen = set()
|
|
|
|
|
roots = []
|
|
|
|
|
for root in self.edges.get(None, ()):
|
|
|
|
|
roots.extend(self._nested(root, seen, format_callback))
|
|
|
|
|
return roots
|
|
|
|
|
|
|
|
|
|
def can_fast_delete(self, *args, **kwargs):
|
|
|
|
|
"""
|
|
|
|
|
We always want to load the objects into memory so that we can display
|
|
|
|
|
them to the user in confirm page.
|
|
|
|
|
"""
|
|
|
|
|
return False
|
2013-08-02 10:52:38 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def quote(s):
|
|
|
|
|
"""
|
|
|
|
|
Ensure that primary key values do not confuse the admin URLs by escaping
|
|
|
|
|
any '/', '_' and ':' and similarly problematic characters.
|
|
|
|
|
Similar to urllib.quote, except that the quoting is slightly different so
|
|
|
|
|
that it doesn't get automatically unquoted by the Web browser.
|
|
|
|
|
|
|
|
|
|
This is adopted from the Django core. django-admin2 mandates that code
|
|
|
|
|
doesn't depend on imports from django.contrib.admin.
|
|
|
|
|
|
2016-05-07 21:26:48 +00:00
|
|
|
https://github.com/django/django/blob/1.9.6/django/contrib/admin/utils.py#L66-L73
|
2013-08-02 10:52:38 +00:00
|
|
|
"""
|
2020-09-26 21:21:43 +00:00
|
|
|
if not isinstance(s, str):
|
2013-08-02 10:52:38 +00:00
|
|
|
return s
|
|
|
|
|
res = list(s)
|
|
|
|
|
for i in range(len(res)):
|
|
|
|
|
c = res[i]
|
2016-05-07 21:26:48 +00:00
|
|
|
if c in """:/_#?;@&=+$,"[]<>%\n\\""":
|
2013-08-02 10:52:38 +00:00
|
|
|
res[i] = '_%02X' % ord(c)
|
|
|
|
|
return ''.join(res)
|
2014-09-28 17:04:23 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def type_str(text):
|
2020-09-26 21:21:43 +00:00
|
|
|
return force_str(text)
|