From: michael <>
Date: Sun, 15 Jun 2008 17:31:17 +0000 (+0000)
Subject: we're now in a state were so far existing features are complete; but more to come
X-Git-Tag: 0.1~30
X-Git-Url: http://git.tremily.us/?a=commitdiff_plain;h=723d4a74e5caa1eee9a4f46f18c0e501b3e07622;p=django-tables2.git
we're now in a state were so far existing features are complete; but more to come
---
diff --git a/django_tables/columns.py b/django_tables/columns.py
index 1ee5e84..66ceb88 100644
--- a/django_tables/columns.py
+++ b/django_tables/columns.py
@@ -17,7 +17,10 @@ class Column(object):
does not provide None for a row, the default will be used instead. Note
that this currently affects ordering.
- You can use ``visible`` to flag the column as hidden.
+ You can use ``visible`` to flag the column as hidden by default.
+ However, this can be overridden by the ``visibility`` argument to the
+ table constructor. If you want to make the column completely unavailable
+ to the user, set ``inaccessible`` to True.
Setting ``sortable`` to False will result in this column being unusable
in ordering.
@@ -26,11 +29,12 @@ class Column(object):
creation_counter = 0
def __init__(self, verbose_name=None, name=None, default=None,
- visible=True, sortable=True):
+ visible=True, inaccessible=False, sortable=True):
self.verbose_name = verbose_name
self.name = name
self.default = default
self.visible = visible
+ self.inaccessible = inaccessible
self.sortable = sortable
self.creation_counter = Column.creation_counter
diff --git a/django_tables/tables.py b/django_tables/tables.py
index 140a9dc..338a5bb 100644
--- a/django_tables/tables.py
+++ b/django_tables/tables.py
@@ -3,7 +3,7 @@ from django.utils.datastructures import SortedDict
from django.utils.encoding import StrAndUnicode
from columns import Column
-__all__ = ('BaseTable', 'Table', 'Row')
+__all__ = ('BaseTable', 'Table')
def sort_table(data, order_by):
"""Sort a list of dicts according to the fieldnames in the
@@ -23,12 +23,6 @@ def sort_table(data, order_by):
instructions.append((o, False,))
data.sort(cmp=_cmp)
-class Row(object):
- def __init__(self, data):
- self.data = data
- def as_html(self):
- pass
-
class DeclarativeColumnsMetaclass(type):
"""
Metaclass that converts Column attributes to a dictionary called
@@ -100,43 +94,52 @@ class BaseTable(object):
If ``order_by`` is specified, the data will be sorted accordingly.
- Note that unlike a ``Form``, tables are always bound to data.
+ Note that unlike a ``Form``, tables are always bound to data. Also
+ unlike a form, the ``columns`` attribute is read-only and returns
+ ``BoundColum`` wrappers, similar to the ``BoundField``'s you get
+ when iterating over a form. This is because the table iterator
+ already yields rows, and we need an attribute via which to expose
+ the (visible) set of (bound) columns - ``Table.columns`` is simply
+ the perfect fit for this. Instead, ``base_colums`` is copied to
+ table instances, so modifying that will not touch the class-wide
+ column list.
"""
self._data = data
- self._data_cache = None # will store output dataset (ordered...)
- self._row_cache = None # will store Row objects
+ self._snapshot = None # will store output dataset (ordered...)
+ self._rows = None # will store BoundRow objects
+ self._columns = Columns(self)
self._order_by = order_by
- # The base_columns class attribute is the *class-wide* definition
- # of columns. Because a particular *instance* of the class might
- # want to alter self.columns, we create self.columns here by copying
- # ``base_columns``. Instances should always modify self.columns;
- # they should not modify self.base_columns.
- self.columns = copy.deepcopy(self.base_columns)
+ # Make a copy so that modifying this will not touch the class
+ # definition. Note that this is different from forms, where the
+ # copy is made available in a ``fields`` attribute. See the
+ # ``Table`` class docstring for more information.
+ self.base_columns = copy.deepcopy(type(self).base_columns)
- def _build_data_cache(self):
+ def _build_snapshot(self):
snapshot = copy.copy(self._data)
for row in snapshot:
- # delete unknown column, and add missing ones
+ # delete unknown columns and add missing ones; note that
+ # self.columns already accounts for column name overrides.
for column in row.keys():
if not column in self.columns:
del row[column]
- for column in self.columns.keys():
- if not column in row:
- row[column] = self.columns[column].default
+ for colname, colobj in self.columns.items():
+ if not colname in row:
+ row[colname] = colobj.column.default
if self.order_by:
sort_table(snapshot, self.order_by)
- self._data_cache = snapshot
+ self._snapshot = snapshot
def _get_data(self):
- if self._data_cache is None:
- self._build_data_cache()
- return self._data_cache
+ if self._snapshot is None:
+ self._build_snapshot()
+ return self._snapshot
data = property(lambda s: s._get_data())
def _set_order_by(self, value):
- if self._data_cache is not None:
- self._data_cache = None
+ if self._snapshot is not None:
+ self._snapshot = None
# accept both string and tuple instructions
self._order_by = (isinstance(value, basestring) \
and [value.split(',')] \
@@ -144,7 +147,7 @@ class BaseTable(object):
# validate, remove all invalid order instructions
def can_be_used(o):
c = (o[:1]=='-' and [o[1:]] or [o])[0]
- return c in self.columns and self.columns[c].sortable
+ return c in self.base_columns and self.base_columns[c].sortable
self._order_by = OrderByTuple([o for o in self._order_by if can_be_used(o)])
# TODO: optionally, throw an exception
order_by = property(lambda s: s._order_by, _set_order_by)
@@ -153,8 +156,8 @@ class BaseTable(object):
return self.as_html()
def __iter__(self):
- for name, column in self.columns.items():
- yield BoundColumn(self, column, name)
+ for row in self.rows:
+ yield row
def __getitem__(self, name):
try:
@@ -163,6 +166,13 @@ class BaseTable(object):
raise KeyError('Key %r not found in Table' % name)
return BoundColumn(self, column, name)
+ columns = property(lambda s: s._columns) # just to make it readonly
+
+ def _get_rows(self):
+ for row in self.data:
+ yield BoundRow(self, row)
+ rows = property(_get_rows)
+
def as_html(self):
pass
@@ -172,18 +182,126 @@ class Table(BaseTable):
# self.columns is specified.
__metaclass__ = DeclarativeColumnsMetaclass
+
+class Columns(object):
+ """Container for spawning BoundColumns.
+
+ This is bound to a table and provides it's ``columns`` property. It
+ provides access to those columns in different ways (iterator,
+ item-based, filtered and unfiltered etc)., stuff that would not be
+ possible with a simple iterator on the table class.
+
+ Note that when you define your column using a name override, e.g.
+ ``author_name = tables.Column(name="author")``, then the column will
+ be exposed by this container as "author", not "author_name".
+ """
+ def __init__(self, table):
+ self.table = table
+ self._columns = SortedDict()
+
+ def _spawn_columns(self):
+ # (re)build the "_columns" cache of BoundColumn objects (note that
+ # ``base_columns`` might have changed since last time); creating
+ # BoundColumn instances can be costly, so we reuse existing ones.
+ new_columns = SortedDict()
+ for name, column in self.table.base_columns.items():
+ name = column.name or name # take into account name overrides
+ if name in self._columns:
+ new_columns[name] = self._columns[name]
+ else:
+ new_columns[name] = BoundColumn(self, column, name)
+ self._columns = new_columns
+
+ def all(self):
+ self._spawn_columns()
+ for column in self._columns.values():
+ yield column
+
+ def items(self):
+ self._spawn_columns()
+ for r in self._columns.items():
+ yield r
+
+ def keys(self):
+ self._spawn_columns()
+ for r in self._columns.keys():
+ yield r
+
+ def index(self, name):
+ self._spawn_columns()
+ return self._columns.keyOrder.index(name)
+
+ def __iter__(self):
+ for column in self.all():
+ if column.column.visible:
+ yield column
+
+ def __contains__(self, item):
+ """Check by both column object and column name."""
+ self._spawn_columns()
+ if isinstance(item, basestring):
+ return item in self.keys()
+ else:
+ return item in self.all()
+
+ def __getitem__(self, name):
+ """Return a column by name."""
+ self._spawn_columns()
+ return self._columns[name]
+
+
class BoundColumn(StrAndUnicode):
"""'Runtime' version of ``Column`` that is bound to a table instance,
and thus knows about the table's data.
"""
+ def __init__(self, table, column, name):
+ self.table = table
+ self.column = column
+ self.name = column.name or name
+ # expose some attributes of the column more directly
+ self.sortable = column.sortable
+ self.visible = column.visible
+
def _get_values(self):
- # build a list of values used
+ # TODO: build a list of values used
pass
values = property(_get_values)
def __unicode__(self):
- """Renders this field as an HTML widget."""
- return self.as_html()
+ return self.column.verbose_name or self.name
+
+ def as_html(self):
+ pass
+
+class BoundRow(object):
+ """Represents a single row of data, bound to a table.
+
+ Tables will spawn these row objects, wrapping around the actual data
+ stored in a row.
+ """
+ def __init__(self, table, data):
+ self.table = table
+ self.data = data
+
+ def __iter__(self):
+ for value in self.values:
+ yield value
+
+ def __getitem__(self, name):
+ "Returns the value for the column with the given name."
+ return self.data[name]
+
+ def __contains__(self, item):
+ """Check by both row object and column name."""
+ if isinstance(item, basestring):
+ return item in self.table._columns
+ else:
+ return item in self
+
+ def _get_values(self):
+ for column in self.table.columns:
+ yield self[column.name]
+ values = property(_get_values)
def as_html(self):
pass
\ No newline at end of file
diff --git a/tests/test_basic.py b/tests/test_basic.py
index 3fe7fe6..3d12f98 100644
--- a/tests/test_basic.py
+++ b/tests/test_basic.py
@@ -48,14 +48,35 @@ def test_declaration():
def test_basic():
class BookTable(tables.Table):
name = tables.Column()
+ answer = tables.Column(default=42)
+ c = tables.Column(name="count", default=1)
books = BookTable([
{'id': 1, 'name': 'Foo: Bar'},
])
# access without order_by works
books.data
- # unknown fields are removed
- for d in books.data:
- assert not 'id' in d
+
+ for r in books.rows:
+ # unknown fields are removed
+ assert 'name' in r
+ assert not 'id' in r
+ # missing data is available as default
+ assert 'answer' in r
+ assert r['answer'] == 42 # note: different from prev. line!
+
+ # all that still works when name overrides are used
+ assert not 'c' in r
+ assert 'count' in r
+ assert r['count'] == 1
+
+ # changing an instance's base_columns does not change the class
+ assert id(books.base_columns) != id(BookTable.base_columns)
+ books.base_columns['test'] = tables.Column()
+ assert not 'test' in BookTable.base_columns
+
+ # make sure the row and column caches work
+ id(list(books.rows)[0]) == id(list(books.rows)[0])
+ id(list(books.columns)[0]) == id(list(books.columns)[0])
def test_sort():
class BookTable(tables.Table):
@@ -90,7 +111,7 @@ def test_sort():
# test invalid order instructions
books.order_by = 'xyz'
assert not books.order_by
- books.columns['language'].sortable = False
+ books.base_columns['language'].sortable = False
books.order_by = 'language'
assert not books.order_by
test_order(('language', 'pages'), [1,3,2,4]) # as if: 'pages'
\ No newline at end of file
diff --git a/tests/test_templates.py b/tests/test_templates.py
index 7d2e5bc..9c0852f 100644
--- a/tests/test_templates.py
+++ b/tests/test_templates.py
@@ -1,11 +1,15 @@
-"""Test template specific functionality.
+"""Test template specific functionality.
-Make sure tables expose their functionality to templates right.
+Make sure tables expose their functionality to templates right. This
+generally about testing "out"-functionality of the tables, whether
+via templates or otherwise. Whether a test belongs here or, say, in
+``test_basic``, is not always a clear-cut decision.
"""
+from py.test import raises
import django_tables as tables
-def test_for_templates():
+def test_order_by():
class BookTable(tables.Table):
id = tables.Column()
name = tables.Column()
@@ -19,38 +23,78 @@ def test_for_templates():
books.order_by = ('name', '-id')
assert str(books.order_by) == 'name,-id'
+def test_columns_and_rows():
+ class CountryTable(tables.Table):
+ name = tables.TextColumn()
+ capital = tables.TextColumn(sortable=False)
+ population = tables.NumberColumn(verbose_name="Population Size")
+ currency = tables.NumberColumn(visible=False, inaccessible=True)
+ tld = tables.TextColumn(visible=False, verbose_name="Domain")
+ calling_code = tables.NumberColumn(name="cc", verbose_name="Phone Ext.")
-"""
-
-
- {% for column in book.columns %}
- {{ column }} | {{ column }}
-{% for row in book %}
-
- {% for value in row %}
- {{ value }] |
- {% endfor %}
-
-{% endfor %}
-
-
-OR:
-
-
-{% for row in book %}
-
- {% if book.columns.name.visible %}
- {{ row.name }] |
- {% endif %}
- {% if book.columns.score.visible %}
- {{ row.score }] |
- {% endif %}
-
-{% endfor %}
-
-
-
-"""
\ No newline at end of file
+ countries = CountryTable(
+ [{'name': 'Germany', 'capital': 'Berlin', 'population': 83, 'currency': 'Euro (â¬)', 'tld': 'de', 'cc': 49},
+ {'name': 'France', 'population': 64, 'currency': 'Euro (â¬)', 'tld': 'fr', 'cc': 33},
+ {'name': 'Netherlands', 'capital': 'Amsterdam', 'cc': '31'},
+ {'name': 'Austria', 'cc': 43, 'currency': 'Euro (â¬)', 'population': 8}])
+
+ assert len(list(countries.columns)) == 4
+ assert len(list(countries.rows)) == len(list(countries)) == 4
+
+ # column name override, hidden columns
+ assert [c.name for c in countries.columns] == ['name', 'capital', 'population', 'cc']
+ # verbose_name, and fallback to field name
+ assert [unicode(c) for c in countries.columns] == ['name', 'capital', 'Population Size', 'Phone Ext.']
+
+ # data yielded by each row matches the defined columns
+ for row in countries.rows:
+ assert len(list(row)) == len(list(countries.columns))
+
+ # we can access each column and row by name...
+ assert countries.columns['population'].column.verbose_name == "Population Size"
+ assert countries.columns['cc'].column.verbose_name == "Phone Ext."
+ # ...even invisible ones
+ assert countries.columns['tld'].column.verbose_name == "Domain"
+ # ...and even inaccessible ones (but accessible to the coder)
+ assert countries.columns['currency'].column == countries.base_columns['currency']
+ # this also works for rows
+ for row in countries:
+ row['tld'], row['cc'], row['population']
+
+ # certain data is available on columns
+ assert countries.columns['currency'].sortable == True
+ assert countries.columns['capital'].sortable == False
+ assert countries.columns['name'].visible == True
+ assert countries.columns['tld'].visible == False
+
+
+def test_render():
+ """For good measure, render some actual templates."""
+
+ class CountryTable(tables.Table):
+ name = tables.TextColumn()
+ capital = tables.TextColumn()
+ population = tables.NumberColumn(verbose_name="Population Size")
+ currency = tables.NumberColumn(visible=False, inaccessible=True)
+ tld = tables.TextColumn(visible=False, verbose_name="Domain")
+ calling_code = tables.NumberColumn(name="cc", verbose_name="Phone Ext.")
+
+ countries = CountryTable(
+ [{'name': 'Germany', 'capital': 'Berlin', 'population': 83, 'currency': 'Euro (â¬)', 'tld': 'de', 'cc': 49},
+ {'name': 'France', 'population': 64, 'currency': 'Euro (â¬)', 'tld': 'fr', 'cc': 33},
+ {'name': 'Netherlands', 'capital': 'Amsterdam', 'cc': '31'},
+ {'name': 'Austria', 'cc': 43, 'currency': 'Euro (â¬)', 'population': 8}])
+
+ from django.template import Template, Context
+
+ assert Template("{% for column in countries.columns %}{{ column }}/{{ column.name }} {% endfor %}").\
+ render(Context({'countries': countries})) == \
+ "name/name capital/capital Population Size/population Phone Ext./cc "
+
+ assert Template("{% for row in countries %}{% for value in row %}{{ value }} {% endfor %}{% endfor %}").\
+ render(Context({'countries': countries})) == \
+ "Germany Berlin 83 49 France None 64 33 Netherlands Amsterdam None 31 Austria None 8 43 "
+
+ print Template("{% for row in countries %}{% if countries.columns.name.visible %}{{ row.name }} {% endif %}{% if countries.columns.tld.visible %}{{ row.tld }} {% endif %}{% endfor %}").\
+ render(Context({'countries': countries})) == \
+ "Germany France Netherlands Austria"
\ No newline at end of file