utils.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621
  1. import inspect
  2. import warnings
  3. from collections import OrderedDict
  4. from functools import total_ordering
  5. from itertools import chain
  6. from django.core.exceptions import FieldDoesNotExist
  7. from django.db import models
  8. from django.utils.html import format_html_join
  9. class Sequence(list):
  10. """
  11. Represents a column sequence, e.g. ``('first_name', '...', 'last_name')``
  12. This is used to represent `.Table.Meta.sequence` or the `.Table`
  13. constructors's *sequence* keyword argument.
  14. The sequence must be a list of column names and is used to specify the
  15. order of the columns on a table. Optionally a '...' item can be inserted,
  16. which is treated as a *catch-all* for column names that are not explicitly
  17. specified.
  18. """
  19. def expand(self, columns):
  20. """
  21. Expands the ``'...'`` item in the sequence into the appropriate column
  22. names that should be placed there.
  23. arguments:
  24. columns (list): list of column names.
  25. returns:
  26. The current instance.
  27. raises:
  28. `ValueError` if the sequence is invalid for the columns.
  29. """
  30. ellipses = self.count("...")
  31. if ellipses > 1:
  32. raise ValueError("'...' must be used at most once in a sequence.")
  33. elif ellipses == 0:
  34. self.append("...")
  35. # everything looks good, let's expand the "..." item
  36. columns = list(columns) # take a copy and exhaust the generator
  37. head = []
  38. tail = []
  39. target = head # start by adding things to the head
  40. for name in self:
  41. if name == "...":
  42. # now we'll start adding elements to the tail
  43. target = tail
  44. continue
  45. target.append(name)
  46. if name in columns:
  47. columns.pop(columns.index(name))
  48. self[:] = chain(head, columns, tail)
  49. return self
  50. class OrderBy(str):
  51. """
  52. A single item in an `.OrderByTuple` object.
  53. This class is essentially just a `str` with some extra properties.
  54. """
  55. QUERYSET_SEPARATOR = "__"
  56. def __new__(cls, value):
  57. instance = super().__new__(cls, value)
  58. if Accessor.LEGACY_SEPARATOR in value:
  59. message = (
  60. "Use '__' to separate path components, not '.' in accessor '{}'"
  61. " (fallback will be removed in django_tables2 version 3)."
  62. ).format(value)
  63. warnings.warn(message, DeprecationWarning, stacklevel=3)
  64. return instance
  65. @property
  66. def bare(self):
  67. """
  68. Returns:
  69. `.OrderBy`: the bare form.
  70. The *bare form* is the non-prefixed form. Typically the bare form is
  71. just the ascending form.
  72. Example: ``age`` is the bare form of ``-age``
  73. """
  74. return OrderBy(self[1:]) if self[:1] == "-" else self
  75. @property
  76. def opposite(self):
  77. """
  78. Provides the opposite of the current sorting direction.
  79. Returns:
  80. `.OrderBy`: object with an opposite sort influence.
  81. Example::
  82. >>> order_by = OrderBy('name')
  83. >>> order_by.opposite
  84. '-name'
  85. """
  86. return OrderBy(self[1:]) if self.is_descending else OrderBy("-" + self)
  87. @property
  88. def is_descending(self):
  89. """
  90. Returns `True` if this object induces *descending* ordering.
  91. """
  92. return self.startswith("-")
  93. @property
  94. def is_ascending(self):
  95. """
  96. Returns `True` if this object induces *ascending* ordering.
  97. """
  98. return not self.is_descending
  99. def for_queryset(self):
  100. """
  101. Returns the current instance usable in Django QuerySet's order_by
  102. arguments.
  103. """
  104. return self.replace(Accessor.LEGACY_SEPARATOR, OrderBy.QUERYSET_SEPARATOR)
  105. class OrderByTuple(tuple):
  106. """
  107. Stores ordering as (as `.OrderBy` objects).
  108. The `~.Table.order_by` property is always converted to an `.OrderByTuple` object.
  109. This class is essentially just a `tuple` with some useful extras.
  110. Example::
  111. >>> x = OrderByTuple(('name', '-age'))
  112. >>> x['age']
  113. '-age'
  114. >>> x['age'].is_descending
  115. True
  116. >>> x['age'].opposite
  117. 'age'
  118. """
  119. def __new__(cls, iterable):
  120. transformed = []
  121. for item in iterable:
  122. if not isinstance(item, OrderBy):
  123. item = OrderBy(item)
  124. transformed.append(item)
  125. return super().__new__(cls, transformed)
  126. def __str__(self):
  127. return ",".join(self)
  128. def __contains__(self, name):
  129. """
  130. Determine if a column has an influence on ordering.
  131. Example::
  132. >>> x = OrderByTuple(('name', ))
  133. >>> 'name' in x
  134. True
  135. >>> '-name' in x
  136. True
  137. Arguments:
  138. name (str): The name of a column. (optionally prefixed)
  139. Returns:
  140. bool: `True` if the column with `name` influences the ordering.
  141. """
  142. name = OrderBy(name).bare
  143. for order_by in self:
  144. if order_by.bare == name:
  145. return True
  146. return False
  147. def __getitem__(self, index):
  148. """
  149. Allows an `.OrderBy` object to be extracted via named or integer
  150. based indexing.
  151. When using named based indexing, it's fine to used a prefixed named::
  152. >>> x = OrderByTuple(('name', '-age'))
  153. >>> x[0]
  154. 'name'
  155. >>> x['age']
  156. '-age'
  157. >>> x['-age']
  158. '-age'
  159. Arguments:
  160. index (int): Index to query the ordering for.
  161. Returns:
  162. `.OrderBy`: for the ordering at the index.
  163. """
  164. if isinstance(index, str):
  165. for order_by in self:
  166. if order_by == index or order_by.bare == index:
  167. return order_by
  168. raise KeyError
  169. return super().__getitem__(index)
  170. @property
  171. def key(self):
  172. accessors = []
  173. reversing = []
  174. for order_by in self:
  175. accessors.append(Accessor(order_by.bare))
  176. reversing.append(order_by.is_descending)
  177. @total_ordering
  178. class Comparator:
  179. def __init__(self, obj):
  180. self.obj = obj
  181. def __eq__(self, other):
  182. for accessor in accessors:
  183. a = accessor.resolve(self.obj, quiet=True)
  184. b = accessor.resolve(other.obj, quiet=True)
  185. if not a == b:
  186. return False
  187. return True
  188. def __lt__(self, other):
  189. for accessor, reverse in zip(accessors, reversing):
  190. a = accessor.resolve(self.obj, quiet=True)
  191. b = accessor.resolve(other.obj, quiet=True)
  192. if a == b:
  193. continue
  194. if reverse:
  195. a, b = b, a
  196. # The rest of this should be refactored out into a util
  197. # function 'compare' that handles different types.
  198. try:
  199. return a < b
  200. except TypeError:
  201. # If the truth values differ, it's a good way to
  202. # determine ordering.
  203. if bool(a) is not bool(b):
  204. return bool(a) < bool(b)
  205. # Handle comparing different types, by falling back to
  206. # the string and id of the type. This at least groups
  207. # different types together.
  208. a_type = type(a)
  209. b_type = type(b)
  210. return (repr(a_type), id(a_type)) < (repr(b_type), id(b_type))
  211. return False
  212. return Comparator
  213. def get(self, key, fallback):
  214. """
  215. Identical to `__getitem__`, but supports fallback value.
  216. """
  217. try:
  218. return self[key]
  219. except (KeyError, IndexError):
  220. return fallback
  221. @property
  222. def opposite(self):
  223. """
  224. Return version with each `.OrderBy` prefix toggled::
  225. >>> order_by = OrderByTuple(('name', '-age'))
  226. >>> order_by.opposite
  227. ('-name', 'age')
  228. """
  229. return type(self)((o.opposite for o in self))
  230. class Accessor(str):
  231. """
  232. A string describing a path from one object to another via attribute/index
  233. accesses. For convenience, the class has an alias `.A` to allow for more concise code.
  234. Relations are separated by a ``__`` character.
  235. To support list-of-dicts from ``QuerySet.values()``, if the context is a dictionary,
  236. and the accessor is a key in the dictionary, it is returned right away.
  237. """
  238. LEGACY_SEPARATOR = "."
  239. SEPARATOR = "__"
  240. ALTERS_DATA_ERROR_FMT = "Refusing to call {method}() because `.alters_data = True`"
  241. LOOKUP_ERROR_FMT = (
  242. "Failed lookup for key [{key}] in {context}, when resolving the accessor {accessor}"
  243. )
  244. def __new__(cls, value):
  245. instance = super().__new__(cls, value)
  246. if cls.LEGACY_SEPARATOR in value:
  247. instance.SEPARATOR = cls.LEGACY_SEPARATOR
  248. message = (
  249. "Use '__' to separate path components, not '.' in accessor '{}'"
  250. " (fallback will be removed in django_tables2 version 3)."
  251. ).format(value)
  252. warnings.warn(message, DeprecationWarning, stacklevel=3)
  253. return instance
  254. def resolve(self, context, safe=True, quiet=False):
  255. """
  256. Return an object described by the accessor by traversing the attributes of *context*.
  257. Lookups are attempted in the following order:
  258. - dictionary (e.g. ``obj[related]``)
  259. - attribute (e.g. ``obj.related``)
  260. - list-index lookup (e.g. ``obj[int(related)]``)
  261. Callable objects are called, and their result is used, before
  262. proceeding with the resolving.
  263. Example::
  264. >>> x = Accessor("__len__")
  265. >>> x.resolve("brad")
  266. 4
  267. >>> x = Accessor("0__upper")
  268. >>> x.resolve("brad")
  269. "B"
  270. If the context is a dictionary and the accessor-value is a key in it,
  271. the value for that key is immediately returned::
  272. >>> x = Accessor("user__first_name")
  273. >>> x.resolve({"user__first_name": "brad"})
  274. "brad"
  275. Arguments:
  276. context : The root/first object to traverse.
  277. safe (bool): Don't call anything with `alters_data = True`
  278. quiet (bool): Smother all exceptions and instead return `None`
  279. Returns:
  280. target object
  281. Raises:
  282. TypeError`, `AttributeError`, `KeyError`, `ValueError`
  283. (unless `quiet` == `True`)
  284. """
  285. # Short-circuit if the context contains a key with the exact name of the accessor,
  286. # supporting list-of-dicts data returned from values_list("related_model__field")
  287. if isinstance(context, dict) and self in context:
  288. return context[self]
  289. try:
  290. current = context
  291. for bit in self.bits:
  292. try: # dictionary lookup
  293. current = current[bit]
  294. except (TypeError, AttributeError, KeyError):
  295. try: # attribute lookup
  296. current = getattr(current, bit)
  297. except (TypeError, AttributeError):
  298. try: # list-index lookup
  299. current = current[int(bit)]
  300. except (
  301. IndexError, # list index out of range
  302. ValueError, # invalid literal for int()
  303. KeyError, # dict without `int(bit)` key
  304. TypeError, # unsubscriptable object
  305. ):
  306. current_context = (
  307. type(current) if isinstance(current, models.Model) else current
  308. )
  309. raise ValueError(
  310. self.LOOKUP_ERROR_FMT.format(
  311. key=bit, context=current_context, accessor=self
  312. )
  313. )
  314. if callable(current):
  315. if safe and getattr(current, "alters_data", False):
  316. raise ValueError(self.ALTERS_DATA_ERROR_FMT.format(method=repr(current)))
  317. if not getattr(current, "do_not_call_in_templates", False):
  318. current = current()
  319. # Important that we break in None case, or a relationship
  320. # spanning across a null-key will raise an exception in the
  321. # next iteration, instead of defaulting.
  322. if current is None:
  323. break
  324. return current
  325. except Exception:
  326. if not quiet:
  327. raise
  328. @property
  329. def bits(self):
  330. if self == "":
  331. return ()
  332. return self.split(self.SEPARATOR)
  333. def get_field(self, model):
  334. """
  335. Return the django model field for model in context, following relations.
  336. """
  337. if not hasattr(model, "_meta"):
  338. return
  339. field = None
  340. for bit in self.bits:
  341. try:
  342. field = model._meta.get_field(bit)
  343. except FieldDoesNotExist:
  344. break
  345. if hasattr(field, "remote_field"):
  346. rel = getattr(field, "remote_field", None)
  347. model = getattr(rel, "model", model)
  348. return field
  349. def penultimate(self, context, quiet=True):
  350. """
  351. Split the accessor on the right-most separator ('__'), return a tuple with:
  352. - the resolved left part.
  353. - the remainder
  354. Example::
  355. >>> Accessor("a__b__c").penultimate({"a": {"a": 1, "b": {"c": 2, "d": 4}}})
  356. ({"c": 2, "d": 4}, "c")
  357. """
  358. path, _, remainder = self.rpartition(self.SEPARATOR)
  359. return A(path).resolve(context, quiet=quiet), remainder
  360. A = Accessor # alias
  361. class AttributeDict(OrderedDict):
  362. """
  363. A wrapper around `collections.OrderedDict` that knows how to render itself
  364. as HTML style tag attributes.
  365. Any key with ``value is None`` will be skipped.
  366. The returned string is marked safe, so it can be used safely in a template.
  367. See `.as_html` for a usage example.
  368. """
  369. blacklist = ("th", "td", "_ordering", "thead", "tbody", "tfoot")
  370. def _iteritems(self):
  371. for key, v in self.items():
  372. value = v() if callable(v) else v
  373. if key not in self.blacklist and value is not None:
  374. yield (key, value)
  375. def as_html(self):
  376. """
  377. Render to HTML tag attributes.
  378. Example:
  379. .. code-block:: python
  380. >>> from django_tables2.utils import AttributeDict
  381. >>> attrs = AttributeDict({'class': 'mytable', 'id': 'someid'})
  382. >>> attrs.as_html()
  383. 'class="mytable" id="someid"'
  384. returns: `~django.utils.safestring.SafeUnicode` object
  385. """
  386. return format_html_join(" ", '{}="{}"', self._iteritems())
  387. def segment(sequence, aliases):
  388. """
  389. Translates a flat sequence of items into a set of prefixed aliases.
  390. This allows the value set by `.QuerySet.order_by` to be translated into
  391. a list of columns that would have the same result. These are called
  392. "order by aliases" which are optionally prefixed column names::
  393. >>> list(segment(('a', '-b', 'c'),
  394. ... {'x': ('a'),
  395. ... 'y': ('b', '-c'),
  396. ... 'z': ('-b', 'c')}))
  397. [('x', '-y'), ('x', 'z')]
  398. """
  399. if not (sequence or aliases):
  400. return
  401. for alias, parts in aliases.items():
  402. variants = {
  403. # alias: order by tuple
  404. alias: OrderByTuple(parts),
  405. OrderBy(alias).opposite: OrderByTuple(parts).opposite,
  406. }
  407. for valias, vparts in variants.items():
  408. if list(sequence[: len(vparts)]) == list(vparts):
  409. tail_aliases = dict(aliases)
  410. del tail_aliases[alias]
  411. tail_sequence = sequence[len(vparts) :]
  412. if tail_sequence:
  413. for tail in segment(tail_sequence, tail_aliases):
  414. yield tuple(chain([valias], tail))
  415. else:
  416. continue
  417. else:
  418. yield tuple([valias])
  419. def signature(fn):
  420. """
  421. Returns:
  422. tuple: Returns a (arguments, kwarg_name)-tuple:
  423. - the arguments (positional or keyword)
  424. - the name of the ** kwarg catch all.
  425. The self-argument for methods is always removed.
  426. """
  427. signature = inspect.signature(fn)
  428. args = []
  429. keywords = None
  430. for arg in signature.parameters.values():
  431. if arg.kind == arg.VAR_KEYWORD:
  432. keywords = arg.name
  433. elif arg.kind == arg.VAR_POSITIONAL:
  434. continue # skip *args catch-all
  435. else:
  436. args.append(arg.name)
  437. return tuple(args), keywords
  438. def call_with_appropriate(fn, kwargs):
  439. """
  440. Calls the function ``fn`` with the keyword arguments from ``kwargs`` it expects
  441. If the kwargs argument is defined, pass all arguments, else provide exactly
  442. the arguments wanted.
  443. If one of the arguments of ``fn`` are not contained in kwargs, ``fn`` will not
  444. be called and ``None`` will be returned.
  445. """
  446. args, kwargs_name = signature(fn)
  447. # no catch-all defined, we need to exactly pass the arguments specified.
  448. if not kwargs_name:
  449. kwargs = {key: kwargs[key] for key in kwargs if key in args}
  450. # if any argument of fn is not in kwargs, just return None
  451. if any(arg not in kwargs for arg in args):
  452. return None
  453. return fn(**kwargs)
  454. def computed_values(d, kwargs=None):
  455. """
  456. Returns a new `dict` that has callable values replaced with the return values.
  457. Example::
  458. >>> compute_values({'foo': lambda: 'bar'})
  459. {'foo': 'bar'}
  460. Arbitrarily deep structures are supported. The logic is as follows:
  461. 1. If the value is callable, call it and make that the new value.
  462. 2. If the value is an instance of dict, use ComputableDict to compute its keys.
  463. Example::
  464. >>> def parents():
  465. ... return {
  466. ... 'father': lambda: 'Foo',
  467. ... 'mother': 'Bar'
  468. ... }
  469. ...
  470. >>> a = {
  471. ... 'name': 'Brad',
  472. ... 'parents': parents
  473. ... }
  474. ...
  475. >>> computed_values(a)
  476. {'name': 'Brad', 'parents': {'father': 'Foo', 'mother': 'Bar'}}
  477. Arguments:
  478. d (dict): The original dictionary.
  479. kwargs: any extra keyword arguments will be passed to the callables, if the callable
  480. takes an argument with such a name.
  481. Returns:
  482. dict: with callable values replaced.
  483. """
  484. kwargs = kwargs or {}
  485. result = {}
  486. for k, v in d.items():
  487. if callable(v):
  488. v = call_with_appropriate(v, kwargs=kwargs)
  489. if isinstance(v, dict):
  490. v = computed_values(v, kwargs=kwargs)
  491. result[k] = v
  492. return result