{% endfor %}\r
\r
\r
+Advanced Features\r
+~~~~~~~~~~~~~~~~~\r
+\r
+There are few requirements for the source data of a table. It should be an\r
+iterable with dict-like objects. Values found in the source data that are\r
+not associated with a column are ignored, missing values are replaced by\r
+the column default or None.\r
+\r
+If any value in the source data is a callable, it will be passed it's own row\r
+instance and is expected to return the actual value for this particular table\r
+cell.\r
+\r
+Similarily, the colunn default value may also be callable that will takes\r
+the row instance as an argument (representing the row that the default is\r
+needed for).\r
+\r
+\r
ModelTables\r
-----------\r
\r
* A ModelTable column's ``default`` value does not affect ordering.\r
This differs from the non-model table behaviour.\r
\r
-Columns\r
--------\r
+If a column is mapped to a method on the model, that method will be called\r
+without arguments. This behavior differs from non-model tables, where a\r
+row object will be passed.\r
+\r
+Columns (# TODO)\r
+----------------\r
\r
verbose_name, default, visible, sortable\r
``verbose_name`` defines a display name for this column used for output.\r
``default`` is the default value for this column. If the data source\r
does not provide None for a row, the default will be used instead. Note\r
that whether this effects ordering might depend on the table type (model\r
- or normal).\r
+ or normal). default might be a callable.\r
\r
You can use ``visible`` to flag the column as hidden by default.\r
However, this can be overridden by the ``visibility`` argument to the\r
- let columns change their default ordering (ascending/descending)\r
- filters\r
- grouping\r
- - choices support for columns (internal column value will be looked up for output)
\ No newline at end of file
+ - choices support for columns (internal column value will be looked up for output)\r
+ - for columns that span model relationships, automatically generate select_related()
\ No newline at end of file
use the model field name.\r
\r
``default`` is the default value for this column. If the data source\r
- does not provide None for a row, the default will be used instead. Note\r
+ does provide ``None`` for a row, the default will be used instead. Note\r
that whether this effects ordering might depend on the table type (model\r
- or normal).\r
+ or normal). Also, you can specify a callable, which will be passed a\r
+ ``BoundRow`` instance and is expected to return the default to be used.\r
\r
You can use ``visible`` to flag the column as hidden by default.\r
However, this can be overridden by the ``visibility`` argument to the\r
Setting ``sortable`` to False will result in this column being unusable\r
in ordering.\r
"""\r
+\r
# Tracks each time a Column instance is created. Used to retain order.\r
creation_counter = 0\r
\r
"""\r
\r
# find the column for the requested field, for reference\r
- boundcol = (name in self.table._columns) \\r
- and self.table._columns[name]\\r
- or None\r
+ boundcol = self.table._columns[name]\r
\r
# If the column has a name override (we know then that is was also\r
# used for access, e.g. if the condition is true, then\r
# ``boundcol.column.name == name``), we need to make sure we use the\r
# declaration name to access the model field.\r
if boundcol.column.name:\r
- name = boundcol.declared_name\r
+ name = boundcol.declared_name\r
\r
result = getattr(self.data, name, None)\r
- if result is None:\r
- if boundcol and boundcol.column.default is not None:\r
- result = boundcol.column.default\r
- else:\r
- raise AttributeError()\r
+ if callable(result):\r
+ result = result()\r
+ elif result is None:\r
+ if boundcol.column.default is not None:\r
+ result = boundcol.get_default(self)\r
+\r
return result
\ No newline at end of file
def sort_table(data, order_by):\r
"""Sort a list of dicts according to the fieldnames in the\r
``order_by`` iterable. Prefix with hypen for reverse.\r
+\r
+ Dict values can be callables.\r
"""\r
def _cmp(x, y):\r
for name, reverse in instructions:\r
- res = cmp(x.get(name), y.get(name))\r
+ lhs, rhs = x.get(name), y.get(name)\r
+ res = cmp((callable(lhs) and [lhs(x)] or [lhs])[0],\r
+ (callable(rhs) and [rhs(y)] or [rhs])[0])\r
if res != 0:\r
return reverse and -res or res\r
return 0\r
\r
In the case of this base table implementation, a copy of the\r
source data is created, and then modified appropriately.\r
+\r
+ # TODO: currently this is called whenever data changes; it is\r
+ # probably much better to do this on-demand instead, when the\r
+ # data is *needed* for the first time.\r
"""\r
\r
# reset caches\r
# which is the current design decision.\r
for column in self.columns.all():\r
if not column.declared_name in row:\r
- row[column.declared_name] = column.column.default\r
+ # since rows are not really in the picture yet, create a\r
+ # temporary row object for this call.\r
+ row[column.declared_name] = column.get_default(BoundRow(self, row))\r
\r
if self.order_by:\r
sort_table(snapshot, self._cols_to_fields(self.order_by))\r
\r
name = property(lambda s: s.column.name or s.declared_name)\r
\r
+ def get_default(self, row):\r
+ """Since a column's ``default`` property may be a callable, we need\r
+ this function to resolve it when needed.\r
+\r
+ Make sure ``row`` is a ``BoundRow`` objects, since that is what\r
+ we promise the callable will get.\r
+ """\r
+ if callable(self.column.default):\r
+ return self.column.default(row)\r
+ return self.column.default\r
+\r
def _get_values(self):\r
# TODO: build a list of values used\r
pass\r
def __getitem__(self, name):\r
"""Returns this row's value for a column. All other access methods,\r
e.g. __iter__, lead ultimately to this."""\r
- return self.data[self.table.columns[name].declared_name]\r
+ result = self.data[self.table.columns[name].declared_name]\r
+ if callable(result):\r
+ result = result(self)\r
+ return result\r
\r
def __contains__(self, item):\r
"""Check by both row object and column name."""\r
books.base_columns['language'].sortable = False\r
books.order_by = 'language'\r
assert not books.order_by\r
- test_order(('language', 'num_pages'), [1,3,2,4]) # as if: 'num_pages'
\ No newline at end of file
+ test_order(('language', 'num_pages'), [1,3,2,4]) # as if: 'num_pages'\r
+\r
+def test_callable():\r
+ """Data fields, ``default`` option can be callables.\r
+ """\r
+\r
+ class MathTable(tables.Table):\r
+ lhs = tables.Column()\r
+ rhs = tables.Column()\r
+ op = tables.Column(default='+')\r
+ sum = tables.Column(default=lambda d: calc(d['op'], d['lhs'], d['rhs']))\r
+\r
+ math = MathTable([\r
+ {'lhs': 1, 'rhs': lambda x: x['lhs']*3}, # 1+3\r
+ {'lhs': 9, 'rhs': lambda x: x['lhs'], 'op': '/'}, # 9/9\r
+ {'lhs': lambda x: x['rhs']+3, 'rhs': 4, 'op': '-'}, # 7-4\r
+ ])\r
+\r
+ # function is called when queried\r
+ def calc(op, lhs, rhs):\r
+ if op == '+': return lhs+rhs\r
+ elif op == '/': return lhs/rhs\r
+ elif op == '-': return lhs-rhs\r
+ assert [calc(row['op'], row['lhs'], row['rhs']) for row in math] == [4,1,3]\r
+\r
+ # function is called while sorting\r
+ math.order_by = ('-rhs',)\r
+ assert [row['rhs'] for row in math] == [9,4,3]\r
+\r
+ math.order_by = ('sum',)\r
+ assert [row['sum'] for row in math] == [1,3,4]
\ No newline at end of file
Sets up a temporary Django project using a memory SQLite database.\r
"""\r
\r
+from py.test import raises\r
from django.conf import settings\r
import django_tables as tables\r
\r
capital = models.ForeignKey(City, blank=True, null=True)\r
tld = models.TextField(verbose_name='Domain Extension', max_length=2)\r
system = models.TextField(blank=True, null=True)\r
- null = models.TextField(blank=True, null=True) # tests expect this to be always null!\r
+ null = models.TextField(blank=True, null=True) # tests expect this to be always null!\r
+ null2 = models.TextField(blank=True, null=True) # - " -\r
+ def example_domain(self):\r
+ return 'example.%s' % self.tld\r
class Meta:\r
app_label = 'testapp'\r
module.Country = Country\r
class Meta:\r
model = Country\r
\r
- assert len(CountryTable.base_columns) == 7\r
+ assert len(CountryTable.base_columns) == 8\r
assert 'name' in CountryTable.base_columns\r
assert not hasattr(CountryTable, 'name')\r
\r
model = Country\r
exclude = ['tld']\r
\r
- assert len(CountryTable.base_columns) == 7\r
+ assert len(CountryTable.base_columns) == 8\r
assert 'projected' in CountryTable.base_columns\r
assert 'capital' in CountryTable.base_columns\r
assert not 'tld' in CountryTable.base_columns\r
assert not 'does-not-exist' in r\r
# ...so are excluded fields\r
assert not 'id' in r\r
+ # [bug] access to data that might be available, but does not\r
+ # have a corresponding column is denied.\r
+ raises(Exception, "r['id']")\r
# missing data is available with default values\r
assert 'null' in r\r
assert r['null'] == "foo" # note: different from prev. line!\r
+ # if everything else fails (no default), we get None back\r
+ assert r['null2'] is None\r
\r
# all that still works when name overrides are used\r
assert not 'tld' in r\r
capital = tables.Column()\r
system = tables.Column()\r
null = tables.Column(default="foo")\r
+ null2 = tables.Column()\r
tld = tables.Column(name="domain")\r
countries = CountryTable(Country)\r
test_country_table(countries)\r
def test_pagination():\r
pass\r
\r
+def test_callable():\r
+ """Some of the callable code is reimplemented for modeltables, so\r
+ test some specifics again.\r
+ """\r
+\r
+ class CountryTable(tables.ModelTable):\r
+ null = tables.Column(default=lambda s: s['example_domain'])\r
+ example_domain = tables.Column()\r
+ class Meta:\r
+ model = Country\r
+ countries = CountryTable(Country)\r
+\r
+ # model method is called\r
+ assert [row['example_domain'] for row in countries] == \\r
+ ['example.'+row['tld'] for row in countries]\r
+\r
+ # column default method is called\r
+ assert [row['example_domain'] for row in countries] == \\r
+ [row['null'] for row in countries]\r
+\r
# TODO: pagination\r
-# TODO: support function column sources both for modeltables (methods on model) and static tables (functions in dict)\r
-# TODO: support relationship spanning columns (we could generate select_related() automatically)
\ No newline at end of file
+# TODO: support relationship spanning columns
\ No newline at end of file