/packages/Django/django/contrib/admin/util.py

https://github.com/lmorchard/home-snippets-server-lib · Python · 335 lines · 256 code · 24 blank · 55 comment · 27 complexity · 7d1e5d641f73a3ebf70e588bf7bf3ab5 MD5 · raw file

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