utils.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. from __future__ import unicode_literals
  2. import datetime
  3. import decimal
  4. from django.contrib.auth import get_permission_codename
  5. from django.db import models
  6. from django.db.models.constants import LOOKUP_SEP
  7. from django.db.models.deletion import Collector
  8. from django.forms.forms import pretty_name
  9. from django.utils import formats
  10. from django.utils.html import format_html
  11. from django.utils.text import capfirst
  12. from django.utils import timezone
  13. from django.utils.encoding import force_str, force_text, smart_text
  14. from django.utils import six
  15. from django.utils.translation import ungettext
  16. from django.core.urlresolvers import reverse, NoReverseMatch
  17. def lookup_needs_distinct(opts, lookup_path):
  18. """
  19. Returns True if 'distinct()' should be used to query the given lookup path.
  20. """
  21. field_name = lookup_path.split('__', 1)[0]
  22. field = opts.get_field_by_name(field_name)[0]
  23. if hasattr(field, 'get_path_info') and any(path.m2m for path in field.get_path_info()):
  24. return True
  25. return False
  26. def prepare_lookup_value(key, value):
  27. """
  28. Returns a lookup value prepared to be used in queryset filtering.
  29. """
  30. # if key ends with __in, split parameter into separate values
  31. if key.endswith('__in'):
  32. value = value.split(',')
  33. # if key ends with __isnull, special case '' and the string literals 'false' and '0'
  34. if key.endswith('__isnull'):
  35. if value.lower() in ('', 'false', '0'):
  36. value = False
  37. else:
  38. value = True
  39. return value
  40. def quote(s):
  41. """
  42. Ensure that primary key values do not confuse the admin URLs by escaping
  43. any '/', '_' and ':' and similarly problematic characters.
  44. Similar to urllib.quote, except that the quoting is slightly different so
  45. that it doesn't get automatically unquoted by the Web browser.
  46. """
  47. if not isinstance(s, six.string_types):
  48. return s
  49. res = list(s)
  50. for i in range(len(res)):
  51. c = res[i]
  52. if c in """:/_#?;@&=+$,"<>%\\""":
  53. res[i] = '_%02X' % ord(c)
  54. return ''.join(res)
  55. def unquote(s):
  56. """
  57. Undo the effects of quote(). Based heavily on urllib.unquote().
  58. """
  59. mychr = chr
  60. myatoi = int
  61. list = s.split('_')
  62. res = [list[0]]
  63. myappend = res.append
  64. del list[0]
  65. for item in list:
  66. if item[1:2]:
  67. try:
  68. myappend(mychr(myatoi(item[:2], 16)) + item[2:])
  69. except ValueError:
  70. myappend('_' + item)
  71. else:
  72. myappend('_' + item)
  73. return "".join(res)
  74. def flatten(fields):
  75. """Returns a list which is a single level of flattening of the
  76. original list."""
  77. flat = []
  78. for field in fields:
  79. if isinstance(field, (list, tuple)):
  80. flat.extend(field)
  81. else:
  82. flat.append(field)
  83. return flat
  84. def flatten_fieldsets(fieldsets):
  85. """Returns a list of field names from an admin fieldsets structure."""
  86. field_names = []
  87. for name, opts in fieldsets:
  88. field_names.extend(
  89. flatten(opts['fields'])
  90. )
  91. return field_names
  92. def get_deleted_objects(objs, opts, user, admin_site, using):
  93. """
  94. Find all objects related to ``objs`` that should also be deleted. ``objs``
  95. must be a homogeneous iterable of objects (e.g. a QuerySet).
  96. Returns a nested list of strings suitable for display in the
  97. template with the ``unordered_list`` filter.
  98. """
  99. collector = NestedObjects(using=using)
  100. collector.collect(objs)
  101. perms_needed = set()
  102. def format_callback(obj):
  103. has_admin = obj.__class__ in admin_site._registry
  104. opts = obj._meta
  105. no_edit_link = '%s: %s' % (capfirst(opts.verbose_name),
  106. force_text(obj))
  107. if has_admin:
  108. try:
  109. admin_url = reverse('%s:%s_%s_change'
  110. % (admin_site.name,
  111. opts.app_label,
  112. opts.model_name),
  113. None, (quote(obj._get_pk_val()),))
  114. except NoReverseMatch:
  115. # Change url doesn't exist -- don't display link to edit
  116. return no_edit_link
  117. p = '%s.%s' % (opts.app_label,
  118. get_permission_codename('delete', opts))
  119. if not user.has_perm(p):
  120. perms_needed.add(opts.verbose_name)
  121. # Display a link to the admin page.
  122. return format_html('{0}: <a href="{1}">{2}</a>',
  123. capfirst(opts.verbose_name),
  124. admin_url,
  125. obj)
  126. else:
  127. # Don't display link to edit, because it either has no
  128. # admin or is edited inline.
  129. return no_edit_link
  130. to_delete = collector.nested(format_callback)
  131. protected = [format_callback(obj) for obj in collector.protected]
  132. return to_delete, perms_needed, protected
  133. class NestedObjects(Collector):
  134. def __init__(self, *args, **kwargs):
  135. super(NestedObjects, self).__init__(*args, **kwargs)
  136. self.edges = {} # {from_instance: [to_instances]}
  137. self.protected = set()
  138. def add_edge(self, source, target):
  139. self.edges.setdefault(source, []).append(target)
  140. def collect(self, objs, source=None, source_attr=None, **kwargs):
  141. for obj in objs:
  142. if source_attr and not source_attr.endswith('+'):
  143. related_name = source_attr % {
  144. 'class': source._meta.model_name,
  145. 'app_label': source._meta.app_label,
  146. }
  147. self.add_edge(getattr(obj, related_name), obj)
  148. else:
  149. self.add_edge(None, obj)
  150. try:
  151. return super(NestedObjects, self).collect(objs, source_attr=source_attr, **kwargs)
  152. except models.ProtectedError as e:
  153. self.protected.update(e.protected_objects)
  154. def related_objects(self, related, objs):
  155. qs = super(NestedObjects, self).related_objects(related, objs)
  156. return qs.select_related(related.field.name)
  157. def _nested(self, obj, seen, format_callback):
  158. if obj in seen:
  159. return []
  160. seen.add(obj)
  161. children = []
  162. for child in self.edges.get(obj, ()):
  163. children.extend(self._nested(child, seen, format_callback))
  164. if format_callback:
  165. ret = [format_callback(obj)]
  166. else:
  167. ret = [obj]
  168. if children:
  169. ret.append(children)
  170. return ret
  171. def nested(self, format_callback=None):
  172. """
  173. Return the graph as a nested list.
  174. """
  175. seen = set()
  176. roots = []
  177. for root in self.edges.get(None, ()):
  178. roots.extend(self._nested(root, seen, format_callback))
  179. return roots
  180. def can_fast_delete(self, *args, **kwargs):
  181. """
  182. We always want to load the objects into memory so that we can display
  183. them to the user in confirm page.
  184. """
  185. return False
  186. def model_format_dict(obj):
  187. """
  188. Return a `dict` with keys 'verbose_name' and 'verbose_name_plural',
  189. typically for use with string formatting.
  190. `obj` may be a `Model` instance, `Model` subclass, or `QuerySet` instance.
  191. """
  192. if isinstance(obj, (models.Model, models.base.ModelBase)):
  193. opts = obj._meta
  194. elif isinstance(obj, models.query.QuerySet):
  195. opts = obj.model._meta
  196. else:
  197. opts = obj
  198. return {
  199. 'verbose_name': force_text(opts.verbose_name),
  200. 'verbose_name_plural': force_text(opts.verbose_name_plural)
  201. }
  202. def model_ngettext(obj, n=None):
  203. """
  204. Return the appropriate `verbose_name` or `verbose_name_plural` value for
  205. `obj` depending on the count `n`.
  206. `obj` may be a `Model` instance, `Model` subclass, or `QuerySet` instance.
  207. If `obj` is a `QuerySet` instance, `n` is optional and the length of the
  208. `QuerySet` is used.
  209. """
  210. if isinstance(obj, models.query.QuerySet):
  211. if n is None:
  212. n = obj.count()
  213. obj = obj.model
  214. d = model_format_dict(obj)
  215. singular, plural = d["verbose_name"], d["verbose_name_plural"]
  216. return ungettext(singular, plural, n or 0)
  217. def lookup_field(name, obj, model_admin=None):
  218. opts = obj._meta
  219. try:
  220. f = opts.get_field(name)
  221. except models.FieldDoesNotExist:
  222. # For non-field values, the value is either a method, property or
  223. # returned via a callable.
  224. if callable(name):
  225. attr = name
  226. value = attr(obj)
  227. elif (model_admin is not None and
  228. hasattr(model_admin, name) and
  229. not name == '__str__' and
  230. not name == '__unicode__'):
  231. attr = getattr(model_admin, name)
  232. value = attr(obj)
  233. else:
  234. attr = getattr(obj, name)
  235. if callable(attr):
  236. value = attr()
  237. else:
  238. value = attr
  239. f = None
  240. else:
  241. attr = None
  242. value = getattr(obj, name)
  243. return f, attr, value
  244. def label_for_field(name, model, model_admin=None, return_attr=False):
  245. """
  246. Returns a sensible label for a field name. The name can be a callable,
  247. property (but not created with @property decorator) or the name of an
  248. object's attribute, as well as a genuine fields. If return_attr is
  249. True, the resolved attribute (which could be a callable) is also returned.
  250. This will be None if (and only if) the name refers to a field.
  251. """
  252. attr = None
  253. try:
  254. field = model._meta.get_field_by_name(name)[0]
  255. try:
  256. label = field.verbose_name
  257. except AttributeError:
  258. # field is likely a RelatedObject
  259. label = field.opts.verbose_name
  260. except models.FieldDoesNotExist:
  261. if name == "__unicode__":
  262. label = force_text(model._meta.verbose_name)
  263. attr = six.text_type
  264. elif name == "__str__":
  265. label = force_str(model._meta.verbose_name)
  266. attr = bytes
  267. else:
  268. if callable(name):
  269. attr = name
  270. elif model_admin is not None and hasattr(model_admin, name):
  271. attr = getattr(model_admin, name)
  272. elif hasattr(model, name):
  273. attr = getattr(model, name)
  274. else:
  275. message = "Unable to lookup '%s' on %s" % (name, model._meta.object_name)
  276. if model_admin:
  277. message += " or %s" % (model_admin.__class__.__name__,)
  278. raise AttributeError(message)
  279. if hasattr(attr, "short_description"):
  280. label = attr.short_description
  281. elif (isinstance(attr, property) and
  282. hasattr(attr, "fget") and
  283. hasattr(attr.fget, "short_description")):
  284. label = attr.fget.short_description
  285. elif callable(attr):
  286. if attr.__name__ == "<lambda>":
  287. label = "--"
  288. else:
  289. label = pretty_name(attr.__name__)
  290. else:
  291. label = pretty_name(name)
  292. if return_attr:
  293. return (label, attr)
  294. else:
  295. return label
  296. def help_text_for_field(name, model):
  297. help_text = ""
  298. try:
  299. field_data = model._meta.get_field_by_name(name)
  300. except models.FieldDoesNotExist:
  301. pass
  302. else:
  303. field = field_data[0]
  304. if hasattr(field, 'help_text'):
  305. help_text = field.help_text
  306. return smart_text(help_text)
  307. def display_for_field(value, field):
  308. from django.contrib.admin.templatetags.admin_list import _boolean_icon
  309. from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
  310. if field.flatchoices:
  311. return dict(field.flatchoices).get(value, EMPTY_CHANGELIST_VALUE)
  312. # NullBooleanField needs special-case null-handling, so it comes
  313. # before the general null test.
  314. elif isinstance(field, models.BooleanField) or isinstance(field, models.NullBooleanField):
  315. return _boolean_icon(value)
  316. elif value is None:
  317. return EMPTY_CHANGELIST_VALUE
  318. elif isinstance(field, models.DateTimeField):
  319. return formats.localize(timezone.template_localtime(value))
  320. elif isinstance(field, (models.DateField, models.TimeField)):
  321. return formats.localize(value)
  322. elif isinstance(field, models.DecimalField):
  323. return formats.number_format(value, field.decimal_places)
  324. elif isinstance(field, models.FloatField):
  325. return formats.number_format(value)
  326. else:
  327. return smart_text(value)
  328. def display_for_value(value, boolean=False):
  329. from django.contrib.admin.templatetags.admin_list import _boolean_icon
  330. from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
  331. if boolean:
  332. return _boolean_icon(value)
  333. elif value is None:
  334. return EMPTY_CHANGELIST_VALUE
  335. elif isinstance(value, datetime.datetime):
  336. return formats.localize(timezone.template_localtime(value))
  337. elif isinstance(value, (datetime.date, datetime.time)):
  338. return formats.localize(value)
  339. elif isinstance(value, six.integer_types + (decimal.Decimal, float)):
  340. return formats.number_format(value)
  341. else:
  342. return smart_text(value)
  343. class NotRelationField(Exception):
  344. pass
  345. def get_model_from_relation(field):
  346. if hasattr(field, 'get_path_info'):
  347. return field.get_path_info()[-1].to_opts.model
  348. else:
  349. raise NotRelationField
  350. def reverse_field_path(model, path):
  351. """ Create a reversed field path.
  352. E.g. Given (Order, "user__groups"),
  353. return (Group, "user__order").
  354. Final field must be a related model, not a data field.
  355. """
  356. reversed_path = []
  357. parent = model
  358. pieces = path.split(LOOKUP_SEP)
  359. for piece in pieces:
  360. field, model, direct, m2m = parent._meta.get_field_by_name(piece)
  361. # skip trailing data field if extant:
  362. if len(reversed_path) == len(pieces) - 1: # final iteration
  363. try:
  364. get_model_from_relation(field)
  365. except NotRelationField:
  366. break
  367. if direct:
  368. related_name = field.related_query_name()
  369. parent = field.rel.to
  370. else:
  371. related_name = field.field.name
  372. parent = field.model
  373. reversed_path.insert(0, related_name)
  374. return (parent, LOOKUP_SEP.join(reversed_path))
  375. def get_fields_from_path(model, path):
  376. """ Return list of Fields given path relative to model.
  377. e.g. (ModelX, "user__groups__name") -> [
  378. <django.db.models.fields.related.ForeignKey object at 0x...>,
  379. <django.db.models.fields.related.ManyToManyField object at 0x...>,
  380. <django.db.models.fields.CharField object at 0x...>,
  381. ]
  382. """
  383. pieces = path.split(LOOKUP_SEP)
  384. fields = []
  385. for piece in pieces:
  386. if fields:
  387. parent = get_model_from_relation(fields[-1])
  388. else:
  389. parent = model
  390. fields.append(parent._meta.get_field_by_name(piece)[0])
  391. return fields
  392. def remove_trailing_data_field(fields):
  393. """ Discard trailing non-relation field if extant. """
  394. try:
  395. get_model_from_relation(fields[-1])
  396. except NotRelationField:
  397. fields = fields[:-1]
  398. return fields
  399. def get_limit_choices_to_from_path(model, path):
  400. """ Return Q object for limiting choices if applicable.
  401. If final model in path is linked via a ForeignKey or ManyToManyField which
  402. has a ``limit_choices_to`` attribute, return it as a Q object.
  403. """
  404. fields = get_fields_from_path(model, path)
  405. fields = remove_trailing_data_field(fields)
  406. get_limit_choices_to = (
  407. fields and hasattr(fields[-1], 'rel') and
  408. getattr(fields[-1].rel, 'get_limit_choices_to', None))
  409. if not get_limit_choices_to:
  410. return models.Q() # empty Q
  411. limit_choices_to = get_limit_choices_to()
  412. if isinstance(limit_choices_to, models.Q):
  413. return limit_choices_to # already a Q
  414. else:
  415. return models.Q(**limit_choices_to) # convert dict to Q