2012-06-08 00:08:47 +08:00
|
|
|
from __future__ import unicode_literals
|
|
|
|
|
2012-04-29 17:51:12 +08:00
|
|
|
import datetime
|
|
|
|
import decimal
|
|
|
|
|
2013-07-01 21:19:55 +08:00
|
|
|
from django.contrib.auth import get_permission_codename
|
2008-07-19 07:54:34 +08:00
|
|
|
from django.db import models
|
2012-09-09 07:51:36 +08:00
|
|
|
from django.db.models.constants import LOOKUP_SEP
|
2010-11-10 00:46:42 +08:00
|
|
|
from django.db.models.deletion import Collector
|
2010-10-17 23:17:52 +08:00
|
|
|
from django.db.models.related import RelatedObject
|
2010-04-11 16:35:04 +08:00
|
|
|
from django.forms.forms import pretty_name
|
2009-12-23 02:29:00 +08:00
|
|
|
from django.utils import formats
|
2012-07-03 07:31:14 +08:00
|
|
|
from django.utils.html import format_html
|
2008-07-19 07:54:34 +08:00
|
|
|
from django.utils.text import capfirst
|
2011-11-18 21:01:06 +08:00
|
|
|
from django.utils import timezone
|
2012-08-29 02:59:56 +08:00
|
|
|
from django.utils.encoding import force_str, force_text, smart_text
|
2012-07-20 18:45:19 +08:00
|
|
|
from django.utils import six
|
2010-10-12 09:03:53 +08:00
|
|
|
from django.utils.translation import ungettext
|
2013-08-18 06:05:13 +08:00
|
|
|
from django.core.urlresolvers import reverse, NoReverseMatch
|
2009-12-23 02:29:00 +08:00
|
|
|
|
2011-11-22 20:26:17 +08:00
|
|
|
def lookup_needs_distinct(opts, lookup_path):
|
|
|
|
"""
|
|
|
|
Returns True if 'distinct()' should be used to query the given lookup path.
|
|
|
|
"""
|
|
|
|
field_name = lookup_path.split('__', 1)[0]
|
|
|
|
field = opts.get_field_by_name(field_name)[0]
|
|
|
|
if ((hasattr(field, 'rel') and
|
|
|
|
isinstance(field.rel, models.ManyToManyRel)) or
|
|
|
|
(isinstance(field, models.related.RelatedObject) and
|
|
|
|
not field.field.unique)):
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
|
|
def prepare_lookup_value(key, value):
|
|
|
|
"""
|
|
|
|
Returns a lookup value prepared to be used in queryset filtering.
|
|
|
|
"""
|
|
|
|
# if key ends with __in, split parameter into separate values
|
|
|
|
if key.endswith('__in'):
|
|
|
|
value = value.split(',')
|
2013-05-28 00:31:49 +08:00
|
|
|
# if key ends with __isnull, special case '' and the string literals 'false' and '0'
|
2011-11-22 20:26:17 +08:00
|
|
|
if key.endswith('__isnull'):
|
2013-05-28 00:31:49 +08:00
|
|
|
if value.lower() in ('', 'false', '0'):
|
2011-11-22 20:26:17 +08:00
|
|
|
value = False
|
|
|
|
else:
|
|
|
|
value = True
|
|
|
|
return value
|
2010-11-10 00:46:42 +08:00
|
|
|
|
2008-07-19 07:54:34 +08:00
|
|
|
def quote(s):
|
|
|
|
"""
|
|
|
|
Ensure that primary key values do not confuse the admin URLs by escaping
|
2012-09-25 09:02:59 +08:00
|
|
|
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.
|
2008-07-19 07:54:34 +08:00
|
|
|
"""
|
2012-07-20 20:22:00 +08:00
|
|
|
if not isinstance(s, six.string_types):
|
2008-07-19 07:54:34 +08:00
|
|
|
return s
|
|
|
|
res = list(s)
|
|
|
|
for i in range(len(res)):
|
|
|
|
c = res[i]
|
|
|
|
if c in """:/_#?;@&=+$,"<>%\\""":
|
|
|
|
res[i] = '_%02X' % ord(c)
|
|
|
|
return ''.join(res)
|
|
|
|
|
2010-11-10 00:46:42 +08:00
|
|
|
|
2008-07-19 07:54:34 +08:00
|
|
|
def unquote(s):
|
|
|
|
"""
|
|
|
|
Undo the effects of quote(). Based heavily on urllib.unquote().
|
|
|
|
"""
|
|
|
|
mychr = chr
|
|
|
|
myatoi = int
|
|
|
|
list = s.split('_')
|
|
|
|
res = [list[0]]
|
|
|
|
myappend = res.append
|
|
|
|
del list[0]
|
|
|
|
for item in list:
|
|
|
|
if item[1:2]:
|
|
|
|
try:
|
|
|
|
myappend(mychr(myatoi(item[:2], 16)) + item[2:])
|
|
|
|
except ValueError:
|
|
|
|
myappend('_' + item)
|
|
|
|
else:
|
|
|
|
myappend('_' + item)
|
|
|
|
return "".join(res)
|
|
|
|
|
2010-11-10 00:46:42 +08:00
|
|
|
|
2008-08-18 23:49:58 +08:00
|
|
|
def flatten_fieldsets(fieldsets):
|
|
|
|
"""Returns a list of field names from an admin fieldsets structure."""
|
|
|
|
field_names = []
|
|
|
|
for name, opts in fieldsets:
|
|
|
|
for field in opts['fields']:
|
2013-01-22 05:34:36 +08:00
|
|
|
if isinstance(field, (list, tuple)):
|
2008-08-18 23:49:58 +08:00
|
|
|
field_names.extend(field)
|
|
|
|
else:
|
|
|
|
field_names.append(field)
|
|
|
|
return field_names
|
|
|
|
|
2009-03-24 04:22:56 +08:00
|
|
|
|
2010-11-10 00:46:42 +08:00
|
|
|
def get_deleted_objects(objs, opts, user, admin_site, using):
|
2009-03-24 04:22:56 +08:00
|
|
|
"""
|
2010-11-10 00:46:42 +08:00
|
|
|
Find all objects related to ``objs`` that should also be deleted. ``objs``
|
|
|
|
must be a homogenous iterable of objects (e.g. a QuerySet).
|
2010-02-26 21:17:43 +08:00
|
|
|
|
|
|
|
Returns a nested list of strings suitable for display in the
|
|
|
|
template with the ``unordered_list`` filter.
|
2009-03-24 04:22:56 +08:00
|
|
|
|
|
|
|
"""
|
2010-11-10 00:46:42 +08:00
|
|
|
collector = NestedObjects(using=using)
|
|
|
|
collector.collect(objs)
|
2010-02-26 21:17:43 +08:00
|
|
|
perms_needed = set()
|
|
|
|
|
2010-11-10 00:46:42 +08:00
|
|
|
def format_callback(obj):
|
|
|
|
has_admin = obj.__class__ in admin_site._registry
|
|
|
|
opts = obj._meta
|
2010-02-26 21:17:43 +08:00
|
|
|
|
2013-08-18 06:05:13 +08:00
|
|
|
no_edit_link = '%s: %s' % (capfirst(opts.verbose_name),
|
|
|
|
force_text(obj))
|
|
|
|
|
2010-11-10 00:46:42 +08:00
|
|
|
if has_admin:
|
2013-08-18 06:05:13 +08:00
|
|
|
try:
|
|
|
|
admin_url = reverse('%s:%s_%s_change'
|
|
|
|
% (admin_site.name,
|
|
|
|
opts.app_label,
|
|
|
|
opts.model_name),
|
|
|
|
None, (quote(obj._get_pk_val()),))
|
|
|
|
except NoReverseMatch:
|
|
|
|
# Change url doesn't exist -- don't display link to edit
|
|
|
|
return no_edit_link
|
|
|
|
|
2010-11-10 00:46:42 +08:00
|
|
|
p = '%s.%s' % (opts.app_label,
|
2013-07-01 21:19:55 +08:00
|
|
|
get_permission_codename('delete', opts))
|
2010-11-10 00:46:42 +08:00
|
|
|
if not user.has_perm(p):
|
|
|
|
perms_needed.add(opts.verbose_name)
|
|
|
|
# Display a link to the admin page.
|
2012-07-03 07:31:14 +08:00
|
|
|
return format_html('{0}: <a href="{1}">{2}</a>',
|
|
|
|
capfirst(opts.verbose_name),
|
|
|
|
admin_url,
|
|
|
|
obj)
|
2010-11-10 00:46:42 +08:00
|
|
|
else:
|
|
|
|
# Don't display link to edit, because it either has no
|
|
|
|
# admin or is edited inline.
|
2013-08-18 06:05:13 +08:00
|
|
|
return no_edit_link
|
2010-02-26 21:17:43 +08:00
|
|
|
|
2010-11-10 00:46:42 +08:00
|
|
|
to_delete = collector.nested(format_callback)
|
2010-02-26 21:17:43 +08:00
|
|
|
|
2011-01-20 08:33:32 +08:00
|
|
|
protected = [format_callback(obj) for obj in collector.protected]
|
|
|
|
|
|
|
|
return to_delete, perms_needed, protected
|
2010-02-26 21:17:43 +08:00
|
|
|
|
|
|
|
|
2010-11-10 00:46:42 +08:00
|
|
|
class NestedObjects(Collector):
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super(NestedObjects, self).__init__(*args, **kwargs)
|
|
|
|
self.edges = {} # {from_instance: [to_instances]}
|
2011-01-20 08:33:32 +08:00
|
|
|
self.protected = set()
|
2010-03-15 21:15:01 +08:00
|
|
|
|
2010-11-10 00:46:42 +08:00
|
|
|
def add_edge(self, source, target):
|
|
|
|
self.edges.setdefault(source, []).append(target)
|
2010-02-26 21:17:43 +08:00
|
|
|
|
2010-11-10 00:46:42 +08:00
|
|
|
def collect(self, objs, source_attr=None, **kwargs):
|
|
|
|
for obj in objs:
|
|
|
|
if source_attr:
|
|
|
|
self.add_edge(getattr(obj, source_attr), obj)
|
|
|
|
else:
|
|
|
|
self.add_edge(None, obj)
|
2011-01-20 08:33:32 +08:00
|
|
|
try:
|
|
|
|
return super(NestedObjects, self).collect(objs, source_attr=source_attr, **kwargs)
|
2012-04-29 00:09:37 +08:00
|
|
|
except models.ProtectedError as e:
|
2011-01-20 08:33:32 +08:00
|
|
|
self.protected.update(e.protected_objects)
|
2010-02-26 21:17:43 +08:00
|
|
|
|
2010-11-10 00:46:42 +08:00
|
|
|
def related_objects(self, related, objs):
|
|
|
|
qs = super(NestedObjects, self).related_objects(related, objs)
|
|
|
|
return qs.select_related(related.field.name)
|
2010-02-26 21:17:43 +08:00
|
|
|
|
2010-11-10 00:46:42 +08: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))
|
2010-02-26 21:17:43 +08:00
|
|
|
if format_callback:
|
2010-11-10 00:46:42 +08:00
|
|
|
ret = [format_callback(obj)]
|
2008-07-19 07:54:34 +08:00
|
|
|
else:
|
2010-02-26 21:17:43 +08:00
|
|
|
ret = [obj]
|
|
|
|
if children:
|
|
|
|
ret.append(children)
|
|
|
|
return ret
|
|
|
|
|
2010-11-10 00:46:42 +08:00
|
|
|
def nested(self, format_callback=None):
|
2010-02-26 21:17:43 +08:00
|
|
|
"""
|
|
|
|
Return the graph as a nested list.
|
|
|
|
|
|
|
|
"""
|
2010-11-10 00:46:42 +08:00
|
|
|
seen = set()
|
2010-02-26 21:17:43 +08:00
|
|
|
roots = []
|
2010-11-10 00:46:42 +08:00
|
|
|
for root in self.edges.get(None, ()):
|
|
|
|
roots.extend(self._nested(root, seen, format_callback))
|
2010-02-26 21:17:43 +08:00
|
|
|
return roots
|
|
|
|
|
2012-09-20 23:51:30 +08:00
|
|
|
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
|
|
|
|
|
2009-03-24 04:22:56 +08:00
|
|
|
|
|
|
|
def model_format_dict(obj):
|
|
|
|
"""
|
|
|
|
Return a `dict` with keys 'verbose_name' and 'verbose_name_plural',
|
|
|
|
typically for use with string formatting.
|
|
|
|
|
|
|
|
`obj` may be a `Model` instance, `Model` subclass, or `QuerySet` instance.
|
|
|
|
|
|
|
|
"""
|
|
|
|
if isinstance(obj, (models.Model, models.base.ModelBase)):
|
|
|
|
opts = obj._meta
|
|
|
|
elif isinstance(obj, models.query.QuerySet):
|
|
|
|
opts = obj.model._meta
|
|
|
|
else:
|
|
|
|
opts = obj
|
|
|
|
return {
|
2012-07-21 16:00:10 +08:00
|
|
|
'verbose_name': force_text(opts.verbose_name),
|
|
|
|
'verbose_name_plural': force_text(opts.verbose_name_plural)
|
2009-03-24 04:22:56 +08:00
|
|
|
}
|
|
|
|
|
2010-11-10 00:46:42 +08:00
|
|
|
|
2009-03-24 04:22:56 +08:00
|
|
|
def model_ngettext(obj, n=None):
|
|
|
|
"""
|
2009-03-25 09:31:57 +08:00
|
|
|
Return the appropriate `verbose_name` or `verbose_name_plural` value for
|
|
|
|
`obj` depending on the count `n`.
|
2009-03-24 04:22:56 +08:00
|
|
|
|
|
|
|
`obj` may be a `Model` instance, `Model` subclass, or `QuerySet` instance.
|
|
|
|
If `obj` is a `QuerySet` instance, `n` is optional and the length of the
|
|
|
|
`QuerySet` is used.
|
|
|
|
|
|
|
|
"""
|
|
|
|
if isinstance(obj, models.query.QuerySet):
|
|
|
|
if n is None:
|
|
|
|
n = obj.count()
|
|
|
|
obj = obj.model
|
|
|
|
d = model_format_dict(obj)
|
2009-03-25 09:31:57 +08:00
|
|
|
singular, plural = d["verbose_name"], d["verbose_name_plural"]
|
|
|
|
return ungettext(singular, plural, n or 0)
|
2009-12-23 02:29:00 +08:00
|
|
|
|
2010-11-10 00:46:42 +08:00
|
|
|
|
2009-12-23 02:29:00 +08:00
|
|
|
def lookup_field(name, obj, model_admin=None):
|
|
|
|
opts = obj._meta
|
|
|
|
try:
|
|
|
|
f = opts.get_field(name)
|
|
|
|
except models.FieldDoesNotExist:
|
|
|
|
# For non-field values, the value is either a method, property or
|
|
|
|
# returned via a callable.
|
|
|
|
if callable(name):
|
|
|
|
attr = name
|
|
|
|
value = attr(obj)
|
|
|
|
elif (model_admin is not None and hasattr(model_admin, name) and
|
|
|
|
not name == '__str__' and not name == '__unicode__'):
|
|
|
|
attr = getattr(model_admin, name)
|
|
|
|
value = attr(obj)
|
|
|
|
else:
|
|
|
|
attr = getattr(obj, name)
|
|
|
|
if callable(attr):
|
|
|
|
value = attr()
|
|
|
|
else:
|
|
|
|
value = attr
|
|
|
|
f = None
|
|
|
|
else:
|
|
|
|
attr = None
|
2010-02-01 22:12:56 +08:00
|
|
|
value = getattr(obj, name)
|
2009-12-23 02:29:00 +08:00
|
|
|
return f, attr, value
|
|
|
|
|
2010-11-10 00:46:42 +08:00
|
|
|
|
2010-01-10 11:36:59 +08:00
|
|
|
def label_for_field(name, model, model_admin=None, return_attr=False):
|
2011-06-02 07:17:40 +08:00
|
|
|
"""
|
2013-05-21 19:03:45 +08:00
|
|
|
Returns a sensible label for a field name. The name can be a callable,
|
|
|
|
property (but not created with @property decorator) or the name of an
|
|
|
|
object's attribute, as well as a genuine fields. If return_attr is
|
2011-06-02 07:17:40 +08:00
|
|
|
True, the resolved attribute (which could be a callable) is also returned.
|
|
|
|
This will be None if (and only if) the name refers to a field.
|
|
|
|
"""
|
2010-01-10 11:36:59 +08:00
|
|
|
attr = None
|
2009-12-23 02:29:00 +08:00
|
|
|
try:
|
2010-10-17 23:17:52 +08:00
|
|
|
field = model._meta.get_field_by_name(name)[0]
|
|
|
|
if isinstance(field, RelatedObject):
|
|
|
|
label = field.opts.verbose_name
|
|
|
|
else:
|
|
|
|
label = field.verbose_name
|
2009-12-23 02:29:00 +08:00
|
|
|
except models.FieldDoesNotExist:
|
|
|
|
if name == "__unicode__":
|
2012-07-21 16:00:10 +08:00
|
|
|
label = force_text(model._meta.verbose_name)
|
2012-07-20 20:48:51 +08:00
|
|
|
attr = six.text_type
|
2010-01-10 11:36:59 +08:00
|
|
|
elif name == "__str__":
|
2012-08-29 02:59:56 +08:00
|
|
|
label = force_str(model._meta.verbose_name)
|
2012-07-20 20:48:51 +08:00
|
|
|
attr = bytes
|
2009-12-23 02:29:00 +08:00
|
|
|
else:
|
2010-01-10 11:36:59 +08:00
|
|
|
if callable(name):
|
|
|
|
attr = name
|
|
|
|
elif model_admin is not None and hasattr(model_admin, name):
|
|
|
|
attr = getattr(model_admin, name)
|
|
|
|
elif hasattr(model, name):
|
|
|
|
attr = getattr(model, name)
|
|
|
|
else:
|
|
|
|
message = "Unable to lookup '%s' on %s" % (name, model._meta.object_name)
|
|
|
|
if model_admin:
|
2011-02-26 09:44:41 +08:00
|
|
|
message += " or %s" % (model_admin.__class__.__name__,)
|
2010-01-10 11:36:59 +08:00
|
|
|
raise AttributeError(message)
|
2009-12-23 02:29:00 +08:00
|
|
|
|
2010-01-10 11:36:59 +08:00
|
|
|
if hasattr(attr, "short_description"):
|
|
|
|
label = attr.short_description
|
2013-05-21 19:03:45 +08:00
|
|
|
elif (isinstance(attr, property) and
|
|
|
|
hasattr(attr, "fget") and
|
|
|
|
hasattr(attr.fget, "short_description")):
|
|
|
|
label = attr.fget.short_description
|
2010-01-10 11:36:59 +08:00
|
|
|
elif callable(attr):
|
|
|
|
if attr.__name__ == "<lambda>":
|
|
|
|
label = "--"
|
|
|
|
else:
|
2010-04-11 16:35:04 +08:00
|
|
|
label = pretty_name(attr.__name__)
|
2009-12-23 02:29:00 +08:00
|
|
|
else:
|
2010-04-11 16:35:04 +08:00
|
|
|
label = pretty_name(name)
|
2010-01-10 11:36:59 +08:00
|
|
|
if return_attr:
|
|
|
|
return (label, attr)
|
|
|
|
else:
|
|
|
|
return label
|
2009-12-23 02:29:00 +08:00
|
|
|
|
2013-05-21 19:03:45 +08:00
|
|
|
|
2011-02-19 20:55:09 +08:00
|
|
|
def help_text_for_field(name, model):
|
|
|
|
try:
|
|
|
|
help_text = model._meta.get_field_by_name(name)[0].help_text
|
|
|
|
except models.FieldDoesNotExist:
|
|
|
|
help_text = ""
|
2012-07-21 16:00:10 +08:00
|
|
|
return smart_text(help_text)
|
2011-02-19 20:55:09 +08:00
|
|
|
|
2009-12-23 02:29:00 +08:00
|
|
|
|
|
|
|
def display_for_field(value, field):
|
|
|
|
from django.contrib.admin.templatetags.admin_list import _boolean_icon
|
|
|
|
from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
|
2010-01-10 05:28:54 +08:00
|
|
|
|
|
|
|
if field.flatchoices:
|
|
|
|
return dict(field.flatchoices).get(value, EMPTY_CHANGELIST_VALUE)
|
2010-03-10 16:37:17 +08:00
|
|
|
# NullBooleanField needs special-case null-handling, so it comes
|
|
|
|
# before the general null test.
|
|
|
|
elif isinstance(field, models.BooleanField) or isinstance(field, models.NullBooleanField):
|
|
|
|
return _boolean_icon(value)
|
2010-01-10 05:28:54 +08:00
|
|
|
elif value is None:
|
|
|
|
return EMPTY_CHANGELIST_VALUE
|
2011-11-18 21:01:06 +08:00
|
|
|
elif isinstance(field, models.DateTimeField):
|
2012-04-29 21:37:23 +08:00
|
|
|
return formats.localize(timezone.template_localtime(value))
|
2012-04-29 17:51:12 +08:00
|
|
|
elif isinstance(field, (models.DateField, models.TimeField)):
|
2009-12-23 02:29:00 +08:00
|
|
|
return formats.localize(value)
|
|
|
|
elif isinstance(field, models.DecimalField):
|
|
|
|
return formats.number_format(value, field.decimal_places)
|
|
|
|
elif isinstance(field, models.FloatField):
|
|
|
|
return formats.number_format(value)
|
|
|
|
else:
|
2012-07-21 16:00:10 +08:00
|
|
|
return smart_text(value)
|
2010-11-22 03:29:15 +08:00
|
|
|
|
|
|
|
|
2012-04-29 17:51:12 +08:00
|
|
|
def display_for_value(value, boolean=False):
|
|
|
|
from django.contrib.admin.templatetags.admin_list import _boolean_icon
|
|
|
|
from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
|
|
|
|
|
|
|
|
if boolean:
|
|
|
|
return _boolean_icon(value)
|
|
|
|
elif value is None:
|
|
|
|
return EMPTY_CHANGELIST_VALUE
|
|
|
|
elif isinstance(value, datetime.datetime):
|
2012-04-29 21:37:23 +08:00
|
|
|
return formats.localize(timezone.template_localtime(value))
|
2012-04-29 17:51:12 +08:00
|
|
|
elif isinstance(value, (datetime.date, datetime.time)):
|
|
|
|
return formats.localize(value)
|
2012-07-20 18:45:19 +08:00
|
|
|
elif isinstance(value, six.integer_types + (decimal.Decimal, float)):
|
2012-04-29 17:51:12 +08:00
|
|
|
return formats.number_format(value)
|
|
|
|
else:
|
2012-07-21 16:00:10 +08:00
|
|
|
return smart_text(value)
|
2012-04-29 17:51:12 +08:00
|
|
|
|
|
|
|
|
2010-11-22 03:29:15 +08:00
|
|
|
class NotRelationField(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
def get_model_from_relation(field):
|
|
|
|
if isinstance(field, models.related.RelatedObject):
|
|
|
|
return field.model
|
|
|
|
elif getattr(field, 'rel'): # or isinstance?
|
|
|
|
return field.rel.to
|
|
|
|
else:
|
|
|
|
raise NotRelationField
|
|
|
|
|
|
|
|
|
|
|
|
def reverse_field_path(model, path):
|
|
|
|
""" Create a reversed field path.
|
|
|
|
|
|
|
|
E.g. Given (Order, "user__groups"),
|
|
|
|
return (Group, "user__order").
|
|
|
|
|
|
|
|
Final field must be a related model, not a data field.
|
|
|
|
|
|
|
|
"""
|
|
|
|
reversed_path = []
|
|
|
|
parent = model
|
|
|
|
pieces = path.split(LOOKUP_SEP)
|
|
|
|
for piece in pieces:
|
|
|
|
field, model, direct, m2m = parent._meta.get_field_by_name(piece)
|
|
|
|
# skip trailing data field if extant:
|
|
|
|
if len(reversed_path) == len(pieces)-1: # final iteration
|
|
|
|
try:
|
|
|
|
get_model_from_relation(field)
|
|
|
|
except NotRelationField:
|
|
|
|
break
|
|
|
|
if direct:
|
|
|
|
related_name = field.related_query_name()
|
|
|
|
parent = field.rel.to
|
|
|
|
else:
|
|
|
|
related_name = field.field.name
|
|
|
|
parent = field.model
|
|
|
|
reversed_path.insert(0, related_name)
|
|
|
|
return (parent, LOOKUP_SEP.join(reversed_path))
|
|
|
|
|
|
|
|
|
|
|
|
def get_fields_from_path(model, path):
|
|
|
|
""" Return list of Fields given path relative to model.
|
|
|
|
|
|
|
|
e.g. (ModelX, "user__groups__name") -> [
|
|
|
|
<django.db.models.fields.related.ForeignKey object at 0x...>,
|
|
|
|
<django.db.models.fields.related.ManyToManyField object at 0x...>,
|
|
|
|
<django.db.models.fields.CharField object at 0x...>,
|
|
|
|
]
|
|
|
|
"""
|
|
|
|
pieces = path.split(LOOKUP_SEP)
|
|
|
|
fields = []
|
|
|
|
for piece in pieces:
|
|
|
|
if fields:
|
|
|
|
parent = get_model_from_relation(fields[-1])
|
|
|
|
else:
|
|
|
|
parent = model
|
|
|
|
fields.append(parent._meta.get_field_by_name(piece)[0])
|
|
|
|
return fields
|
|
|
|
|
|
|
|
|
|
|
|
def remove_trailing_data_field(fields):
|
|
|
|
""" Discard trailing non-relation field if extant. """
|
|
|
|
try:
|
|
|
|
get_model_from_relation(fields[-1])
|
|
|
|
except NotRelationField:
|
|
|
|
fields = fields[:-1]
|
|
|
|
return fields
|
|
|
|
|
|
|
|
|
|
|
|
def get_limit_choices_to_from_path(model, path):
|
|
|
|
""" Return Q object for limiting choices if applicable.
|
|
|
|
|
|
|
|
If final model in path is linked via a ForeignKey or ManyToManyField which
|
|
|
|
has a `limit_choices_to` attribute, return it as a Q object.
|
|
|
|
"""
|
|
|
|
|
|
|
|
fields = get_fields_from_path(model, path)
|
|
|
|
fields = remove_trailing_data_field(fields)
|
|
|
|
limit_choices_to = (
|
|
|
|
fields and hasattr(fields[-1], 'rel') and
|
|
|
|
getattr(fields[-1].rel, 'limit_choices_to', None))
|
|
|
|
if not limit_choices_to:
|
|
|
|
return models.Q() # empty Q
|
|
|
|
elif isinstance(limit_choices_to, models.Q):
|
|
|
|
return limit_choices_to # already a Q
|
|
|
|
else:
|
|
|
|
return models.Q(**limit_choices_to) # convert dict to Q
|