You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

494 lines
17 KiB

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