App Engine Python SDK version 1.9.12
[gae.git] / python / lib / django-1.2 / django / contrib / admin / util.py
blob792d0b689960ee48541b0d4daccbf96e5e954f0d
1 from django.core.exceptions import ObjectDoesNotExist
2 from django.db import models
3 from django.db.models.related import RelatedObject
4 from django.forms.forms import pretty_name
5 from django.utils import formats
6 from django.utils.html import escape
7 from django.utils.safestring import mark_safe
8 from django.utils.text import capfirst
9 from django.utils.encoding import force_unicode, smart_unicode, smart_str
10 from django.utils.translation import ungettext, ugettext as _
11 from django.core.urlresolvers import reverse, NoReverseMatch
12 from django.utils.datastructures import SortedDict
14 def quote(s):
15 """
16 Ensure that primary key values do not confuse the admin URLs by escaping
17 any '/', '_' and ':' characters. Similar to urllib.quote, except that the
18 quoting is slightly different so that it doesn't get automatically
19 unquoted by the Web browser.
20 """
21 if not isinstance(s, basestring):
22 return s
23 res = list(s)
24 for i in range(len(res)):
25 c = res[i]
26 if c in """:/_#?;@&=+$,"<>%\\""":
27 res[i] = '_%02X' % ord(c)
28 return ''.join(res)
30 def unquote(s):
31 """
32 Undo the effects of quote(). Based heavily on urllib.unquote().
33 """
34 mychr = chr
35 myatoi = int
36 list = s.split('_')
37 res = [list[0]]
38 myappend = res.append
39 del list[0]
40 for item in list:
41 if item[1:2]:
42 try:
43 myappend(mychr(myatoi(item[:2], 16)) + item[2:])
44 except ValueError:
45 myappend('_' + item)
46 else:
47 myappend('_' + item)
48 return "".join(res)
50 def flatten_fieldsets(fieldsets):
51 """Returns a list of field names from an admin fieldsets structure."""
52 field_names = []
53 for name, opts in fieldsets:
54 for field in opts['fields']:
55 # type checking feels dirty, but it seems like the best way here
56 if type(field) == tuple:
57 field_names.extend(field)
58 else:
59 field_names.append(field)
60 return field_names
62 def _format_callback(obj, user, admin_site, levels_to_root, perms_needed):
63 has_admin = obj.__class__ in admin_site._registry
64 opts = obj._meta
65 try:
66 admin_url = reverse('%s:%s_%s_change'
67 % (admin_site.name,
68 opts.app_label,
69 opts.object_name.lower()),
70 None, (quote(obj._get_pk_val()),))
71 except NoReverseMatch:
72 admin_url = '%s%s/%s/%s/' % ('../'*levels_to_root,
73 opts.app_label,
74 opts.object_name.lower(),
75 quote(obj._get_pk_val()))
76 if has_admin:
77 p = '%s.%s' % (opts.app_label,
78 opts.get_delete_permission())
79 if not user.has_perm(p):
80 perms_needed.add(opts.verbose_name)
81 # Display a link to the admin page.
82 return mark_safe(u'%s: <a href="%s">%s</a>' %
83 (escape(capfirst(opts.verbose_name)),
84 admin_url,
85 escape(obj)))
86 else:
87 # Don't display link to edit, because it either has no
88 # admin or is edited inline.
89 return u'%s: %s' % (capfirst(opts.verbose_name),
90 force_unicode(obj))
92 def get_deleted_objects(objs, opts, user, admin_site, levels_to_root=4):
93 """
94 Find all objects related to ``objs`` that should also be
95 deleted. ``objs`` should be an iterable of objects.
97 Returns a nested list of strings suitable for display in the
98 template with the ``unordered_list`` filter.
100 `levels_to_root` defines the number of directories (../) to reach
101 the admin root path. In a change_view this is 4, in a change_list
102 view 2.
104 This is for backwards compatibility since the options.delete_selected
105 method uses this function also from a change_list view.
106 This will not be used if we can reverse the URL.
108 collector = NestedObjects()
109 for obj in objs:
110 # TODO using a private model API!
111 obj._collect_sub_objects(collector)
113 perms_needed = set()
115 to_delete = collector.nested(_format_callback,
116 user=user,
117 admin_site=admin_site,
118 levels_to_root=levels_to_root,
119 perms_needed=perms_needed)
121 return to_delete, perms_needed
124 class NestedObjects(object):
126 A directed acyclic graph collection that exposes the add() API
127 expected by Model._collect_sub_objects and can present its data as
128 a nested list of objects.
131 def __init__(self):
132 # Use object keys of the form (model, pk) because actual model
133 # objects may not be unique
135 # maps object key to list of child keys
136 self.children = SortedDict()
138 # maps object key to parent key
139 self.parents = SortedDict()
141 # maps object key to actual object
142 self.seen = SortedDict()
144 def add(self, model, pk, obj,
145 parent_model=None, parent_obj=None, nullable=False):
147 Add item ``obj`` to the graph. Returns True (and does nothing)
148 if the item has been seen already.
150 The ``parent_obj`` argument must already exist in the graph; if
151 not, it's ignored (but ``obj`` is still added with no
152 parent). In any case, Model._collect_sub_objects (for whom
153 this API exists) will never pass a parent that hasn't already
154 been added itself.
156 These restrictions in combination ensure the graph will remain
157 acyclic (but can have multiple roots).
159 ``model``, ``pk``, and ``parent_model`` arguments are ignored
160 in favor of the appropriate lookups on ``obj`` and
161 ``parent_obj``; unlike CollectedObjects, we can't maintain
162 independence from the knowledge that we're operating on model
163 instances, and we don't want to allow for inconsistency.
165 ``nullable`` arg is ignored: it doesn't affect how the tree of
166 collected objects should be nested for display.
168 model, pk = type(obj), obj._get_pk_val()
170 # auto-created M2M models don't interest us
171 if model._meta.auto_created:
172 return True
174 key = model, pk
176 if key in self.seen:
177 return True
178 self.seen.setdefault(key, obj)
180 if parent_obj is not None:
181 parent_model, parent_pk = (type(parent_obj),
182 parent_obj._get_pk_val())
183 parent_key = (parent_model, parent_pk)
184 if parent_key in self.seen:
185 self.children.setdefault(parent_key, list()).append(key)
186 self.parents.setdefault(key, parent_key)
188 def _nested(self, key, format_callback=None, **kwargs):
189 obj = self.seen[key]
190 if format_callback:
191 ret = [format_callback(obj, **kwargs)]
192 else:
193 ret = [obj]
195 children = []
196 for child in self.children.get(key, ()):
197 children.extend(self._nested(child, format_callback, **kwargs))
198 if children:
199 ret.append(children)
201 return ret
203 def nested(self, format_callback=None, **kwargs):
205 Return the graph as a nested list.
207 Passes **kwargs back to the format_callback as kwargs.
210 roots = []
211 for key in self.seen.keys():
212 if key not in self.parents:
213 roots.extend(self._nested(key, format_callback, **kwargs))
214 return roots
217 def model_format_dict(obj):
219 Return a `dict` with keys 'verbose_name' and 'verbose_name_plural',
220 typically for use with string formatting.
222 `obj` may be a `Model` instance, `Model` subclass, or `QuerySet` instance.
225 if isinstance(obj, (models.Model, models.base.ModelBase)):
226 opts = obj._meta
227 elif isinstance(obj, models.query.QuerySet):
228 opts = obj.model._meta
229 else:
230 opts = obj
231 return {
232 'verbose_name': force_unicode(opts.verbose_name),
233 'verbose_name_plural': force_unicode(opts.verbose_name_plural)
236 def model_ngettext(obj, n=None):
238 Return the appropriate `verbose_name` or `verbose_name_plural` value for
239 `obj` depending on the count `n`.
241 `obj` may be a `Model` instance, `Model` subclass, or `QuerySet` instance.
242 If `obj` is a `QuerySet` instance, `n` is optional and the length of the
243 `QuerySet` is used.
246 if isinstance(obj, models.query.QuerySet):
247 if n is None:
248 n = obj.count()
249 obj = obj.model
250 d = model_format_dict(obj)
251 singular, plural = d["verbose_name"], d["verbose_name_plural"]
252 return ungettext(singular, plural, n or 0)
254 def lookup_field(name, obj, model_admin=None):
255 opts = obj._meta
256 try:
257 f = opts.get_field(name)
258 except models.FieldDoesNotExist:
259 # For non-field values, the value is either a method, property or
260 # returned via a callable.
261 if callable(name):
262 attr = name
263 value = attr(obj)
264 elif (model_admin is not None and hasattr(model_admin, name) and
265 not name == '__str__' and not name == '__unicode__'):
266 attr = getattr(model_admin, name)
267 value = attr(obj)
268 else:
269 attr = getattr(obj, name)
270 if callable(attr):
271 value = attr()
272 else:
273 value = attr
274 f = None
275 else:
276 attr = None
277 value = getattr(obj, name)
278 return f, attr, value
280 def label_for_field(name, model, model_admin=None, return_attr=False):
281 attr = None
282 try:
283 field = model._meta.get_field_by_name(name)[0]
284 if isinstance(field, RelatedObject):
285 label = field.opts.verbose_name
286 else:
287 label = field.verbose_name
288 except models.FieldDoesNotExist:
289 if name == "__unicode__":
290 label = force_unicode(model._meta.verbose_name)
291 elif name == "__str__":
292 label = smart_str(model._meta.verbose_name)
293 else:
294 if callable(name):
295 attr = name
296 elif model_admin is not None and hasattr(model_admin, name):
297 attr = getattr(model_admin, name)
298 elif hasattr(model, name):
299 attr = getattr(model, name)
300 else:
301 message = "Unable to lookup '%s' on %s" % (name, model._meta.object_name)
302 if model_admin:
303 message += " or %s" % (model_admin.__name__,)
304 raise AttributeError(message)
306 if hasattr(attr, "short_description"):
307 label = attr.short_description
308 elif callable(attr):
309 if attr.__name__ == "<lambda>":
310 label = "--"
311 else:
312 label = pretty_name(attr.__name__)
313 else:
314 label = pretty_name(name)
315 if return_attr:
316 return (label, attr)
317 else:
318 return label
321 def display_for_field(value, field):
322 from django.contrib.admin.templatetags.admin_list import _boolean_icon
323 from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
325 if field.flatchoices:
326 return dict(field.flatchoices).get(value, EMPTY_CHANGELIST_VALUE)
327 # NullBooleanField needs special-case null-handling, so it comes
328 # before the general null test.
329 elif isinstance(field, models.BooleanField) or isinstance(field, models.NullBooleanField):
330 return _boolean_icon(value)
331 elif value is None:
332 return EMPTY_CHANGELIST_VALUE
333 elif isinstance(field, models.DateField) or isinstance(field, models.TimeField):
334 return formats.localize(value)
335 elif isinstance(field, models.DecimalField):
336 return formats.number_format(value, field.decimal_places)
337 elif isinstance(field, models.FloatField):
338 return formats.number_format(value)
339 else:
340 return smart_unicode(value)