from django.core.exceptions import FieldDoesNotExist
from django.db import models
from .columns.linkcolumn import BaseLinkColumn
from .columns.manytomanycolumn import ManyToManyColumn
from .utils import A, AttributeDict, call_with_appropriate, computed_values
class CellAccessor:
"""
Allows accessing cell contents on a row object (see `BoundRow`)
"""
def __init__(self, row):
self.row = row
def __getitem__(self, key):
return self.row.get_cell(key)
def __getattr__(self, name):
return self.row.get_cell(name)
[docs]class BoundRow:
"""
Represents a *specific* row in a table.
`.BoundRow` objects are a container that make it easy to access the
final 'rendered' values for cells in a row. You can simply iterate over a
`.BoundRow` object and it will take care to return values rendered
using the correct method (e.g. :ref:`table.render_FOO`)
To access the rendered value of each cell in a row, just iterate over it::
>>> import django_tables2 as tables
>>> class SimpleTable(tables.Table):
... a = tables.Column()
... b = tables.CheckBoxColumn(attrs={'name': 'my_chkbox'})
...
>>> table = SimpleTable([{'a': 1, 'b': 2}])
>>> row = table.rows[0] # we only have one row, so let's use it
>>> for cell in row:
... print(cell)
...
1
<input type="checkbox" name="my_chkbox" value="2" />
Alternatively you can use row.cells[0] to retrieve a specific cell::
>>> row.cells[0]
1
>>> row.cells[1]
'<input type="checkbox" name="my_chkbox" value="2" />'
>>> row.cells[2]
...
IndexError: list index out of range
Finally you can also use the column names to retrieve a specific cell::
>>> row.cells.a
1
>>> row.cells.b
'<input type="checkbox" name="my_chkbox" value="2" />'
>>> row.cells.c
...
KeyError: "Column with name 'c' does not exist; choices are: ['a', 'b']"
If you have the column name in a variable, you can also treat the `cells`
property like a `dict`::
>>> key = 'a'
>>> row.cells[key]
1
Arguments:
table: The `.Table` in which this row exists.
record: a single record from the :term:`table data` that is used to
populate the row. A record could be a `~django.db.Model` object, a
`dict`, or something else.
"""
[docs] def __init__(self, record, table):
self._record = record
self._table = table
self.row_counter = next(table._counter)
# support accessing cells from a template: {{ row.cells.column_name }}
self.cells = CellAccessor(self)
@property
def table(self):
"""The `.Table` this row is part of."""
return self._table
[docs] def get_even_odd_css_class(self):
"""
Return css class, alternating for odd and even records.
Return:
string: `even` for even records, `odd` otherwise.
"""
return "odd" if self.row_counter % 2 else "even"
@property
def attrs(self):
"""Return the attributes for a certain row."""
cssClass = self.get_even_odd_css_class()
row_attrs = computed_values(
self._table.row_attrs, kwargs=dict(table=self._table, record=self._record)
)
if "class" in row_attrs and row_attrs["class"]:
row_attrs["class"] += " " + cssClass
else:
row_attrs["class"] = cssClass
return AttributeDict(row_attrs)
@property
def record(self):
"""The data record from the data source which is used to populate this row with data."""
return self._record
[docs] def __iter__(self):
"""
Iterate over the rendered values for cells in the row.
Under the hood this method just makes a call to
`.BoundRow.__getitem__` for each cell.
"""
for column, value in self.items():
# this uses __getitem__, using the name (rather than the accessor)
# is correct – it's what __getitem__ expects.
yield value
def _get_and_render_with(self, bound_column, render_func, default):
value = None
accessor = A(bound_column.accessor)
column = bound_column.column
# We need to take special care here to allow get_FOO_display()
# methods on a model to be used if available. See issue #30.
penultimate, remainder = accessor.penultimate(self.record)
# If the penultimate is a model and the remainder is a field
# using choices, use get_FOO_display().
if isinstance(penultimate, models.Model):
try:
field = accessor.get_field(self.record)
display_fn = getattr(penultimate, "get_%s_display" % remainder, None)
if getattr(field, "choices", ()) and display_fn:
value = display_fn()
remainder = None
except FieldDoesNotExist:
pass
# Fall back to just using the original accessor
if remainder:
try:
value = accessor.resolve(self.record)
except Exception:
# we need to account for non-field based columns (issue #257)
if isinstance(column, BaseLinkColumn) and column.text is not None:
return render_func(bound_column)
is_manytomanycolumn = isinstance(column, ManyToManyColumn)
if value in column.empty_values or (is_manytomanycolumn and not value.exists()):
return default
return render_func(bound_column, value)
[docs] def _optional_cell_arguments(self, bound_column, value):
"""
Defines the arguments that will optionally be passed while calling the
cell's rendering or value getter if that function has one of these as a
keyword argument.
"""
return {
"value": value,
"record": self.record,
"column": bound_column.column,
"bound_column": bound_column,
"bound_row": self,
"table": self._table,
}
[docs] def get_cell(self, name):
"""
Returns the final rendered html for a cell in the row, given the name
of a column.
"""
bound_column = self.table.columns[name]
return self._get_and_render_with(
bound_column, render_func=self._call_render, default=bound_column.default
)
[docs] def _call_render(self, bound_column, value=None):
"""
Call the column's render method with appropriate kwargs
"""
render_kwargs = self._optional_cell_arguments(bound_column, value)
content = call_with_appropriate(bound_column.render, render_kwargs)
return bound_column.link(content, **render_kwargs) if bound_column.link else content
[docs] def get_cell_value(self, name):
"""
Returns the final rendered value (excluding any html) for a cell in the
row, given the name of a column.
"""
return self._get_and_render_with(
self.table.columns[name], render_func=self._call_value, default=None
)
[docs] def _call_value(self, bound_column, value=None):
"""
Call the column's value method with appropriate kwargs
"""
return call_with_appropriate(
bound_column.value, self._optional_cell_arguments(bound_column, value)
)
[docs] def __contains__(self, item):
"""
Check by both row object and column name.
"""
return item in (self.table.columns if isinstance(item, str) else self)
[docs] def items(self):
"""
Returns iterator yielding ``(bound_column, cell)`` pairs.
*cell* is ``row[name]`` -- the rendered unicode value that should be
``rendered within ``<td>``.
"""
for column in self.table.columns:
# column gets some attributes relevant only relevant in this iteration,
# used to allow passing the value/record to a callable Column.attrs /
# Table.attrs item.
column.current_value = self.get_cell(column.name)
column.current_record = self.record
yield (column, column.current_value)
[docs]class BoundPinnedRow(BoundRow):
"""
Represents a *pinned* row in a table.
"""
@property
def attrs(self):
"""
Return the attributes for a certain pinned row.
Add CSS classes `pinned-row` and `odd` or `even` to `class` attribute.
Return:
AttributeDict: Attributes for pinned rows.
"""
row_attrs = computed_values(self._table.pinned_row_attrs, kwargs={"record": self._record})
css_class = " ".join(
[self.get_even_odd_css_class(), "pinned-row", row_attrs.get("class", "")]
)
row_attrs["class"] = css_class
return AttributeDict(row_attrs)
[docs]class BoundRows:
"""
Container for spawning `.BoundRow` objects.
Arguments:
data: iterable of records
table: the `~.Table` in which the rows exist
pinned_data: dictionary with iterable of records for top and/or
bottom pinned rows.
Example:
>>> pinned_data = {
... 'top': iterable, # or None value
... 'bottom': iterable, # or None value
... }
This is used for `~.Table.rows`.
"""
[docs] def __init__(self, data, table, pinned_data=None):
self.data = data
self.table = table
self.pinned_data = pinned_data or {}
[docs] def generator_pinned_row(self, data):
"""
Top and bottom pinned rows generator.
Arguments:
data: Iterable data for all records for top or bottom pinned rows.
Yields:
BoundPinnedRow: Top or bottom `BoundPinnedRow` object for single pinned record.
"""
if data is not None:
if hasattr(data, "__iter__") is False:
raise ValueError("The data for pinned rows must be iterable")
for pinned_record in data:
yield BoundPinnedRow(pinned_record, table=self.table)
def __iter__(self):
# Top pinned rows
for pinned_record in self.generator_pinned_row(self.pinned_data.get("top")):
yield pinned_record
for record in self.data:
yield BoundRow(record, table=self.table)
# Bottom pinned rows
for pinned_record in self.generator_pinned_row(self.pinned_data.get("bottom")):
yield pinned_record
def __len__(self):
length = len(self.data)
pinned_top = self.pinned_data.get("top")
pinned_bottom = self.pinned_data.get("bottom")
length += 0 if pinned_top is None else len(pinned_top)
length += 0 if pinned_bottom is None else len(pinned_bottom)
return length
[docs] def __getitem__(self, key):
"""
Slicing returns a new `~.BoundRows` instance, indexing returns a single
`~.BoundRow` instance.
"""
if isinstance(key, slice):
return BoundRows(data=self.data[key], table=self.table, pinned_data=self.pinned_data)
else:
return BoundRow(record=self.data[key], table=self.table)