From: Bradley Ayers Date: Sun, 10 Apr 2011 00:02:06 +0000 (+1000) Subject: Rendered tables now include empty_text X-Git-Tag: v0.4.0.beta6~1^2~3 X-Git-Url: http://git.tremily.us/?a=commitdiff_plain;h=d20176a1fc1a6e21bc509647bdd339d6e3149b6a;p=django-tables2.git Rendered tables now include empty_text --- diff --git a/.gitignore b/.gitignore index 6eabac0..3aacb65 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *.pyc /*.komodoproject +/*.tmproj /*.egg-info/ /*.egg /MANIFEST diff --git a/django_tables/columns.py b/django_tables/columns.py index 9bb29e9..cf46a63 100644 --- a/django_tables/columns.py +++ b/django_tables/columns.py @@ -297,32 +297,30 @@ class TemplateColumn(Column): class BoundColumn(object): - """A *runtime* version of :class:`Column`. The difference between - :class:`BoundColumn` and :class:`Column`, is that :class:`BoundColumn` - objects are of the relationship between a :class:`Column` and a - :class:`Table`. This means that it knows the *name* given to the - :class:`Column`. + """A *runtime* version of :class:`.Column`. The difference between + ``BoundColumn`` and ``Column``, is that ``BoundColumn`` objects are of + the relationship between a ``Column`` and a :class:`.Table`. This + means that it knows the *name* given to the ``Column``. - For convenience, all :class:`Column` properties are available from this + For convenience, all :class:`.Column` properties are available from this class. - - :type table: :class:`Table` object + :type table: :class:`.Table` object :param table: the table in which this column exists - :type column: :class:`Column` object + :type column: :class:`.Column` object :param column: the type of column - :type name: :class:`basestring` object + :type name: ``basestring`` object :param name: the variable name of the column used to when defining the - :class:`Table`. Example: + :class:`.Table`. Example: .. code-block:: python class SimpleTable(tables.Table): age = tables.Column() - `age` is the name. + ``age`` is the name. """ def __init__(self, table, column, name): @@ -333,11 +331,6 @@ class BoundColumn(object): def __unicode__(self): return self.verbose_name - @property - def column(self): - """Returns the :class:`Column` object for this column.""" - return self._column - @property def accessor(self): """ @@ -347,6 +340,11 @@ class BoundColumn(object): """ return self.column.accessor or A(self.name) + @property + def column(self): + """Returns the :class:`.Column` object for this column.""" + return self._column + @property def default(self): """Returns the default value for this column.""" @@ -355,8 +353,7 @@ class BoundColumn(object): @property def header(self): """ - Return the value that should be used in the header cell for this - column. + The value that should be used in the header cell for this column. """ return self.column.header or self.verbose_name @@ -370,7 +367,7 @@ class BoundColumn(object): def order_by(self): """ If this column is sorted, return the associated :class:`.OrderBy` - instance, otherwise :const:`None`. + instance, otherwise ``None``. """ try: @@ -380,17 +377,10 @@ class BoundColumn(object): @property def sortable(self): - """ - Return a :class:`bool` depending on whether this column is - sortable. - - """ + """Return a ``bool`` depending on whether this column is sortable.""" if self.column.sortable is not None: return self.column.sortable - elif self.table._meta.sortable is not None: - return self.table._meta.sortable - else: - return True # the default value + return self.table.sortable @property def table(self): @@ -425,11 +415,11 @@ class BoundColumns(object): item-based, filtered and unfiltered etc), stuff that would not be possible with a simple iterator in the table class. - A :class:`BoundColumns` object is a container for holding - :class:`BoundColumn` objects. It provides methods that make accessing - columns easier than if they were stored in a :class:`list` or - :class:`dict`. :class:`Columns` has a similar API to a :class:`dict` (it - actually uses a :class:`SortedDict` interally). + A :class:`.BoundColumns` object is a container for holding + :class:`.BoundColumn` objects. It provides methods that make accessing + columns easier than if they were stored in a ``list`` or + :class:`dict`. :class:`Columns` has a similar API to a ``dict`` (it + actually uses a ``SortedDict`` interally). At the moment you'll only come across this class when you access a :attr:`.Table.columns` property. @@ -480,16 +470,10 @@ class BoundColumns(object): for r in self._columns.items(): yield r - def names(self): - """Return an iterator of column names.""" - self._spawn_columns() - for r in self._columns.keys(): - yield r - def sortable(self): """ - Same as :meth:`.BoundColumns.all` but only returns sortable :class:`BoundColumn` - objects. + Same as :meth:`.BoundColumns.all` but only returns sortable + :class:`.BoundColumn` objects. This is useful in templates, where iterating over the full set and checking ``{% if column.sortable %}`` can be problematic in @@ -518,15 +502,18 @@ class BoundColumns(object): return self.visible() def __contains__(self, item): - """Check if a column is contained within a :class:`Columns` object. + """Check if a column is contained within a :class:`.Columns` object. - *item* can either be a :class:`BoundColumn` object, or the name of a + *item* can either be a :class:`.BoundColumn` object, or the name of a column. """ self._spawn_columns() if isinstance(item, basestring): - return item in self.names() + for key in self._columns.keys(): + if item == key: + return True + return False else: return item in self.all() diff --git a/django_tables/tables.py b/django_tables/tables.py index 91fcbdf..bc92e9e 100644 --- a/django_tables/tables.py +++ b/django_tables/tables.py @@ -83,6 +83,7 @@ class TableData(object): def __getitem__(self, index): """Forwards indexing accesses to underlying data""" return (self.list if hasattr(self, 'list') else self.queryset)[index] + class DeclarativeColumnsMetaclass(type): @@ -138,7 +139,7 @@ class TableOptions(object): """ super(TableOptions, self).__init__() - self.sortable = getattr(options, 'sortable', None) + self.sortable = getattr(options, 'sortable', True) order_by = getattr(options, 'order_by', ()) if isinstance(order_by, basestring): order_by = (order_by, ) @@ -184,19 +185,16 @@ class Table(StrAndUnicode): self._rows = BoundRows(self) # bound rows self._columns = BoundColumns(self) # bound columns self._data = self.TableDataClass(data=data, table=self) - + self.empty_text = empty_text + self.sortable = sortable if order_by is None: self.order_by = self._meta.order_by else: self.order_by = order_by - self.sortable = sortable - self.empty_text = empty_text - # 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. + # copy is made available in a ``fields`` attribute. self.base_columns = copy.deepcopy(type(self).base_columns) def __unicode__(self): @@ -217,15 +215,17 @@ class Table(StrAndUnicode): of column names. """ - # accept both string and tuple instructions + # accept string order_by = value.split(',') if isinstance(value, basestring) else value + # accept None order_by = () if order_by is None else order_by new = [] - # validate, raise exception on failure + # everything's been converted to a iterable, accept iterable! for o in order_by: - name = OrderBy(o).bare + ob = OrderBy(o) + name = ob.bare if name in self.columns and self.columns[name].sortable: - new.append(o) + new.append(ob) order_by = OrderByTuple(new) self._order_by = order_by self._data.order_by(order_by) diff --git a/django_tables/templates/django_tables/basic_table.html b/django_tables/templates/django_tables/basic_table.html index a564e7b..a3ada0b 100644 --- a/django_tables/templates/django_tables/basic_table.html +++ b/django_tables/templates/django_tables/basic_table.html @@ -14,6 +14,10 @@ {{ value }} {% endfor %} + {% empty %} + {% if table.empty_text %} + {{ table.empty_text }} + {% endif %} {% endfor %} diff --git a/django_tables/templates/django_tables/table.html b/django_tables/templates/django_tables/table.html index c6d12c0..21699bb 100644 --- a/django_tables/templates/django_tables/table.html +++ b/django_tables/templates/django_tables/table.html @@ -24,6 +24,10 @@ {{ cell }} {% endfor %} + {% empty %} + {% if table.empty_text %} + {{ table.empty_text }} + {% endif %} {% endfor %} diff --git a/docs/index.rst b/docs/index.rst index ac3622b..23b198f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -486,7 +486,12 @@ API Reference .. attribute:: sortable - Does the table support ordering? + The default value for determining if a :class:`.Column` is sortable. + + If the ``Table`` and ``Column`` don't specify a value, a column's + ``sortable`` value will fallback to this. object specify. This provides + an easy mechanism to disable sorting on an entire table, without adding + ``sortable=False`` to each ``Column`` in a ``Table``. Default: :const:`True` diff --git a/tests/__init__.py b/tests/__init__.py index 5b434e4..0774bec 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -27,6 +27,7 @@ from .templates import templates from .models import models from .utils import utils from .rows import rows +from .columns import columns -everything = Tests([core, templates, models, utils, rows]) +everything = Tests([core, templates, models, utils, rows, columns]) diff --git a/tests/columns.py b/tests/columns.py new file mode 100644 index 0000000..d429ec9 --- /dev/null +++ b/tests/columns.py @@ -0,0 +1,30 @@ +"""Test the core table functionality.""" +from attest import Tests, Assert +import django_tables as tables +from django_tables import utils + + +columns = Tests() + + +@columns.test +def sortable(): + class SimpleTable(tables.Table): + name = tables.Column() + Assert(SimpleTable([]).columns['name'].sortable) is True + + class SimpleTable(tables.Table): + name = tables.Column() + + class Meta: + sortable = False + Assert(SimpleTable([]).columns['name'].sortable) is False + + class SimpleTable(tables.Table): + name = tables.Column() + + class Meta: + sortable = True + Assert(SimpleTable([]).columns['name'].sortable) is True + + diff --git a/tests/core.py b/tests/core.py index 66cbe4f..b329b07 100644 --- a/tests/core.py +++ b/tests/core.py @@ -117,6 +117,34 @@ def sorting(ctx): table = ctx.SortedTable(ctx.memory_data) Assert(1) == table.rows[0]['i'] + # column's can't be sorted if they're not allowed to be + class TestTable(tables.Table): + a = tables.Column(sortable=False) + b = tables.Column() + + table = TestTable([], order_by='a') + Assert(table.order_by) == () + + table = TestTable([], order_by='b') + Assert(table.order_by) == ('b', ) + + # sorting disabled by default + class TestTable(tables.Table): + a = tables.Column(sortable=True) + b = tables.Column() + + class Meta: + sortable = False + + table = TestTable([], order_by='a') + Assert(table.order_by) == ('a', ) + + table = TestTable([], order_by='b') + Assert(table.order_by) == () + + table = TestTable([], sortable=True, order_by='b') + Assert(table.order_by) == ('b', ) + @core.test def column_count(context): @@ -173,3 +201,24 @@ def pagination(): with Assert.raises(Http404) as error: books.paginate(Paginator, page=9999, per_page=10) books.paginate(Paginator, page='abc', per_page=10) + + +@core.test +def empty_text(): + class TestTable(tables.Table): + a = tables.Column() + + table = TestTable([]) + Assert(table.empty_text) is None + + class TestTable(tables.Table): + a = tables.Column() + + class Meta: + empty_text = 'nothing here' + + table = TestTable([]) + Assert(table.empty_text) == 'nothing here' + + table = TestTable([], empty_text='still nothing') + Assert(table.empty_text) == 'still nothing' diff --git a/tests/templates.py b/tests/templates.py index f6f1700..2a5f482 100644 --- a/tests/templates.py +++ b/tests/templates.py @@ -11,6 +11,7 @@ from django.template import Template, Context from django.http import HttpRequest import django_tables as tables from attest import Tests, Assert +from xml.etree import ElementTree as ET templates = Tests() @@ -41,8 +42,29 @@ def context(): @templates.test def as_html(context): - countries = context.CountryTable(context.data) - countries.as_html() + table = context.CountryTable(context.data) + root = ET.fromstring(table.as_html()) + Assert(len(root.findall('.//thead/tr'))) == 1 + Assert(len(root.findall('.//thead/tr/th'))) == 4 + Assert(len(root.findall('.//tbody/tr'))) == 4 + Assert(len(root.findall('.//tbody/tr/td'))) == 16 + + # no data with no empty_text + table = context.CountryTable([]) + root = ET.fromstring(table.as_html()) + Assert(1) == len(root.findall('.//thead/tr')) + Assert(4) == len(root.findall('.//thead/tr/th')) + Assert(0) == len(root.findall('.//tbody/tr')) + + # no data WITH empty_text + table = context.CountryTable([], empty_text='this table is empty') + root = ET.fromstring(table.as_html()) + Assert(1) == len(root.findall('.//thead/tr')) + Assert(4) == len(root.findall('.//thead/tr/th')) + Assert(1) == len(root.findall('.//tbody/tr')) + Assert(1) == len(root.findall('.//tbody/tr/td')) + Assert(int(root.find('.//tbody/tr/td').attrib['colspan'])) == len(root.findall('.//thead/tr/th')) + Assert(root.find('.//tbody/tr/td').text) == 'this table is empty' @templates.test @@ -69,7 +91,36 @@ def custom_rendering(context): @templates.test def templatetag(context): # ensure it works with a multi-order-by - countries = context.CountryTable(context.data, - order_by=('name', 'population')) + table = context.CountryTable(context.data, order_by=('name', 'population')) + t = Template('{% load django_tables %}{% render_table table %}') + html = t.render(Context({'request': HttpRequest(), 'table': table})) + + root = ET.fromstring(html) + Assert(len(root.findall('.//thead/tr'))) == 1 + Assert(len(root.findall('.//thead/tr/th'))) == 4 + Assert(len(root.findall('.//tbody/tr'))) == 4 + Assert(len(root.findall('.//tbody/tr/td'))) == 16 + + # no data with no empty_text + table = context.CountryTable([]) + t = Template('{% load django_tables %}{% render_table table %}') + html = t.render(Context({'request': HttpRequest(), 'table': table})) + root = ET.fromstring(html) + Assert(len(root.findall('.//thead/tr'))) == 1 + Assert(len(root.findall('.//thead/tr/th'))) == 4 + Assert(len(root.findall('.//tbody/tr'))) == 0 + + # no data WITH empty_text + table = context.CountryTable([], empty_text='this table is empty') t = Template('{% load django_tables %}{% render_table table %}') - t.render(Context({'request': HttpRequest(), 'table': countries})) + html = t.render(Context({'request': HttpRequest(), 'table': table})) + root = ET.fromstring(html) + Assert(len(root.findall('.//thead/tr'))) == 1 + Assert(len(root.findall('.//thead/tr/th'))) == 4 + Assert(len(root.findall('.//tbody/tr'))) == 1 + Assert(len(root.findall('.//tbody/tr/td'))) == 1 + Assert(int(root.find('.//tbody/tr/td').attrib['colspan'])) == len(root.findall('.//thead/tr/th')) + Assert(root.find('.//tbody/tr/td').text) == 'this table is empty' + + +