rows.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. from django.core.exceptions import FieldDoesNotExist
  2. from django.db import models
  3. from .columns.linkcolumn import BaseLinkColumn
  4. from .columns.manytomanycolumn import ManyToManyColumn
  5. from .utils import A, AttributeDict, call_with_appropriate, computed_values
  6. class CellAccessor:
  7. """
  8. Allows accessing cell contents on a row object (see `BoundRow`)
  9. """
  10. def __init__(self, row):
  11. self.row = row
  12. def __getitem__(self, key):
  13. return self.row.get_cell(key)
  14. def __getattr__(self, name):
  15. return self.row.get_cell(name)
  16. class BoundRow:
  17. """
  18. Represents a *specific* row in a table.
  19. `.BoundRow` objects are a container that make it easy to access the
  20. final 'rendered' values for cells in a row. You can simply iterate over a
  21. `.BoundRow` object and it will take care to return values rendered
  22. using the correct method (e.g. :ref:`table.render_FOO`)
  23. To access the rendered value of each cell in a row, just iterate over it::
  24. >>> import django_tables2 as tables
  25. >>> class SimpleTable(tables.Table):
  26. ... a = tables.Column()
  27. ... b = tables.CheckBoxColumn(attrs={'name': 'my_chkbox'})
  28. ...
  29. >>> table = SimpleTable([{'a': 1, 'b': 2}])
  30. >>> row = table.rows[0] # we only have one row, so let's use it
  31. >>> for cell in row:
  32. ... print(cell)
  33. ...
  34. 1
  35. <input type="checkbox" name="my_chkbox" value="2" />
  36. Alternatively you can use row.cells[0] to retrieve a specific cell::
  37. >>> row.cells[0]
  38. 1
  39. >>> row.cells[1]
  40. '<input type="checkbox" name="my_chkbox" value="2" />'
  41. >>> row.cells[2]
  42. ...
  43. IndexError: list index out of range
  44. Finally you can also use the column names to retrieve a specific cell::
  45. >>> row.cells.a
  46. 1
  47. >>> row.cells.b
  48. '<input type="checkbox" name="my_chkbox" value="2" />'
  49. >>> row.cells.c
  50. ...
  51. KeyError: "Column with name 'c' does not exist; choices are: ['a', 'b']"
  52. If you have the column name in a variable, you can also treat the `cells`
  53. property like a `dict`::
  54. >>> key = 'a'
  55. >>> row.cells[key]
  56. 1
  57. Arguments:
  58. table: The `.Table` in which this row exists.
  59. record: a single record from the :term:`table data` that is used to
  60. populate the row. A record could be a `~django.db.Model` object, a
  61. `dict`, or something else.
  62. """
  63. def __init__(self, record, table):
  64. self._record = record
  65. self._table = table
  66. self.row_counter = next(table._counter)
  67. # support accessing cells from a template: {{ row.cells.column_name }}
  68. self.cells = CellAccessor(self)
  69. @property
  70. def table(self):
  71. """The `.Table` this row is part of."""
  72. return self._table
  73. def get_even_odd_css_class(self):
  74. """
  75. Return css class, alternating for odd and even records.
  76. Return:
  77. string: `even` for even records, `odd` otherwise.
  78. """
  79. return "odd" if self.row_counter % 2 else "even"
  80. @property
  81. def attrs(self):
  82. """Return the attributes for a certain row."""
  83. cssClass = self.get_even_odd_css_class()
  84. row_attrs = computed_values(
  85. self._table.row_attrs, kwargs=dict(table=self._table, record=self._record)
  86. )
  87. if "class" in row_attrs and row_attrs["class"]:
  88. row_attrs["class"] += " " + cssClass
  89. else:
  90. row_attrs["class"] = cssClass
  91. return AttributeDict(row_attrs)
  92. @property
  93. def record(self):
  94. """The data record from the data source which is used to populate this row with data."""
  95. return self._record
  96. def __iter__(self):
  97. """
  98. Iterate over the rendered values for cells in the row.
  99. Under the hood this method just makes a call to
  100. `.BoundRow.__getitem__` for each cell.
  101. """
  102. for column, value in self.items():
  103. # this uses __getitem__, using the name (rather than the accessor)
  104. # is correct – it's what __getitem__ expects.
  105. yield value
  106. def _get_and_render_with(self, bound_column, render_func, default):
  107. value = None
  108. accessor = A(bound_column.accessor)
  109. column = bound_column.column
  110. # We need to take special care here to allow get_FOO_display()
  111. # methods on a model to be used if available. See issue #30.
  112. penultimate, remainder = accessor.penultimate(self.record)
  113. # If the penultimate is a model and the remainder is a field
  114. # using choices, use get_FOO_display().
  115. if isinstance(penultimate, models.Model):
  116. try:
  117. field = accessor.get_field(self.record)
  118. display_fn = getattr(penultimate, "get_%s_display" % remainder, None)
  119. if getattr(field, "choices", ()) and display_fn:
  120. value = display_fn()
  121. remainder = None
  122. except FieldDoesNotExist:
  123. pass
  124. # Fall back to just using the original accessor
  125. if remainder:
  126. try:
  127. value = accessor.resolve(self.record)
  128. except Exception:
  129. # we need to account for non-field based columns (issue #257)
  130. if isinstance(column, BaseLinkColumn) and column.text is not None:
  131. return render_func(bound_column)
  132. is_manytomanycolumn = isinstance(column, ManyToManyColumn)
  133. if value in column.empty_values or (is_manytomanycolumn and not value.exists()):
  134. return default
  135. return render_func(bound_column, value)
  136. def _optional_cell_arguments(self, bound_column, value):
  137. """
  138. Defines the arguments that will optionally be passed while calling the
  139. cell's rendering or value getter if that function has one of these as a
  140. keyword argument.
  141. """
  142. return {
  143. "value": value,
  144. "record": self.record,
  145. "column": bound_column.column,
  146. "bound_column": bound_column,
  147. "bound_row": self,
  148. "table": self._table,
  149. }
  150. def get_cell(self, name):
  151. """
  152. Returns the final rendered html for a cell in the row, given the name
  153. of a column.
  154. """
  155. bound_column = self.table.columns[name]
  156. return self._get_and_render_with(
  157. bound_column, render_func=self._call_render, default=bound_column.default
  158. )
  159. def _call_render(self, bound_column, value=None):
  160. """
  161. Call the column's render method with appropriate kwargs
  162. """
  163. render_kwargs = self._optional_cell_arguments(bound_column, value)
  164. content = call_with_appropriate(bound_column.render, render_kwargs)
  165. return bound_column.link(content, **render_kwargs) if bound_column.link else content
  166. def get_cell_value(self, name):
  167. """
  168. Returns the final rendered value (excluding any html) for a cell in the
  169. row, given the name of a column.
  170. """
  171. return self._get_and_render_with(
  172. self.table.columns[name], render_func=self._call_value, default=None
  173. )
  174. def _call_value(self, bound_column, value=None):
  175. """
  176. Call the column's value method with appropriate kwargs
  177. """
  178. return call_with_appropriate(
  179. bound_column.value, self._optional_cell_arguments(bound_column, value)
  180. )
  181. def __contains__(self, item):
  182. """
  183. Check by both row object and column name.
  184. """
  185. return item in (self.table.columns if isinstance(item, str) else self)
  186. def items(self):
  187. """
  188. Returns iterator yielding ``(bound_column, cell)`` pairs.
  189. *cell* is ``row[name]`` -- the rendered unicode value that should be
  190. ``rendered within ``<td>``.
  191. """
  192. for column in self.table.columns:
  193. # column gets some attributes relevant only relevant in this iteration,
  194. # used to allow passing the value/record to a callable Column.attrs /
  195. # Table.attrs item.
  196. column.current_value = self.get_cell(column.name)
  197. column.current_record = self.record
  198. yield (column, column.current_value)
  199. class BoundPinnedRow(BoundRow):
  200. """
  201. Represents a *pinned* row in a table.
  202. """
  203. @property
  204. def attrs(self):
  205. """
  206. Return the attributes for a certain pinned row.
  207. Add CSS classes `pinned-row` and `odd` or `even` to `class` attribute.
  208. Return:
  209. AttributeDict: Attributes for pinned rows.
  210. """
  211. row_attrs = computed_values(self._table.pinned_row_attrs, kwargs={"record": self._record})
  212. css_class = " ".join(
  213. [self.get_even_odd_css_class(), "pinned-row", row_attrs.get("class", "")]
  214. )
  215. row_attrs["class"] = css_class
  216. return AttributeDict(row_attrs)
  217. class BoundRows:
  218. """
  219. Container for spawning `.BoundRow` objects.
  220. Arguments:
  221. data: iterable of records
  222. table: the `~.Table` in which the rows exist
  223. pinned_data: dictionary with iterable of records for top and/or
  224. bottom pinned rows.
  225. Example:
  226. >>> pinned_data = {
  227. ... 'top': iterable, # or None value
  228. ... 'bottom': iterable, # or None value
  229. ... }
  230. This is used for `~.Table.rows`.
  231. """
  232. def __init__(self, data, table, pinned_data=None):
  233. self.data = data
  234. self.table = table
  235. self.pinned_data = pinned_data or {}
  236. def generator_pinned_row(self, data):
  237. """
  238. Top and bottom pinned rows generator.
  239. Arguments:
  240. data: Iterable data for all records for top or bottom pinned rows.
  241. Yields:
  242. BoundPinnedRow: Top or bottom `BoundPinnedRow` object for single pinned record.
  243. """
  244. if data is not None:
  245. if hasattr(data, "__iter__") is False:
  246. raise ValueError("The data for pinned rows must be iterable")
  247. for pinned_record in data:
  248. yield BoundPinnedRow(pinned_record, table=self.table)
  249. def __iter__(self):
  250. # Top pinned rows
  251. for pinned_record in self.generator_pinned_row(self.pinned_data.get("top")):
  252. yield pinned_record
  253. for record in self.data:
  254. yield BoundRow(record, table=self.table)
  255. # Bottom pinned rows
  256. for pinned_record in self.generator_pinned_row(self.pinned_data.get("bottom")):
  257. yield pinned_record
  258. def __len__(self):
  259. length = len(self.data)
  260. pinned_top = self.pinned_data.get("top")
  261. pinned_bottom = self.pinned_data.get("bottom")
  262. length += 0 if pinned_top is None else len(pinned_top)
  263. length += 0 if pinned_bottom is None else len(pinned_bottom)
  264. return length
  265. def __getitem__(self, key):
  266. """
  267. Slicing returns a new `~.BoundRows` instance, indexing returns a single
  268. `~.BoundRow` instance.
  269. """
  270. if isinstance(key, slice):
  271. return BoundRows(data=self.data[key], table=self.table, pinned_data=self.pinned_data)
  272. else:
  273. return BoundRow(record=self.data[key], table=self.table)