diff -r 8d941af65caf -r 77b6da96e6f1 web/lib/django/contrib/admin/util.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/lib/django/contrib/admin/util.py Wed Jun 02 18:57:35 2010 +0200 @@ -0,0 +1,335 @@ +from django.core.exceptions import ObjectDoesNotExist +from django.db import models +from django.forms.forms import pretty_name +from django.utils import formats +from django.utils.html import escape +from django.utils.safestring import mark_safe +from django.utils.text import capfirst +from django.utils.encoding import force_unicode, smart_unicode, smart_str +from django.utils.translation import ungettext, ugettext as _ +from django.core.urlresolvers import reverse, NoReverseMatch +from django.utils.datastructures import SortedDict + +def quote(s): + """ + Ensure that primary key values do not confuse the admin URLs by escaping + any '/', '_' and ':' characters. Similar to urllib.quote, except that the + quoting is slightly different so that it doesn't get automatically + unquoted by the Web browser. + """ + if not isinstance(s, basestring): + 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) + +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) + +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']: + # type checking feels dirty, but it seems like the best way here + if type(field) == tuple: + field_names.extend(field) + else: + field_names.append(field) + return field_names + +def _format_callback(obj, user, admin_site, levels_to_root, perms_needed): + has_admin = obj.__class__ in admin_site._registry + opts = obj._meta + try: + admin_url = reverse('%s:%s_%s_change' + % (admin_site.name, + opts.app_label, + opts.object_name.lower()), + None, (quote(obj._get_pk_val()),)) + except NoReverseMatch: + admin_url = '%s%s/%s/%s/' % ('../'*levels_to_root, + opts.app_label, + opts.object_name.lower(), + quote(obj._get_pk_val())) + if has_admin: + p = '%s.%s' % (opts.app_label, + opts.get_delete_permission()) + if not user.has_perm(p): + perms_needed.add(opts.verbose_name) + # Display a link to the admin page. + return mark_safe(u'%s: %s' % + (escape(capfirst(opts.verbose_name)), + admin_url, + escape(obj))) + else: + # Don't display link to edit, because it either has no + # admin or is edited inline. + return u'%s: %s' % (capfirst(opts.verbose_name), + force_unicode(obj)) + +def get_deleted_objects(objs, opts, user, admin_site, levels_to_root=4): + """ + Find all objects related to ``objs`` that should also be + deleted. ``objs`` should be an iterable of objects. + + Returns a nested list of strings suitable for display in the + template with the ``unordered_list`` filter. + + `levels_to_root` defines the number of directories (../) to reach + the admin root path. In a change_view this is 4, in a change_list + view 2. + + This is for backwards compatibility since the options.delete_selected + method uses this function also from a change_list view. + This will not be used if we can reverse the URL. + """ + collector = NestedObjects() + for obj in objs: + # TODO using a private model API! + obj._collect_sub_objects(collector) + + perms_needed = set() + + to_delete = collector.nested(_format_callback, + user=user, + admin_site=admin_site, + levels_to_root=levels_to_root, + perms_needed=perms_needed) + + return to_delete, perms_needed + + +class NestedObjects(object): + """ + A directed acyclic graph collection that exposes the add() API + expected by Model._collect_sub_objects and can present its data as + a nested list of objects. + + """ + def __init__(self): + # Use object keys of the form (model, pk) because actual model + # objects may not be unique + + # maps object key to list of child keys + self.children = SortedDict() + + # maps object key to parent key + self.parents = SortedDict() + + # maps object key to actual object + self.seen = SortedDict() + + def add(self, model, pk, obj, + parent_model=None, parent_obj=None, nullable=False): + """ + Add item ``obj`` to the graph. Returns True (and does nothing) + if the item has been seen already. + + The ``parent_obj`` argument must already exist in the graph; if + not, it's ignored (but ``obj`` is still added with no + parent). In any case, Model._collect_sub_objects (for whom + this API exists) will never pass a parent that hasn't already + been added itself. + + These restrictions in combination ensure the graph will remain + acyclic (but can have multiple roots). + + ``model``, ``pk``, and ``parent_model`` arguments are ignored + in favor of the appropriate lookups on ``obj`` and + ``parent_obj``; unlike CollectedObjects, we can't maintain + independence from the knowledge that we're operating on model + instances, and we don't want to allow for inconsistency. + + ``nullable`` arg is ignored: it doesn't affect how the tree of + collected objects should be nested for display. + """ + model, pk = type(obj), obj._get_pk_val() + + # auto-created M2M models don't interest us + if model._meta.auto_created: + return True + + key = model, pk + + if key in self.seen: + return True + self.seen.setdefault(key, obj) + + if parent_obj is not None: + parent_model, parent_pk = (type(parent_obj), + parent_obj._get_pk_val()) + parent_key = (parent_model, parent_pk) + if parent_key in self.seen: + self.children.setdefault(parent_key, list()).append(key) + self.parents.setdefault(key, parent_key) + + def _nested(self, key, format_callback=None, **kwargs): + obj = self.seen[key] + if format_callback: + ret = [format_callback(obj, **kwargs)] + else: + ret = [obj] + + children = [] + for child in self.children.get(key, ()): + children.extend(self._nested(child, format_callback, **kwargs)) + if children: + ret.append(children) + + return ret + + def nested(self, format_callback=None, **kwargs): + """ + Return the graph as a nested list. + + Passes **kwargs back to the format_callback as kwargs. + + """ + roots = [] + for key in self.seen.keys(): + if key not in self.parents: + roots.extend(self._nested(key, format_callback, **kwargs)) + return roots + + +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 { + 'verbose_name': force_unicode(opts.verbose_name), + 'verbose_name_plural': force_unicode(opts.verbose_name_plural) + } + +def model_ngettext(obj, n=None): + """ + Return the appropriate `verbose_name` or `verbose_name_plural` value for + `obj` depending on the count `n`. + + `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) + singular, plural = d["verbose_name"], d["verbose_name_plural"] + return ungettext(singular, plural, n or 0) + +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 + value = getattr(obj, name) + return f, attr, value + +def label_for_field(name, model, model_admin=None, return_attr=False): + attr = None + try: + label = model._meta.get_field_by_name(name)[0].verbose_name + except models.FieldDoesNotExist: + if name == "__unicode__": + label = force_unicode(model._meta.verbose_name) + elif name == "__str__": + label = smart_str(model._meta.verbose_name) + else: + 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: + message += " or %s" % (model_admin.__name__,) + raise AttributeError(message) + + if hasattr(attr, "short_description"): + label = attr.short_description + elif callable(attr): + if attr.__name__ == "": + label = "--" + else: + label = pretty_name(attr.__name__) + else: + label = pretty_name(name) + if return_attr: + return (label, attr) + else: + return label + + +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 + + if field.flatchoices: + return dict(field.flatchoices).get(value, EMPTY_CHANGELIST_VALUE) + # 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) + elif value is None: + return EMPTY_CHANGELIST_VALUE + elif isinstance(field, models.DateField) or isinstance(field, models.TimeField): + 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: + return smart_unicode(value)