from django.utils.encoding import StrAndUnicode\r
from columns import Column\r
\r
-__all__ = ('BaseTable', 'Table', 'Row')\r
+__all__ = ('BaseTable', 'Table')\r
\r
def sort_table(data, order_by):\r
"""Sort a list of dicts according to the fieldnames in the\r
instructions.append((o, False,))\r
data.sort(cmp=_cmp)\r
\r
-class Row(object):\r
- def __init__(self, data):\r
- self.data = data\r
- def as_html(self):\r
- pass\r
-\r
class DeclarativeColumnsMetaclass(type):\r
"""\r
Metaclass that converts Column attributes to a dictionary called\r
\r
If ``order_by`` is specified, the data will be sorted accordingly.\r
\r
- Note that unlike a ``Form``, tables are always bound to data.\r
+ Note that unlike a ``Form``, tables are always bound to data. Also\r
+ unlike a form, the ``columns`` attribute is read-only and returns\r
+ ``BoundColum`` wrappers, similar to the ``BoundField``'s you get\r
+ when iterating over a form. This is because the table iterator\r
+ already yields rows, and we need an attribute via which to expose\r
+ the (visible) set of (bound) columns - ``Table.columns`` is simply\r
+ the perfect fit for this. Instead, ``base_colums`` is copied to\r
+ table instances, so modifying that will not touch the class-wide\r
+ column list.\r
"""\r
self._data = data\r
- self._data_cache = None # will store output dataset (ordered...)\r
- self._row_cache = None # will store Row objects\r
+ self._snapshot = None # will store output dataset (ordered...)\r
+ self._rows = None # will store BoundRow objects\r
+ self._columns = Columns(self)\r
self._order_by = order_by\r
\r
- # The base_columns class attribute is the *class-wide* definition\r
- # of columns. Because a particular *instance* of the class might\r
- # want to alter self.columns, we create self.columns here by copying\r
- # ``base_columns``. Instances should always modify self.columns;\r
- # they should not modify self.base_columns.\r
- self.columns = copy.deepcopy(self.base_columns)\r
+ # Make a copy so that modifying this will not touch the class\r
+ # definition. Note that this is different from forms, where the\r
+ # copy is made available in a ``fields`` attribute. See the\r
+ # ``Table`` class docstring for more information.\r
+ self.base_columns = copy.deepcopy(type(self).base_columns)\r
\r
- def _build_data_cache(self):\r
+ def _build_snapshot(self):\r
snapshot = copy.copy(self._data)\r
for row in snapshot:\r
- # delete unknown column, and add missing ones\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 column in self.columns.keys():\r
- if not column in row:\r
- row[column] = self.columns[column].default\r
+ for colname, colobj in self.columns.items():\r
+ if not colname in row:\r
+ row[colname] = colobj.column.default\r
if self.order_by:\r
sort_table(snapshot, self.order_by)\r
- self._data_cache = snapshot\r
+ self._snapshot = snapshot\r
\r
def _get_data(self):\r
- if self._data_cache is None:\r
- self._build_data_cache()\r
- return self._data_cache\r
+ if self._snapshot is None:\r
+ self._build_snapshot()\r
+ return self._snapshot\r
data = property(lambda s: s._get_data())\r
\r
def _set_order_by(self, value):\r
- if self._data_cache is not None:\r
- self._data_cache = None\r
+ if self._snapshot is not None:\r
+ self._snapshot = None\r
# accept both string and tuple instructions\r
self._order_by = (isinstance(value, basestring) \\r
and [value.split(',')] \\r
# validate, remove all invalid order instructions\r
def can_be_used(o):\r
c = (o[:1]=='-' and [o[1:]] or [o])[0]\r
- return c in self.columns and self.columns[c].sortable\r
+ return c in self.base_columns and self.base_columns[c].sortable\r
self._order_by = OrderByTuple([o for o in self._order_by if can_be_used(o)])\r
# TODO: optionally, throw an exception\r
order_by = property(lambda s: s._order_by, _set_order_by)\r
return self.as_html()\r
\r
def __iter__(self):\r
- for name, column in self.columns.items():\r
- yield BoundColumn(self, column, name)\r
+ for row in self.rows:\r
+ yield row\r
\r
def __getitem__(self, name):\r
try:\r
raise KeyError('Key %r not found in Table' % name)\r
return BoundColumn(self, column, name)\r
\r
+ columns = property(lambda s: s._columns) # just to make it readonly\r
+\r
+ def _get_rows(self):\r
+ for row in self.data:\r
+ yield BoundRow(self, row)\r
+ rows = property(_get_rows)\r
+\r
def as_html(self):\r
pass\r
\r
# self.columns is specified.\r
__metaclass__ = DeclarativeColumnsMetaclass\r
\r
+\r
+class Columns(object):\r
+ """Container for spawning BoundColumns.\r
+\r
+ This is bound to a table and provides it's ``columns`` property. It\r
+ provides access to those columns in different ways (iterator,\r
+ item-based, filtered and unfiltered etc)., stuff that would not be\r
+ possible with a simple iterator on the table class.\r
+\r
+ Note that when you define your column using a name override, e.g.\r
+ ``author_name = tables.Column(name="author")``, then the column will\r
+ be exposed by this container as "author", not "author_name".\r
+ """\r
+ def __init__(self, table):\r
+ self.table = table\r
+ self._columns = SortedDict()\r
+\r
+ def _spawn_columns(self):\r
+ # (re)build the "_columns" cache of BoundColumn objects (note that\r
+ # ``base_columns`` might have changed since last time); creating\r
+ # BoundColumn instances can be costly, so we reuse existing ones.\r
+ new_columns = SortedDict()\r
+ for name, column in self.table.base_columns.items():\r
+ name = column.name or name # take into account name overrides\r
+ if name in self._columns:\r
+ new_columns[name] = self._columns[name]\r
+ else:\r
+ new_columns[name] = BoundColumn(self, column, name)\r
+ self._columns = new_columns\r
+\r
+ def all(self):\r
+ self._spawn_columns()\r
+ for column in self._columns.values():\r
+ yield column\r
+\r
+ def items(self):\r
+ self._spawn_columns()\r
+ for r in self._columns.items():\r
+ yield r\r
+\r
+ def keys(self):\r
+ self._spawn_columns()\r
+ for r in self._columns.keys():\r
+ yield r\r
+\r
+ def index(self, name):\r
+ self._spawn_columns()\r
+ return self._columns.keyOrder.index(name)\r
+\r
+ def __iter__(self):\r
+ for column in self.all():\r
+ if column.column.visible:\r
+ yield column\r
+\r
+ def __contains__(self, item):\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
+ else:\r
+ return item in self.all()\r
+\r
+ def __getitem__(self, name):\r
+ """Return a column by name."""\r
+ self._spawn_columns()\r
+ return self._columns[name]\r
+\r
+\r
class BoundColumn(StrAndUnicode):\r
"""'Runtime' version of ``Column`` that is bound to a table instance,\r
and thus knows about the table's data.\r
"""\r
+ def __init__(self, table, column, name):\r
+ self.table = table\r
+ self.column = column\r
+ self.name = column.name or name\r
+ # expose some attributes of the column more directly\r
+ self.sortable = column.sortable\r
+ self.visible = column.visible\r
+\r
def _get_values(self):\r
- # build a list of values used\r
+ # TODO: build a list of values used\r
pass\r
values = property(_get_values)\r
\r
def __unicode__(self):\r
- """Renders this field as an HTML widget."""\r
- return self.as_html()\r
+ return self.column.verbose_name or self.name\r
+\r
+ def as_html(self):\r
+ pass\r
+\r
+class BoundRow(object):\r
+ """Represents a single row of data, bound to a table.\r
+\r
+ Tables will spawn these row objects, wrapping around the actual data\r
+ stored in a row.\r
+ """\r
+ def __init__(self, table, data):\r
+ self.table = table\r
+ self.data = data\r
+\r
+ def __iter__(self):\r
+ for value in self.values:\r
+ yield value\r
+\r
+ def __getitem__(self, name):\r
+ "Returns the value for the column with the given name."\r
+ return self.data[name]\r
+\r
+ def __contains__(self, item):\r
+ """Check by both row object and column name."""\r
+ if isinstance(item, basestring):\r
+ return item in self.table._columns\r
+ else:\r
+ return item in self\r
+\r
+ def _get_values(self):\r
+ for column in self.table.columns:\r
+ yield self[column.name]\r
+ values = property(_get_values)\r
\r
def as_html(self):\r
pass
\ No newline at end of file
-"""Test template specific functionality.\r
+"""Test template specific functionality.\r
\r
-Make sure tables expose their functionality to templates right.\r
+Make sure tables expose their functionality to templates right. This\r
+generally about testing "out"-functionality of the tables, whether\r
+via templates or otherwise. Whether a test belongs here or, say, in\r
+``test_basic``, is not always a clear-cut decision.\r
"""\r
\r
+from py.test import raises\r
import django_tables as tables\r
\r
-def test_for_templates():\r
+def test_order_by():\r
class BookTable(tables.Table):\r
id = tables.Column()\r
name = tables.Column()\r
books.order_by = ('name', '-id')\r
assert str(books.order_by) == 'name,-id'\r
\r
+def test_columns_and_rows():\r
+ class CountryTable(tables.Table):\r
+ name = tables.TextColumn()\r
+ capital = tables.TextColumn(sortable=False)\r
+ population = tables.NumberColumn(verbose_name="Population Size")\r
+ currency = tables.NumberColumn(visible=False, inaccessible=True)\r
+ tld = tables.TextColumn(visible=False, verbose_name="Domain")\r
+ calling_code = tables.NumberColumn(name="cc", verbose_name="Phone Ext.")\r
\r
-"""\r
-<table>\r
-<tr>\r
- {% for column in book.columns %}\r
- <th><a href="{{ column.name }}">{{ column }}</a></th\r
- <th><a href="{% set_url_param "sort" column.name }}">{{ column }}</a></th\r
- {% endfor %}\r
-</tr>\r
-{% for row in book %}\r
- <tr>\r
- {% for value in row %}\r
- <td>{{ value }]</td>\r
- {% endfor %}\r
- </tr>\r
-{% endfor %}\r
-</table>\r
-\r
-OR:\r
-\r
-<table>\r
-{% for row in book %}\r
- <tr>\r
- {% if book.columns.name.visible %}\r
- <td>{{ row.name }]</td>\r
- {% endif %}\r
- {% if book.columns.score.visible %}\r
- <td>{{ row.score }]</td>\r
- {% endif %}\r
- </tr>\r
-{% endfor %}\r
-</table>\r
-\r
-\r
-"""
\ No newline at end of file
+ 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
+\r
+ assert len(list(countries.columns)) == 4\r
+ assert len(list(countries.rows)) == len(list(countries)) == 4\r
+\r
+ # column name override, hidden columns\r
+ assert [c.name for c in countries.columns] == ['name', 'capital', 'population', 'cc']\r
+ # verbose_name, and fallback to field name\r
+ assert [unicode(c) for c in countries.columns] == ['name', 'capital', 'Population Size', 'Phone Ext.']\r
+\r
+ # data yielded by each row matches the defined columns\r
+ for row in countries.rows:\r
+ assert len(list(row)) == len(list(countries.columns))\r
+\r
+ # we can access each column and row by name...\r
+ assert countries.columns['population'].column.verbose_name == "Population Size"\r
+ assert countries.columns['cc'].column.verbose_name == "Phone Ext."\r
+ # ...even invisible ones\r
+ assert countries.columns['tld'].column.verbose_name == "Domain"\r
+ # ...and even inaccessible ones (but accessible to the coder)\r
+ assert countries.columns['currency'].column == countries.base_columns['currency']\r
+ # this also works for rows\r
+ for row in countries:\r
+ row['tld'], row['cc'], row['population']\r
+\r
+ # certain data is available on columns\r
+ assert countries.columns['currency'].sortable == True\r
+ assert countries.columns['capital'].sortable == False\r
+ 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
+ class CountryTable(tables.Table):\r
+ name = tables.TextColumn()\r
+ capital = tables.TextColumn()\r
+ population = tables.NumberColumn(verbose_name="Population Size")\r
+ currency = tables.NumberColumn(visible=False, inaccessible=True)\r
+ tld = tables.TextColumn(visible=False, verbose_name="Domain")\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
+\r
+ from django.template import Template, Context\r
+\r
+ assert Template("{% for column in countries.columns %}{{ column }}/{{ column.name }} {% endfor %}").\\r
+ render(Context({'countries': countries})) == \\r
+ "name/name capital/capital Population Size/population Phone Ext./cc "\r
+\r
+ assert Template("{% for row in countries %}{% for value in row %}{{ value }} {% endfor %}{% endfor %}").\\r
+ render(Context({'countries': countries})) == \\r
+ "Germany Berlin 83 49 France None 64 33 Netherlands Amsterdam None 31 Austria None 8 43 "\r
+\r
+ print Template("{% for row in countries %}{% if countries.columns.name.visible %}{{ row.name }} {% endif %}{% if countries.columns.tld.visible %}{{ row.tld }} {% endif %}{% endfor %}").\\r
+ render(Context({'countries': countries})) == \\r
+ "Germany France Netherlands Austria"
\ No newline at end of file