*.pyc
/*.komodoproject
+/*.tmproj
/*.egg-info/
/*.egg
/MANIFEST
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):
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):
"""
"""
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."""
@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
def order_by(self):
"""
If this column is sorted, return the associated :class:`.OrderBy`
- instance, otherwise :const:`None`.
+ instance, otherwise ``None``.
"""
try:
@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):
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.
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
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()
def __getitem__(self, index):
"""Forwards indexing accesses to underlying data"""
return (self.list if hasattr(self, 'list') else self.queryset)[index]
+
class DeclarativeColumnsMetaclass(type):
"""
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, )
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):
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)
<td>{{ value }}</td>
{% endfor %}
</tr>
+ {% empty %}
+ {% if table.empty_text %}
+ <tr><td colspan="{{ table.columns|length }}">{{ table.empty_text }}</td></tr>
+ {% endif %}
{% endfor %}
</tbody>
</table>
<td>{{ cell }}</td>
{% endfor %}
</tr>
+ {% empty %}
+ {% if table.empty_text %}
+ <tr><td colspan="{{ table.columns|length }}">{{ table.empty_text }}</td></tr>
+ {% endif %}
{% endfor %}
</tbody>
</table>
.. 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`
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])
--- /dev/null
+"""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
+
+
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):
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'
from django.http import HttpRequest
import django_tables as tables
from attest import Tests, Assert
+from xml.etree import ElementTree as ET
templates = Tests()
@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
@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'
+
+
+