self.base_columns = copy.deepcopy(type(self).base_columns)\r
\r
def _build_snapshot(self):\r
+ """Rebuilds the table whenever it's options change.\r
+\r
+ Whenver the table options change, e.g. say a new sort order,\r
+ this method will be asked to regenerate the actual table from\r
+ the linked data source.\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
snapshot = copy.copy(self._data)\r
for row in snapshot:\r
- # delete unknown columns and add missing ones; note that\r
- # self.columns already accounts for column name overrides.\r
- for column in row.keys():\r
- if not column in self.columns:\r
- del row[column]\r
- for colname, colobj in self.columns.items():\r
- if not colname in row:\r
- row[colname] = colobj.column.default\r
+ # delete unknown columns that are in the source data, but that\r
+ # we don't know about.\r
+ # TODO: does this even make sense? we might in some cases save\r
+ # memory, but at what performance cost?\r
+ decl_colnames = [c.declared_name for c in self.columns.all()]\r
+ for key in row.keys():\r
+ if not key in decl_colnames:\r
+ del row[key]\r
+ # add data for defined columns that is missing from the source.\r
+ # we do this so that colunn default values can affect sorting,\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
+\r
if self.order_by:\r
- sort_table(snapshot, self.order_by)\r
+ sort_table(snapshot, self._cols_to_fields(self.order_by))\r
self._snapshot = snapshot\r
\r
def _get_data(self):\r
return self._snapshot\r
data = property(lambda s: s._get_data())\r
\r
+ def _cols_to_fields(self, names):\r
+ """Utility function. Given a list of column names (as exposed to\r
+ the user), converts column names to the names we have to use to\r
+ retrieve a column's data from the source.\r
+\r
+ Right now, the name used in the table declaration is used for\r
+ access, but a column can define it's own alias-like name that will\r
+ be used to refer to the column from outside.\r
+\r
+ Supports prefixed column names as used e.g. in order_by ("-field").\r
+ """\r
+ result = []\r
+ for ident in names:\r
+ if ident[:1] == '-':\r
+ name = ident[1:]\r
+ prefix = '-'\r
+ else:\r
+ name = ident\r
+ prefix = ''\r
+ result.append(prefix + self.columns[name].declared_name)\r
+ return result\r
+\r
def _validate_column_name(self, name, purpose):\r
"""Return True/False, depending on whether the column ``name`` is\r
valid for ``purpose``. Used to validate things like ``order_by``\r
# validate, remove all invalid order instructions\r
self._order_by = OrderByTuple([o for o in self._order_by\r
if self._validate_column_name((o[:1]=='-' and [o[1:]] or [o])[0], "order_by")])\r
- # TODO: optionally, throw an exception\r
order_by = property(lambda s: s._order_by, _set_order_by)\r
\r
def __unicode__(self):\r
self._columns = new_columns\r
\r
def all(self):\r
+ """Iterate through all columns, regardless of visiblity (as\r
+ opposed to ``__iter__``.\r
+\r
+ This is used internally a lot.\r
+ """\r
self._spawn_columns()\r
for column in self._columns.values():\r
yield column\r
for r in self._columns.items():\r
yield r\r
\r
- def keys(self):\r
+ def names(self):\r
self._spawn_columns()\r
for r in self._columns.keys():\r
yield r\r
return self._columns.keyOrder.index(name)\r
\r
def __iter__(self):\r
+ """Iterate through all *visible* bound columns.\r
+\r
+ This is primarily geared towards table rendering.\r
+ """\r
for column in self.all():\r
if column.column.visible:\r
yield column\r
"""Check by both column object and column name."""\r
self._spawn_columns()\r
if isinstance(item, basestring):\r
- return item in self.keys()\r
+ return item in self.names()\r
else:\r
return item in self.all()\r
\r
"""'Runtime' version of ``Column`` that is bound to a table instance,\r
and thus knows about the table's data.\r
\r
- Note name... TODO\r
+ Note that the name that is passed in tells us how this field is\r
+ delared in the bound table. The column itself can overwrite this name.\r
+ While the overwritten name will be hat mostly counts, we need to\r
+ remember the one used for declaration as well, or we won't know how\r
+ to read a column's value from the source.\r
"""\r
def __init__(self, table, column, name):\r
self.table = table\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[name]\r
+ return self.data[self.table.columns[name].declared_name]\r
\r
def __contains__(self, item):\r
"""Check by both row object and column name."""\r
language = tables.Column(default='en') # default affects sorting\r
\r
books = BookTable([\r
- {'id': 1, 'num_pages': 60, 'name': 'Z: The Book'}, # language: en\r
- {'id': 2, 'num_pages': 100, 'language': 'de', 'name': 'A: The Book'},\r
- {'id': 3, 'num_pages': 80, 'language': 'de', 'name': 'A: The Book, Vol. 2'},\r
- {'id': 4, 'num_pages': 110, 'language': 'fr', 'name': 'A: The Book, French Edition'},\r
+ {'id': 1, 'pages': 60, 'name': 'Z: The Book'}, # language: en\r
+ {'id': 2, 'pages': 100, 'language': 'de', 'name': 'A: The Book'},\r
+ {'id': 3, 'pages': 80, 'language': 'de', 'name': 'A: The Book, Vol. 2'},\r
+ {'id': 4, 'pages': 110, 'language': 'fr', 'name': 'A: The Book, French Edition'},\r
])\r
\r
def test_order(order, result):\r
test_order('-population', [2,3,4,1])\r
test_order('system,-population', [2,4,3,1])\r
\r
- # test invalid order instructions\r
+ # test invalid order instructions...\r
countries.order_by = 'invalid_field,population'\r
assert countries.order_by == ('population',)\r
- # ...for modeltables, this primarily means that only model-based colunns\r
- # are currently sortable at all.\r
+ # ...in case of ModelTables, this primarily means that only\r
+ # model-based colunns are currently sortable at all.\r
countries.order_by = ('custom1', 'custom2')\r
assert countries.order_by == ()\r
\r
# TODO: manual base columns change -> update() call (add as example in docstr here) -> rebuild snapshot: is row cache, column cache etc. reset?\r
# TODO: test that boundcolumn.name works with name overrides and without\r
# TODO: more beautiful auto column names\r
-# TODO: normal tables should handle name overrides differently; the backend data should still use declared_name
\ No newline at end of file
+# TODO: throw an exception on invalid order_by
\ No newline at end of file
assert countries.columns['name'].visible == True\r
assert countries.columns['tld'].visible == False\r
\r
-\r
def test_render():\r
"""For good measure, render some actual templates."""\r
\r
calling_code = tables.NumberColumn(name="cc", verbose_name="Phone Ext.")\r
\r
countries = CountryTable(\r
- [{'name': 'Germany', 'capital': 'Berlin', 'population': 83, 'currency': 'Euro (€)', 'tld': 'de', 'cc': 49},\r
- {'name': 'France', 'population': 64, 'currency': 'Euro (€)', 'tld': 'fr', 'cc': 33},\r
- {'name': 'Netherlands', 'capital': 'Amsterdam', 'cc': '31'},\r
- {'name': 'Austria', 'cc': 43, 'currency': 'Euro (€)', 'population': 8}])\r
+ [{'name': 'Germany', 'capital': 'Berlin', 'population': 83, 'currency': 'Euro (€)', 'tld': 'de', 'calling_code': 49},\r
+ {'name': 'France', 'population': 64, 'currency': 'Euro (€)', 'tld': 'fr', 'calling_code': 33},\r
+ {'name': 'Netherlands', 'capital': 'Amsterdam', 'calling_code': '31'},\r
+ {'name': 'Austria', 'calling_code': 43, 'currency': 'Euro (€)', 'population': 8}])\r
\r
from django.template import Template, Context\r
\r