/django/contrib/admin/util.py

https://code.google.com/p/mango-py/ · Python · 387 lines · 336 code · 22 blank · 29 comment · 24 complexity · 001d2f6feb873e6cc09c50ea85f413e0 MD5 · raw file

  1. from django.db import models
  2. from django.db.models.sql.constants import LOOKUP_SEP
  3. from django.db.models.deletion import Collector
  4. from django.db.models.related import RelatedObject
  5. from django.forms.forms import pretty_name
  6. from django.utils import formats
  7. from django.utils.html import escape
  8. from django.utils.safestring import mark_safe
  9. from django.utils.text import capfirst
  10. from django.utils.encoding import force_unicode, smart_unicode, smart_str
  11. from django.utils.translation import ungettext
  12. from django.core.urlresolvers import reverse
  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. def unquote(s):
  29. """
  30. Undo the effects of quote(). Based heavily on urllib.unquote().
  31. """
  32. mychr = chr
  33. myatoi = int
  34. list = s.split('_')
  35. res = [list[0]]
  36. myappend = res.append
  37. del list[0]
  38. for item in list:
  39. if item[1:2]:
  40. try:
  41. myappend(mychr(myatoi(item[:2], 16)) + item[2:])
  42. except ValueError:
  43. myappend('_' + item)
  44. else:
  45. myappend('_' + item)
  46. return "".join(res)
  47. def flatten_fieldsets(fieldsets):
  48. """Returns a list of field names from an admin fieldsets structure."""
  49. field_names = []
  50. for name, opts in fieldsets:
  51. for field in opts['fields']:
  52. # type checking feels dirty, but it seems like the best way here
  53. if type(field) == tuple:
  54. field_names.extend(field)
  55. else:
  56. field_names.append(field)
  57. return field_names
  58. def get_deleted_objects(objs, opts, user, admin_site, using):
  59. """
  60. Find all objects related to ``objs`` that should also be deleted. ``objs``
  61. must be a homogenous iterable of objects (e.g. a QuerySet).
  62. Returns a nested list of strings suitable for display in the
  63. template with the ``unordered_list`` filter.
  64. """
  65. collector = NestedObjects(using=using)
  66. collector.collect(objs)
  67. perms_needed = set()
  68. def format_callback(obj):
  69. has_admin = obj.__class__ in admin_site._registry
  70. opts = obj._meta
  71. if has_admin:
  72. admin_url = reverse('%s:%s_%s_change'
  73. % (admin_site.name,
  74. opts.app_label,
  75. opts.object_name.lower()),
  76. None, (quote(obj._get_pk_val()),))
  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))
  91. to_delete = collector.nested(format_callback)
  92. protected = [format_callback(obj) for obj in collector.protected]
  93. return to_delete, perms_needed, protected
  94. class NestedObjects(Collector):
  95. def __init__(self, *args, **kwargs):
  96. super(NestedObjects, self).__init__(*args, **kwargs)
  97. self.edges = {} # {from_instance: [to_instances]}
  98. self.protected = set()
  99. def add_edge(self, source, target):
  100. self.edges.setdefault(source, []).append(target)
  101. def collect(self, objs, source_attr=None, **kwargs):
  102. for obj in objs:
  103. if source_attr:
  104. self.add_edge(getattr(obj, source_attr), obj)
  105. else:
  106. self.add_edge(None, obj)
  107. try:
  108. return super(NestedObjects, self).collect(objs, source_attr=source_attr, **kwargs)
  109. except models.ProtectedError, e:
  110. self.protected.update(e.protected_objects)
  111. def related_objects(self, related, objs):
  112. qs = super(NestedObjects, self).related_objects(related, objs)
  113. return qs.select_related(related.field.name)
  114. def _nested(self, obj, seen, format_callback):
  115. if obj in seen:
  116. return []
  117. seen.add(obj)
  118. children = []
  119. for child in self.edges.get(obj, ()):
  120. children.extend(self._nested(child, seen, format_callback))
  121. if format_callback:
  122. ret = [format_callback(obj)]
  123. else:
  124. ret = [obj]
  125. if children:
  126. ret.append(children)
  127. return ret
  128. def nested(self, format_callback=None):
  129. """
  130. Return the graph as a nested list.
  131. """
  132. seen = set()
  133. roots = []
  134. for root in self.edges.get(None, ()):
  135. roots.extend(self._nested(root, seen, format_callback))
  136. return roots
  137. def model_format_dict(obj):
  138. """
  139. Return a `dict` with keys 'verbose_name' and 'verbose_name_plural',
  140. typically for use with string formatting.
  141. `obj` may be a `Model` instance, `Model` subclass, or `QuerySet` instance.
  142. """
  143. if isinstance(obj, (models.Model, models.base.ModelBase)):
  144. opts = obj._meta
  145. elif isinstance(obj, models.query.QuerySet):
  146. opts = obj.model._meta
  147. else:
  148. opts = obj
  149. return {
  150. 'verbose_name': force_unicode(opts.verbose_name),
  151. 'verbose_name_plural': force_unicode(opts.verbose_name_plural)
  152. }
  153. def model_ngettext(obj, n=None):
  154. """
  155. Return the appropriate `verbose_name` or `verbose_name_plural` value for
  156. `obj` depending on the count `n`.
  157. `obj` may be a `Model` instance, `Model` subclass, or `QuerySet` instance.
  158. If `obj` is a `QuerySet` instance, `n` is optional and the length of the
  159. `QuerySet` is used.
  160. """
  161. if isinstance(obj, models.query.QuerySet):
  162. if n is None:
  163. n = obj.count()
  164. obj = obj.model
  165. d = model_format_dict(obj)
  166. singular, plural = d["verbose_name"], d["verbose_name_plural"]
  167. return ungettext(singular, plural, n or 0)
  168. def lookup_field(name, obj, model_admin=None):
  169. opts = obj._meta
  170. try:
  171. f = opts.get_field(name)
  172. except models.FieldDoesNotExist:
  173. # For non-field values, the value is either a method, property or
  174. # returned via a callable.
  175. if callable(name):
  176. attr = name
  177. value = attr(obj)
  178. elif (model_admin is not None and hasattr(model_admin, name) and
  179. not name == '__str__' and not name == '__unicode__'):
  180. attr = getattr(model_admin, name)
  181. value = attr(obj)
  182. else:
  183. attr = getattr(obj, name)
  184. if callable(attr):
  185. value = attr()
  186. else:
  187. value = attr
  188. f = None
  189. else:
  190. attr = None
  191. value = getattr(obj, name)
  192. return f, attr, value
  193. def label_for_field(name, model, model_admin=None, return_attr=False):
  194. attr = None
  195. try:
  196. field = model._meta.get_field_by_name(name)[0]
  197. if isinstance(field, RelatedObject):
  198. label = field.opts.verbose_name
  199. else:
  200. label = field.verbose_name
  201. except models.FieldDoesNotExist:
  202. if name == "__unicode__":
  203. label = force_unicode(model._meta.verbose_name)
  204. elif name == "__str__":
  205. label = smart_str(model._meta.verbose_name)
  206. else:
  207. if callable(name):
  208. attr = name
  209. elif model_admin is not None and hasattr(model_admin, name):
  210. attr = getattr(model_admin, name)
  211. elif hasattr(model, name):
  212. attr = getattr(model, name)
  213. else:
  214. message = "Unable to lookup '%s' on %s" % (name, model._meta.object_name)
  215. if model_admin:
  216. message += " or %s" % (model_admin.__class__.__name__,)
  217. raise AttributeError(message)
  218. if hasattr(attr, "short_description"):
  219. label = attr.short_description
  220. elif callable(attr):
  221. if attr.__name__ == "<lambda>":
  222. label = "--"
  223. else:
  224. label = pretty_name(attr.__name__)
  225. else:
  226. label = pretty_name(name)
  227. if return_attr:
  228. return (label, attr)
  229. else:
  230. return label
  231. def help_text_for_field(name, model):
  232. try:
  233. help_text = model._meta.get_field_by_name(name)[0].help_text
  234. except models.FieldDoesNotExist:
  235. help_text = ""
  236. return smart_unicode(help_text)
  237. def display_for_field(value, field):
  238. from django.contrib.admin.templatetags.admin_list import _boolean_icon
  239. from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
  240. if field.flatchoices:
  241. return dict(field.flatchoices).get(value, EMPTY_CHANGELIST_VALUE)
  242. # NullBooleanField needs special-case null-handling, so it comes
  243. # before the general null test.
  244. elif isinstance(field, models.BooleanField) or isinstance(field, models.NullBooleanField):
  245. return _boolean_icon(value)
  246. elif value is None:
  247. return EMPTY_CHANGELIST_VALUE
  248. elif isinstance(field, models.DateField) or isinstance(field, models.TimeField):
  249. return formats.localize(value)
  250. elif isinstance(field, models.DecimalField):
  251. return formats.number_format(value, field.decimal_places)
  252. elif isinstance(field, models.FloatField):
  253. return formats.number_format(value)
  254. else:
  255. return smart_unicode(value)
  256. class NotRelationField(Exception):
  257. pass
  258. def get_model_from_relation(field):
  259. if isinstance(field, models.related.RelatedObject):
  260. return field.model
  261. elif getattr(field, 'rel'): # or isinstance?
  262. return field.rel.to
  263. else:
  264. raise NotRelationField
  265. def reverse_field_path(model, path):
  266. """ Create a reversed field path.
  267. E.g. Given (Order, "user__groups"),
  268. return (Group, "user__order").
  269. Final field must be a related model, not a data field.
  270. """
  271. reversed_path = []
  272. parent = model
  273. pieces = path.split(LOOKUP_SEP)
  274. for piece in pieces:
  275. field, model, direct, m2m = parent._meta.get_field_by_name(piece)
  276. # skip trailing data field if extant:
  277. if len(reversed_path) == len(pieces)-1: # final iteration
  278. try:
  279. get_model_from_relation(field)
  280. except NotRelationField:
  281. break
  282. if direct:
  283. related_name = field.related_query_name()
  284. parent = field.rel.to
  285. else:
  286. related_name = field.field.name
  287. parent = field.model
  288. reversed_path.insert(0, related_name)
  289. return (parent, LOOKUP_SEP.join(reversed_path))
  290. def get_fields_from_path(model, path):
  291. """ Return list of Fields given path relative to model.
  292. e.g. (ModelX, "user__groups__name") -> [
  293. <django.db.models.fields.related.ForeignKey object at 0x...>,
  294. <django.db.models.fields.related.ManyToManyField object at 0x...>,
  295. <django.db.models.fields.CharField object at 0x...>,
  296. ]
  297. """
  298. pieces = path.split(LOOKUP_SEP)
  299. fields = []
  300. for piece in pieces:
  301. if fields:
  302. parent = get_model_from_relation(fields[-1])
  303. else:
  304. parent = model
  305. fields.append(parent._meta.get_field_by_name(piece)[0])
  306. return fields
  307. def remove_trailing_data_field(fields):
  308. """ Discard trailing non-relation field if extant. """
  309. try:
  310. get_model_from_relation(fields[-1])
  311. except NotRelationField:
  312. fields = fields[:-1]
  313. return fields
  314. def get_limit_choices_to_from_path(model, path):
  315. """ Return Q object for limiting choices if applicable.
  316. If final model in path is linked via a ForeignKey or ManyToManyField which
  317. has a `limit_choices_to` attribute, return it as a Q object.
  318. """
  319. fields = get_fields_from_path(model, path)
  320. fields = remove_trailing_data_field(fields)
  321. limit_choices_to = (
  322. fields and hasattr(fields[-1], 'rel') and
  323. getattr(fields[-1].rel, 'limit_choices_to', None))
  324. if not limit_choices_to:
  325. return models.Q() # empty Q
  326. elif isinstance(limit_choices_to, models.Q):
  327. return limit_choices_to # already a Q
  328. else:
  329. return models.Q(**limit_choices_to) # convert dict to Q