web/lib/django/contrib/admin/util.py
changeset 29 cc9b7e14412b
parent 0 0d40e90630ef
equal deleted inserted replaced
28:b758351d191f 29:cc9b7e14412b
     1 from django.core.exceptions import ObjectDoesNotExist
     1 from django.core.exceptions import ObjectDoesNotExist
     2 from django.db import models
     2 from django.db import models
       
     3 from django.forms.forms import pretty_name
       
     4 from django.utils import formats
     3 from django.utils.html import escape
     5 from django.utils.html import escape
     4 from django.utils.safestring import mark_safe
     6 from django.utils.safestring import mark_safe
     5 from django.utils.text import capfirst
     7 from django.utils.text import capfirst
     6 from django.utils.encoding import force_unicode
     8 from django.utils.encoding import force_unicode, smart_unicode, smart_str
     7 from django.utils.translation import ungettext, ugettext as _
     9 from django.utils.translation import ungettext, ugettext as _
     8 from django.core.urlresolvers import reverse, NoReverseMatch
    10 from django.core.urlresolvers import reverse, NoReverseMatch
       
    11 from django.utils.datastructures import SortedDict
     9 
    12 
    10 def quote(s):
    13 def quote(s):
    11     """
    14     """
    12     Ensure that primary key values do not confuse the admin URLs by escaping
    15     Ensure that primary key values do not confuse the admin URLs by escaping
    13     any '/', '_' and ':' characters. Similar to urllib.quote, except that the
    16     any '/', '_' and ':' characters. Similar to urllib.quote, except that the
    53                 field_names.extend(field)
    56                 field_names.extend(field)
    54             else:
    57             else:
    55                 field_names.append(field)
    58                 field_names.append(field)
    56     return field_names
    59     return field_names
    57 
    60 
    58 def _nest_help(obj, depth, val):
    61 def _format_callback(obj, user, admin_site, levels_to_root, perms_needed):
    59     current = obj
    62     has_admin = obj.__class__ in admin_site._registry
    60     for i in range(depth):
    63     opts = obj._meta
    61         current = current[-1]
       
    62     current.append(val)
       
    63 
       
    64 def get_change_view_url(app_label, module_name, pk, admin_site, levels_to_root):
       
    65     """
       
    66     Returns the url to the admin change view for the given app_label,
       
    67     module_name and primary key.
       
    68     """
       
    69     try:
    64     try:
    70         return reverse('%sadmin_%s_%s_change' % (admin_site.name, app_label, module_name), None, (pk,))
    65         admin_url = reverse('%s:%s_%s_change'
       
    66                             % (admin_site.name,
       
    67                                opts.app_label,
       
    68                                opts.object_name.lower()),
       
    69                             None, (quote(obj._get_pk_val()),))
    71     except NoReverseMatch:
    70     except NoReverseMatch:
    72         return '%s%s/%s/%s/' % ('../'*levels_to_root, app_label, module_name, pk)
    71         admin_url = '%s%s/%s/%s/' % ('../'*levels_to_root,
    73 
    72                                      opts.app_label,
    74 def get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current_depth, admin_site, levels_to_root=4):
    73                                      opts.object_name.lower(),
    75     """
    74                                      quote(obj._get_pk_val()))
    76     Helper function that recursively populates deleted_objects.
    75     if has_admin:
    77 
    76         p = '%s.%s' % (opts.app_label,
    78     `levels_to_root` defines the number of directories (../) to reach the
    77                        opts.get_delete_permission())
    79     admin root path. In a change_view this is 4, in a change_list view 2.
    78         if not user.has_perm(p):
       
    79             perms_needed.add(opts.verbose_name)
       
    80         # Display a link to the admin page.
       
    81         return mark_safe(u'%s: <a href="%s">%s</a>' %
       
    82                          (escape(capfirst(opts.verbose_name)),
       
    83                           admin_url,
       
    84                           escape(obj)))
       
    85     else:
       
    86         # Don't display link to edit, because it either has no
       
    87         # admin or is edited inline.
       
    88         return u'%s: %s' % (capfirst(opts.verbose_name),
       
    89                             force_unicode(obj))
       
    90 
       
    91 def get_deleted_objects(objs, opts, user, admin_site, levels_to_root=4):
       
    92     """
       
    93     Find all objects related to ``objs`` that should also be
       
    94     deleted. ``objs`` should be an iterable of objects.
       
    95 
       
    96     Returns a nested list of strings suitable for display in the
       
    97     template with the ``unordered_list`` filter.
       
    98 
       
    99     `levels_to_root` defines the number of directories (../) to reach
       
   100     the admin root path. In a change_view this is 4, in a change_list
       
   101     view 2.
    80 
   102 
    81     This is for backwards compatibility since the options.delete_selected
   103     This is for backwards compatibility since the options.delete_selected
    82     method uses this function also from a change_list view.
   104     method uses this function also from a change_list view.
    83     This will not be used if we can reverse the URL.
   105     This will not be used if we can reverse the URL.
    84     """
   106     """
    85     nh = _nest_help # Bind to local variable for performance
   107     collector = NestedObjects()
    86     if current_depth > 16:
   108     for obj in objs:
    87         return # Avoid recursing too deep.
   109         # TODO using a private model API!
    88     opts_seen = []
   110         obj._collect_sub_objects(collector)
    89     for related in opts.get_all_related_objects():
   111 
    90         has_admin = related.model in admin_site._registry
   112     perms_needed = set()
    91         if related.opts in opts_seen:
   113 
    92             continue
   114     to_delete = collector.nested(_format_callback,
    93         opts_seen.append(related.opts)
   115                                  user=user,
    94         rel_opts_name = related.get_accessor_name()
   116                                  admin_site=admin_site,
    95         if isinstance(related.field.rel, models.OneToOneRel):
   117                                  levels_to_root=levels_to_root,
    96             try:
   118                                  perms_needed=perms_needed)
    97                 sub_obj = getattr(obj, rel_opts_name)
   119 
    98             except ObjectDoesNotExist:
   120     return to_delete, perms_needed
    99                 pass
   121 
   100             else:
   122 
   101                 if has_admin:
   123 class NestedObjects(object):
   102                     p = '%s.%s' % (related.opts.app_label, related.opts.get_delete_permission())
   124     """
   103                     if not user.has_perm(p):
   125     A directed acyclic graph collection that exposes the add() API
   104                         perms_needed.add(related.opts.verbose_name)
   126     expected by Model._collect_sub_objects and can present its data as
   105                         # We don't care about populating deleted_objects now.
   127     a nested list of objects.
   106                         continue
   128 
   107                 if not has_admin:
   129     """
   108                     # Don't display link to edit, because it either has no
   130     def __init__(self):
   109                     # admin or is edited inline.
   131         # Use object keys of the form (model, pk) because actual model
   110                     nh(deleted_objects, current_depth,
   132         # objects may not be unique
   111                         [u'%s: %s' % (capfirst(related.opts.verbose_name), force_unicode(sub_obj)), []])
   133 
   112                 else:
   134         # maps object key to list of child keys
   113                     # Display a link to the admin page.
   135         self.children = SortedDict()
   114                     nh(deleted_objects, current_depth, [mark_safe(u'%s: <a href="%s">%s</a>' %
   136 
   115                         (escape(capfirst(related.opts.verbose_name)),
   137         # maps object key to parent key
   116                         get_change_view_url(related.opts.app_label,
   138         self.parents = SortedDict()
   117                                             related.opts.object_name.lower(),
   139 
   118                                             sub_obj._get_pk_val(),
   140         # maps object key to actual object
   119                                             admin_site,
   141         self.seen = SortedDict()
   120                                             levels_to_root),
   142 
   121                         escape(sub_obj))), []])
   143     def add(self, model, pk, obj,
   122                 get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, related.opts, current_depth+2, admin_site)
   144             parent_model=None, parent_obj=None, nullable=False):
       
   145         """
       
   146         Add item ``obj`` to the graph. Returns True (and does nothing)
       
   147         if the item has been seen already.
       
   148 
       
   149         The ``parent_obj`` argument must already exist in the graph; if
       
   150         not, it's ignored (but ``obj`` is still added with no
       
   151         parent). In any case, Model._collect_sub_objects (for whom
       
   152         this API exists) will never pass a parent that hasn't already
       
   153         been added itself.
       
   154 
       
   155         These restrictions in combination ensure the graph will remain
       
   156         acyclic (but can have multiple roots).
       
   157 
       
   158         ``model``, ``pk``, and ``parent_model`` arguments are ignored
       
   159         in favor of the appropriate lookups on ``obj`` and
       
   160         ``parent_obj``; unlike CollectedObjects, we can't maintain
       
   161         independence from the knowledge that we're operating on model
       
   162         instances, and we don't want to allow for inconsistency.
       
   163 
       
   164         ``nullable`` arg is ignored: it doesn't affect how the tree of
       
   165         collected objects should be nested for display.
       
   166         """
       
   167         model, pk = type(obj), obj._get_pk_val()
       
   168 
       
   169         # auto-created M2M models don't interest us
       
   170         if model._meta.auto_created:
       
   171             return True
       
   172 
       
   173         key = model, pk
       
   174 
       
   175         if key in self.seen:
       
   176             return True
       
   177         self.seen.setdefault(key, obj)
       
   178 
       
   179         if parent_obj is not None:
       
   180             parent_model, parent_pk = (type(parent_obj),
       
   181                                        parent_obj._get_pk_val())
       
   182             parent_key = (parent_model, parent_pk)
       
   183             if parent_key in self.seen:
       
   184                 self.children.setdefault(parent_key, list()).append(key)
       
   185                 self.parents.setdefault(key, parent_key)
       
   186 
       
   187     def _nested(self, key, format_callback=None, **kwargs):
       
   188         obj = self.seen[key]
       
   189         if format_callback:
       
   190             ret = [format_callback(obj, **kwargs)]
   123         else:
   191         else:
   124             has_related_objs = False
   192             ret = [obj]
   125             for sub_obj in getattr(obj, rel_opts_name).all():
   193 
   126                 has_related_objs = True
   194         children = []
   127                 if not has_admin:
   195         for child in self.children.get(key, ()):
   128                     # Don't display link to edit, because it either has no
   196             children.extend(self._nested(child, format_callback, **kwargs))
   129                     # admin or is edited inline.
   197         if children:
   130                     nh(deleted_objects, current_depth,
   198             ret.append(children)
   131                         [u'%s: %s' % (capfirst(related.opts.verbose_name), force_unicode(sub_obj)), []])
   199 
   132                 else:
   200         return ret
   133                     # Display a link to the admin page.
   201 
   134                     nh(deleted_objects, current_depth, [mark_safe(u'%s: <a href="%s">%s</a>' %
   202     def nested(self, format_callback=None, **kwargs):
   135                         (escape(capfirst(related.opts.verbose_name)),
   203         """
   136                         get_change_view_url(related.opts.app_label,
   204         Return the graph as a nested list.
   137                                             related.opts.object_name.lower(),
   205 
   138                                             sub_obj._get_pk_val(),
   206         Passes **kwargs back to the format_callback as kwargs.
   139                                             admin_site,
   207 
   140                                             levels_to_root),
   208         """
   141                         escape(sub_obj))), []])
   209         roots = []
   142                 get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, related.opts, current_depth+2, admin_site)
   210         for key in self.seen.keys():
   143             # If there were related objects, and the user doesn't have
   211             if key not in self.parents:
   144             # permission to delete them, add the missing perm to perms_needed.
   212                 roots.extend(self._nested(key, format_callback, **kwargs))
   145             if has_admin and has_related_objs:
   213         return roots
   146                 p = '%s.%s' % (related.opts.app_label, related.opts.get_delete_permission())
   214 
   147                 if not user.has_perm(p):
       
   148                     perms_needed.add(related.opts.verbose_name)
       
   149     for related in opts.get_all_related_many_to_many_objects():
       
   150         has_admin = related.model in admin_site._registry
       
   151         if related.opts in opts_seen:
       
   152             continue
       
   153         opts_seen.append(related.opts)
       
   154         rel_opts_name = related.get_accessor_name()
       
   155         has_related_objs = False
       
   156 
       
   157         # related.get_accessor_name() could return None for symmetrical relationships
       
   158         if rel_opts_name:
       
   159             rel_objs = getattr(obj, rel_opts_name, None)
       
   160             if rel_objs:
       
   161                 has_related_objs = True
       
   162 
       
   163         if has_related_objs:
       
   164             for sub_obj in rel_objs.all():
       
   165                 if not has_admin:
       
   166                     # Don't display link to edit, because it either has no
       
   167                     # admin or is edited inline.
       
   168                     nh(deleted_objects, current_depth, [_('One or more %(fieldname)s in %(name)s: %(obj)s') % \
       
   169                         {'fieldname': force_unicode(related.field.verbose_name), 'name': force_unicode(related.opts.verbose_name), 'obj': escape(sub_obj)}, []])
       
   170                 else:
       
   171                     # Display a link to the admin page.
       
   172                     nh(deleted_objects, current_depth, [
       
   173                         mark_safe((_('One or more %(fieldname)s in %(name)s:') % {'fieldname': escape(force_unicode(related.field.verbose_name)), 'name': escape(force_unicode(related.opts.verbose_name))}) + \
       
   174                         (u' <a href="%s">%s</a>' % \
       
   175                             (get_change_view_url(related.opts.app_label,
       
   176                                                  related.opts.object_name.lower(),
       
   177                                                  sub_obj._get_pk_val(),
       
   178                                                  admin_site,
       
   179                                                  levels_to_root),
       
   180                             escape(sub_obj)))), []])
       
   181         # If there were related objects, and the user doesn't have
       
   182         # permission to change them, add the missing perm to perms_needed.
       
   183         if has_admin and has_related_objs:
       
   184             p = u'%s.%s' % (related.opts.app_label, related.opts.get_change_permission())
       
   185             if not user.has_perm(p):
       
   186                 perms_needed.add(related.opts.verbose_name)
       
   187 
   215 
   188 def model_format_dict(obj):
   216 def model_format_dict(obj):
   189     """
   217     """
   190     Return a `dict` with keys 'verbose_name' and 'verbose_name_plural',
   218     Return a `dict` with keys 'verbose_name' and 'verbose_name_plural',
   191     typically for use with string formatting.
   219     typically for use with string formatting.
   219             n = obj.count()
   247             n = obj.count()
   220         obj = obj.model
   248         obj = obj.model
   221     d = model_format_dict(obj)
   249     d = model_format_dict(obj)
   222     singular, plural = d["verbose_name"], d["verbose_name_plural"]
   250     singular, plural = d["verbose_name"], d["verbose_name_plural"]
   223     return ungettext(singular, plural, n or 0)
   251     return ungettext(singular, plural, n or 0)
       
   252 
       
   253 def lookup_field(name, obj, model_admin=None):
       
   254     opts = obj._meta
       
   255     try:
       
   256         f = opts.get_field(name)
       
   257     except models.FieldDoesNotExist:
       
   258         # For non-field values, the value is either a method, property or
       
   259         # returned via a callable.
       
   260         if callable(name):
       
   261             attr = name
       
   262             value = attr(obj)
       
   263         elif (model_admin is not None and hasattr(model_admin, name) and
       
   264           not name == '__str__' and not name == '__unicode__'):
       
   265             attr = getattr(model_admin, name)
       
   266             value = attr(obj)
       
   267         else:
       
   268             attr = getattr(obj, name)
       
   269             if callable(attr):
       
   270                 value = attr()
       
   271             else:
       
   272                 value = attr
       
   273         f = None
       
   274     else:
       
   275         attr = None
       
   276         value = getattr(obj, name)
       
   277     return f, attr, value
       
   278 
       
   279 def label_for_field(name, model, model_admin=None, return_attr=False):
       
   280     attr = None
       
   281     try:
       
   282         label = model._meta.get_field_by_name(name)[0].verbose_name
       
   283     except models.FieldDoesNotExist:
       
   284         if name == "__unicode__":
       
   285             label = force_unicode(model._meta.verbose_name)
       
   286         elif name == "__str__":
       
   287             label = smart_str(model._meta.verbose_name)
       
   288         else:
       
   289             if callable(name):
       
   290                 attr = name
       
   291             elif model_admin is not None and hasattr(model_admin, name):
       
   292                 attr = getattr(model_admin, name)
       
   293             elif hasattr(model, name):
       
   294                 attr = getattr(model, name)
       
   295             else:
       
   296                 message = "Unable to lookup '%s' on %s" % (name, model._meta.object_name)
       
   297                 if model_admin:
       
   298                     message += " or %s" % (model_admin.__name__,)
       
   299                 raise AttributeError(message)
       
   300 
       
   301             if hasattr(attr, "short_description"):
       
   302                 label = attr.short_description
       
   303             elif callable(attr):
       
   304                 if attr.__name__ == "<lambda>":
       
   305                     label = "--"
       
   306                 else:
       
   307                     label = pretty_name(attr.__name__)
       
   308             else:
       
   309                 label = pretty_name(name)
       
   310     if return_attr:
       
   311         return (label, attr)
       
   312     else:
       
   313         return label
       
   314 
       
   315 
       
   316 def display_for_field(value, field):
       
   317     from django.contrib.admin.templatetags.admin_list import _boolean_icon
       
   318     from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
       
   319 
       
   320     if field.flatchoices:
       
   321         return dict(field.flatchoices).get(value, EMPTY_CHANGELIST_VALUE)
       
   322     # NullBooleanField needs special-case null-handling, so it comes
       
   323     # before the general null test.
       
   324     elif isinstance(field, models.BooleanField) or isinstance(field, models.NullBooleanField):
       
   325         return _boolean_icon(value)
       
   326     elif value is None:
       
   327         return EMPTY_CHANGELIST_VALUE
       
   328     elif isinstance(field, models.DateField) or isinstance(field, models.TimeField):
       
   329         return formats.localize(value)
       
   330     elif isinstance(field, models.DecimalField):
       
   331         return formats.number_format(value, field.decimal_places)
       
   332     elif isinstance(field, models.FloatField):
       
   333         return formats.number_format(value)
       
   334     else:
       
   335         return smart_unicode(value)