util.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. from __future__ import absolute_import
  2. import django
  3. from django.db import models
  4. from django.db.models.sql.query import LOOKUP_SEP
  5. from django.db.models.deletion import Collector
  6. from django.db.models.fields.related import ForeignObjectRel
  7. from django.forms.forms import pretty_name
  8. from django.utils import formats, six
  9. from django.utils.html import escape
  10. from django.utils.safestring import mark_safe
  11. from django.utils.text import capfirst
  12. from django.utils.encoding import force_text, smart_text, smart_str
  13. from django.utils.translation import ungettext
  14. from django.urls.base import reverse
  15. from django.conf import settings
  16. from django.forms import Media
  17. from django.utils.translation import get_language
  18. from django.contrib.admin.utils import label_for_field, help_text_for_field
  19. from django import VERSION as version
  20. import datetime
  21. import decimal
  22. if 'django.contrib.staticfiles' in settings.INSTALLED_APPS:
  23. from django.contrib.staticfiles.templatetags.staticfiles import static
  24. else:
  25. from django.templatetags.static import static
  26. try:
  27. import json
  28. except ImportError:
  29. from django.utils import simplejson as json
  30. try:
  31. from django.utils.timezone import template_localtime as tz_localtime
  32. except ImportError:
  33. from django.utils.timezone import localtime as tz_localtime
  34. def xstatic(*tags):
  35. from .vendors import vendors
  36. node = vendors
  37. fs = []
  38. lang = get_language()
  39. cls_str = str if six.PY3 else basestring
  40. for tag in tags:
  41. try:
  42. for p in tag.split('.'):
  43. node = node[p]
  44. except Exception as e:
  45. if tag.startswith('xadmin'):
  46. file_type = tag.split('.')[-1]
  47. if file_type in ('css', 'js'):
  48. node = "xadmin/%s/%s" % (file_type, tag)
  49. else:
  50. raise e
  51. else:
  52. raise e
  53. if isinstance(node, cls_str):
  54. files = node
  55. else:
  56. mode = 'dev'
  57. if not settings.DEBUG:
  58. mode = getattr(settings, 'STATIC_USE_CDN',
  59. False) and 'cdn' or 'production'
  60. if mode == 'cdn' and mode not in node:
  61. mode = 'production'
  62. if mode == 'production' and mode not in node:
  63. mode = 'dev'
  64. files = node[mode]
  65. files = type(files) in (list, tuple) and files or [files, ]
  66. fs.extend([f % {'lang': lang.replace('_', '-')} for f in files])
  67. return [f.startswith('http://') and f or static(f) for f in fs]
  68. def vendor(*tags):
  69. css = {'screen': []}
  70. js = []
  71. for tag in tags:
  72. file_type = tag.split('.')[-1]
  73. files = xstatic(tag)
  74. if file_type == 'js':
  75. js.extend(files)
  76. elif file_type == 'css':
  77. css['screen'] += files
  78. return Media(css=css, js=js)
  79. def lookup_needs_distinct(opts, lookup_path):
  80. """
  81. Returns True if 'distinct()' should be used to query the given lookup path.
  82. """
  83. field_name = lookup_path.split('__', 1)[0]
  84. field = opts.get_field(field_name)
  85. if ((hasattr(field, 'remote_field') and
  86. isinstance(field.remote_field, models.ManyToManyRel)) or
  87. (is_related_field(field) and
  88. not field.field.unique)):
  89. return True
  90. return False
  91. def prepare_lookup_value(key, value):
  92. """
  93. Returns a lookup value prepared to be used in queryset filtering.
  94. """
  95. # if key ends with __in, split parameter into separate values
  96. if key.endswith('__in'):
  97. value = value.split(',')
  98. # if key ends with __isnull, special case '' and false
  99. if key.endswith('__isnull') and type(value) == str:
  100. if value.lower() in ('', 'false'):
  101. value = False
  102. else:
  103. value = True
  104. return value
  105. def quote(s):
  106. """
  107. Ensure that primary key values do not confuse the admin URLs by escaping
  108. any '/', '_' and ':' characters. Similar to urllib.quote, except that the
  109. quoting is slightly different so that it doesn't get automatically
  110. unquoted by the Web browser.
  111. """
  112. cls_str = str if six.PY3 else basestring
  113. if not isinstance(s, cls_str):
  114. return s
  115. res = list(s)
  116. for i in range(len(res)):
  117. c = res[i]
  118. if c in """:/_#?;@&=+$,"<>%\\""":
  119. res[i] = '_%02X' % ord(c)
  120. return ''.join(res)
  121. def unquote(s):
  122. """
  123. Undo the effects of quote(). Based heavily on urllib.unquote().
  124. """
  125. cls_str = str if six.PY3 else basestring
  126. if not isinstance(s, cls_str):
  127. return s
  128. mychr = chr
  129. myatoi = int
  130. list = s.split('_')
  131. res = [list[0]]
  132. myappend = res.append
  133. del list[0]
  134. for item in list:
  135. if item[1:2]:
  136. try:
  137. myappend(mychr(myatoi(item[:2], 16)) + item[2:])
  138. except ValueError:
  139. myappend('_' + item)
  140. else:
  141. myappend('_' + item)
  142. return "".join(res)
  143. def flatten_fieldsets(fieldsets):
  144. """Returns a list of field names from an admin fieldsets structure."""
  145. field_names = []
  146. for name, opts in fieldsets:
  147. for field in opts['fields']:
  148. # type checking feels dirty, but it seems like the best way here
  149. if type(field) == tuple:
  150. field_names.extend(field)
  151. else:
  152. field_names.append(field)
  153. return field_names
  154. class NestedObjects(Collector):
  155. def __init__(self, *args, **kwargs):
  156. super(NestedObjects, self).__init__(*args, **kwargs)
  157. self.edges = {} # {from_instance: [to_instances]}
  158. self.protected = set()
  159. def add_edge(self, source, target):
  160. self.edges.setdefault(source, []).append(target)
  161. def collect(self, objs, source_attr=None, **kwargs):
  162. for obj in objs:
  163. if source_attr and hasattr(obj, source_attr):
  164. self.add_edge(getattr(obj, source_attr), obj)
  165. else:
  166. self.add_edge(None, obj)
  167. try:
  168. return super(NestedObjects, self).collect(objs, source_attr=source_attr, **kwargs)
  169. except models.ProtectedError as e:
  170. self.protected.update(e.protected_objects)
  171. def related_objects(self, related, objs):
  172. qs = super(NestedObjects, self).related_objects(related, objs)
  173. return qs.select_related(related.field.name)
  174. def _nested(self, obj, seen, format_callback):
  175. if obj in seen:
  176. return []
  177. seen.add(obj)
  178. children = []
  179. for child in self.edges.get(obj, ()):
  180. children.extend(self._nested(child, seen, format_callback))
  181. if format_callback:
  182. ret = [format_callback(obj)]
  183. else:
  184. ret = [obj]
  185. if children:
  186. ret.append(children)
  187. return ret
  188. def nested(self, format_callback=None):
  189. """
  190. Return the graph as a nested list.
  191. """
  192. seen = set()
  193. roots = []
  194. for root in self.edges.get(None, ()):
  195. roots.extend(self._nested(root, seen, format_callback))
  196. return roots
  197. def model_format_dict(obj):
  198. """
  199. Return a `dict` with keys 'verbose_name' and 'verbose_name_plural',
  200. typically for use with string formatting.
  201. `obj` may be a `Model` instance, `Model` subclass, or `QuerySet` instance.
  202. """
  203. if isinstance(obj, (models.Model, models.base.ModelBase)):
  204. opts = obj._meta
  205. elif isinstance(obj, models.query.QuerySet):
  206. opts = obj.model._meta
  207. else:
  208. opts = obj
  209. return {
  210. 'verbose_name': force_text(opts.verbose_name),
  211. 'verbose_name_plural': force_text(opts.verbose_name_plural)
  212. }
  213. def model_ngettext(obj, n=None):
  214. """
  215. Return the appropriate `verbose_name` or `verbose_name_plural` value for
  216. `obj` depending on the count `n`.
  217. `obj` may be a `Model` instance, `Model` subclass, or `QuerySet` instance.
  218. If `obj` is a `QuerySet` instance, `n` is optional and the length of the
  219. `QuerySet` is used.
  220. """
  221. if isinstance(obj, models.query.QuerySet):
  222. if n is None:
  223. n = obj.count()
  224. obj = obj.model
  225. d = model_format_dict(obj)
  226. singular, plural = d["verbose_name"], d["verbose_name_plural"]
  227. return ungettext(singular, plural, n or 0)
  228. def is_rel_field(name, model):
  229. if hasattr(name, 'split') and name.find("__") > 0:
  230. parts = name.split("__")
  231. if parts[0] in model._meta.get_all_field_names():
  232. return True
  233. return False
  234. def lookup_field(name, obj, model_admin=None):
  235. opts = obj._meta
  236. try:
  237. f = opts.get_field(name)
  238. except models.FieldDoesNotExist:
  239. # For non-field values, the value is either a method, property or
  240. # returned via a callable.
  241. if callable(name):
  242. attr = name
  243. value = attr(obj)
  244. elif (
  245. model_admin is not None
  246. and hasattr(model_admin, name)
  247. and name not in ('__str__', '__unicode__')
  248. ):
  249. attr = getattr(model_admin, name)
  250. value = attr(obj)
  251. else:
  252. if is_rel_field(name, obj):
  253. parts = name.split("__")
  254. rel_name, sub_rel_name = parts[0], "__".join(parts[1:])
  255. rel_obj = getattr(obj, rel_name)
  256. if rel_obj is not None:
  257. return lookup_field(sub_rel_name, rel_obj, model_admin)
  258. attr = getattr(obj, name)
  259. if callable(attr):
  260. value = attr()
  261. else:
  262. value = attr
  263. f = None
  264. else:
  265. attr = None
  266. value = getattr(obj, name)
  267. return f, attr, value
  268. def admin_urlname(value, arg):
  269. return 'xadmin:%s_%s_%s' % (value.app_label, value.model_name, arg)
  270. def boolean_icon(field_val):
  271. return mark_safe(u'<i class="%s" alt="%s"></i>' % (
  272. {True: 'fa fa-check-circle text-success', False: 'fa fa-times-circle text-error', None: 'fa fa-question-circle muted'}[field_val], field_val))
  273. def display_for_field(value, field):
  274. from xadmin.views.list 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.DateTimeField):
  284. return formats.localize(tz_localtime(value))
  285. elif isinstance(field, (models.DateField, models.TimeField)):
  286. return formats.localize(value)
  287. elif isinstance(field, models.DecimalField):
  288. return formats.number_format(value, field.decimal_places)
  289. elif isinstance(field, models.FloatField):
  290. return formats.number_format(value)
  291. elif isinstance(field.remote_field, models.ManyToManyRel):
  292. return ', '.join([smart_text(obj) for obj in value.all()])
  293. else:
  294. return smart_text(value)
  295. def display_for_value(value, boolean=False):
  296. from xadmin.views.list import EMPTY_CHANGELIST_VALUE
  297. if boolean:
  298. return boolean_icon(value)
  299. elif value is None:
  300. return EMPTY_CHANGELIST_VALUE
  301. elif isinstance(value, datetime.datetime):
  302. return formats.localize(tz_localtime(value))
  303. elif isinstance(value, (datetime.date, datetime.time)):
  304. return formats.localize(value)
  305. elif isinstance(value, (decimal.Decimal, float)):
  306. return formats.number_format(value)
  307. else:
  308. return smart_text(value)
  309. class NotRelationField(Exception):
  310. pass
  311. def get_model_from_relation(field):
  312. if field.related_model:
  313. return field.related_model
  314. elif is_related_field(field):
  315. return field.model
  316. elif getattr(field, 'remote_field'): # or isinstance?
  317. return field.remote_field.to
  318. else:
  319. raise NotRelationField
  320. def reverse_field_path(model, path):
  321. """ Create a reversed field path.
  322. E.g. Given (Order, "user__groups"),
  323. return (Group, "user__order").
  324. Final field must be a related model, not a data field.
  325. """
  326. reversed_path = []
  327. parent = model
  328. pieces = path.split(LOOKUP_SEP)
  329. for piece in pieces:
  330. field = parent._meta.get_field(piece)
  331. direct = not field.auto_created or field.concrete
  332. # skip trailing data field if extant:
  333. if len(reversed_path) == len(pieces) - 1: # final iteration
  334. try:
  335. get_model_from_relation(field)
  336. except NotRelationField:
  337. break
  338. if direct:
  339. related_name = field.related_query_name()
  340. parent = field.rel.to
  341. else:
  342. related_name = field.field.name
  343. parent = field.model
  344. reversed_path.insert(0, related_name)
  345. return (parent, LOOKUP_SEP.join(reversed_path))
  346. def get_fields_from_path(model, path):
  347. """ Return list of Fields given path relative to model.
  348. e.g. (ModelX, "user__groups__name") -> [
  349. <django.db.models.fields.related.ForeignKey object at 0x...>,
  350. <django.db.models.fields.related.ManyToManyField object at 0x...>,
  351. <django.db.models.fields.CharField object at 0x...>,
  352. ]
  353. """
  354. pieces = path.split(LOOKUP_SEP)
  355. fields = []
  356. for piece in pieces:
  357. if fields:
  358. parent = get_model_from_relation(fields[-1])
  359. else:
  360. parent = model
  361. fields.append(parent._meta.get_field(piece))
  362. return fields
  363. def remove_trailing_data_field(fields):
  364. """ Discard trailing non-relation field if extant. """
  365. try:
  366. get_model_from_relation(fields[-1])
  367. except NotRelationField:
  368. fields = fields[:-1]
  369. return fields
  370. def get_limit_choices_to_from_path(model, path):
  371. """ Return Q object for limiting choices if applicable.
  372. If final model in path is linked via a ForeignKey or ManyToManyField which
  373. has a `limit_choices_to` attribute, return it as a Q object.
  374. """
  375. fields = get_fields_from_path(model, path)
  376. fields = remove_trailing_data_field(fields)
  377. limit_choices_to = (
  378. fields and hasattr(fields[-1], 'remote_field') and
  379. getattr(fields[-1].remote_field, 'limit_choices_to', None))
  380. if not limit_choices_to:
  381. return models.Q() # empty Q
  382. elif isinstance(limit_choices_to, models.Q):
  383. return limit_choices_to # already a Q
  384. else:
  385. return models.Q(**limit_choices_to) # convert dict to Q
  386. def sortkeypicker(keynames):
  387. negate = set()
  388. for i, k in enumerate(keynames):
  389. if k[:1] == '-':
  390. keynames[i] = k[1:]
  391. negate.add(k[1:])
  392. def getit(adict):
  393. composite = [adict[k] for k in keynames]
  394. for i, (k, v) in enumerate(zip(keynames, composite)):
  395. if k in negate:
  396. composite[i] = -v
  397. return composite
  398. return getit
  399. def is_related_field(field):
  400. return isinstance(field, ForeignObjectRel)
  401. def is_related_field2(field):
  402. return (hasattr(field, 'remote_field') and field.remote_field != None) or is_related_field(field)