web/lib/django/contrib/admin/util.py
changeset 38 77b6da96e6f1
equal deleted inserted replaced
37:8d941af65caf 38:77b6da96e6f1
       
     1 from django.core.exceptions import ObjectDoesNotExist
       
     2 from django.db import models
       
     3 from django.forms.forms import pretty_name
       
     4 from django.utils import formats
       
     5 from django.utils.html import escape
       
     6 from django.utils.safestring import mark_safe
       
     7 from django.utils.text import capfirst
       
     8 from django.utils.encoding import force_unicode, smart_unicode, smart_str
       
     9 from django.utils.translation import ungettext, ugettext as _
       
    10 from django.core.urlresolvers import reverse, NoReverseMatch
       
    11 from django.utils.datastructures import SortedDict
       
    12 
       
    13 def quote(s):
       
    14     """
       
    15     Ensure that primary key values do not confuse the admin URLs by escaping
       
    16     any '/', '_' and ':' characters. Similar to urllib.quote, except that the
       
    17     quoting is slightly different so that it doesn't get automatically
       
    18     unquoted by the Web browser.
       
    19     """
       
    20     if not isinstance(s, basestring):
       
    21         return s
       
    22     res = list(s)
       
    23     for i in range(len(res)):
       
    24         c = res[i]
       
    25         if c in """:/_#?;@&=+$,"<>%\\""":
       
    26             res[i] = '_%02X' % ord(c)
       
    27     return ''.join(res)
       
    28 
       
    29 def unquote(s):
       
    30     """
       
    31     Undo the effects of quote(). Based heavily on urllib.unquote().
       
    32     """
       
    33     mychr = chr
       
    34     myatoi = int
       
    35     list = s.split('_')
       
    36     res = [list[0]]
       
    37     myappend = res.append
       
    38     del list[0]
       
    39     for item in list:
       
    40         if item[1:2]:
       
    41             try:
       
    42                 myappend(mychr(myatoi(item[:2], 16)) + item[2:])
       
    43             except ValueError:
       
    44                 myappend('_' + item)
       
    45         else:
       
    46             myappend('_' + item)
       
    47     return "".join(res)
       
    48 
       
    49 def flatten_fieldsets(fieldsets):
       
    50     """Returns a list of field names from an admin fieldsets structure."""
       
    51     field_names = []
       
    52     for name, opts in fieldsets:
       
    53         for field in opts['fields']:
       
    54             # type checking feels dirty, but it seems like the best way here
       
    55             if type(field) == tuple:
       
    56                 field_names.extend(field)
       
    57             else:
       
    58                 field_names.append(field)
       
    59     return field_names
       
    60 
       
    61 def _format_callback(obj, user, admin_site, levels_to_root, perms_needed):
       
    62     has_admin = obj.__class__ in admin_site._registry
       
    63     opts = obj._meta
       
    64     try:
       
    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()),))
       
    70     except NoReverseMatch:
       
    71         admin_url = '%s%s/%s/%s/' % ('../'*levels_to_root,
       
    72                                      opts.app_label,
       
    73                                      opts.object_name.lower(),
       
    74                                      quote(obj._get_pk_val()))
       
    75     if has_admin:
       
    76         p = '%s.%s' % (opts.app_label,
       
    77                        opts.get_delete_permission())
       
    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.
       
   102 
       
   103     This is for backwards compatibility since the options.delete_selected
       
   104     method uses this function also from a change_list view.
       
   105     This will not be used if we can reverse the URL.
       
   106     """
       
   107     collector = NestedObjects()
       
   108     for obj in objs:
       
   109         # TODO using a private model API!
       
   110         obj._collect_sub_objects(collector)
       
   111 
       
   112     perms_needed = set()
       
   113 
       
   114     to_delete = collector.nested(_format_callback,
       
   115                                  user=user,
       
   116                                  admin_site=admin_site,
       
   117                                  levels_to_root=levels_to_root,
       
   118                                  perms_needed=perms_needed)
       
   119 
       
   120     return to_delete, perms_needed
       
   121 
       
   122 
       
   123 class NestedObjects(object):
       
   124     """
       
   125     A directed acyclic graph collection that exposes the add() API
       
   126     expected by Model._collect_sub_objects and can present its data as
       
   127     a nested list of objects.
       
   128 
       
   129     """
       
   130     def __init__(self):
       
   131         # Use object keys of the form (model, pk) because actual model
       
   132         # objects may not be unique
       
   133 
       
   134         # maps object key to list of child keys
       
   135         self.children = SortedDict()
       
   136 
       
   137         # maps object key to parent key
       
   138         self.parents = SortedDict()
       
   139 
       
   140         # maps object key to actual object
       
   141         self.seen = SortedDict()
       
   142 
       
   143     def add(self, model, pk, obj,
       
   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)]
       
   191         else:
       
   192             ret = [obj]
       
   193 
       
   194         children = []
       
   195         for child in self.children.get(key, ()):
       
   196             children.extend(self._nested(child, format_callback, **kwargs))
       
   197         if children:
       
   198             ret.append(children)
       
   199 
       
   200         return ret
       
   201 
       
   202     def nested(self, format_callback=None, **kwargs):
       
   203         """
       
   204         Return the graph as a nested list.
       
   205 
       
   206         Passes **kwargs back to the format_callback as kwargs.
       
   207 
       
   208         """
       
   209         roots = []
       
   210         for key in self.seen.keys():
       
   211             if key not in self.parents:
       
   212                 roots.extend(self._nested(key, format_callback, **kwargs))
       
   213         return roots
       
   214 
       
   215 
       
   216 def model_format_dict(obj):
       
   217     """
       
   218     Return a `dict` with keys 'verbose_name' and 'verbose_name_plural',
       
   219     typically for use with string formatting.
       
   220 
       
   221     `obj` may be a `Model` instance, `Model` subclass, or `QuerySet` instance.
       
   222 
       
   223     """
       
   224     if isinstance(obj, (models.Model, models.base.ModelBase)):
       
   225         opts = obj._meta
       
   226     elif isinstance(obj, models.query.QuerySet):
       
   227         opts = obj.model._meta
       
   228     else:
       
   229         opts = obj
       
   230     return {
       
   231         'verbose_name': force_unicode(opts.verbose_name),
       
   232         'verbose_name_plural': force_unicode(opts.verbose_name_plural)
       
   233     }
       
   234 
       
   235 def model_ngettext(obj, n=None):
       
   236     """
       
   237     Return the appropriate `verbose_name` or `verbose_name_plural` value for
       
   238     `obj` depending on the count `n`.
       
   239 
       
   240     `obj` may be a `Model` instance, `Model` subclass, or `QuerySet` instance.
       
   241     If `obj` is a `QuerySet` instance, `n` is optional and the length of the
       
   242     `QuerySet` is used.
       
   243 
       
   244     """
       
   245     if isinstance(obj, models.query.QuerySet):
       
   246         if n is None:
       
   247             n = obj.count()
       
   248         obj = obj.model
       
   249     d = model_format_dict(obj)
       
   250     singular, plural = d["verbose_name"], d["verbose_name_plural"]
       
   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)