columns[name] = BoundColumn(self.table, column, name)
self._columns = columns
- def all(self):
+ def iternames(self):
+ return (name for name, column in self.iteritems())
+
+ def names(self):
+ return list(self.iternames())
+
+ def iterall(self):
"""
Return an iterator that exposes all :class:`.BoundColumn` objects,
regardless of visiblity or sortability.
"""
- self._spawn_columns()
- return (column for name, column in self._columns.iteritems())
+ return (column for name, column in self.iteritems())
- def items(self):
+ def all(self):
+ return list(self.iterall())
+
+ def iteritems(self):
"""
Return an iterator of ``(name, column)`` pairs (where ``column`` is a
:class:`.BoundColumn` object).
"""
self._spawn_columns()
- return self._columns.iteritems()
+ if self.table.sequence:
+ return ((x, self._columns[x]) for x in self.table.sequence)
+ else:
+ return self._columns.iteritems()
- def sortable(self):
+ def items(self):
+ return list(self.iteritems())
+
+ def itersortable(self):
"""
Same as :meth:`.BoundColumns.all` but only returns sortable
:class:`.BoundColumn` objects.
"""
return ifilter(lambda x: x.sortable, self.all())
- def visible(self):
+ def sortable(self):
+ return list(self.itersortable())
+
+ def itervisible(self):
"""
Same as :meth:`.sortable` but only returns visible
:class:`.BoundColumn` objects.
"""
return ifilter(lambda x: x.visible, self.all())
+ def visible(self):
+ return list(self.itervisible())
+
def __iter__(self):
"""
Convenience API with identical functionality to :meth:`visible`.
"""
- return self.visible()
+ return self.itervisible()
def __contains__(self, item):
"""
*item* can either be a :class:`.BoundColumn` object, or the name of a
column.
"""
- self._spawn_columns()
if isinstance(item, basestring):
- for key in self._columns.keys():
- if item == key:
- return True
- return False
+ return item in self.iternames()
else:
- return item in self.all()
+ # let's assume we were given a column
+ return item in self.iterall()
def __len__(self):
"""
- Return how many :class:`BoundColumn` objects are contained.
+ Return how many :class:`BoundColumn` objects are contained (and
+ visible).
"""
self._spawn_columns()
- return len([1 for c in self._columns.values() if c.visible])
+ return len(self.visible())
def __getitem__(self, index):
"""
QUERYSET_ACCESSOR_SEPARATOR = '__'
+def _validate_sequence(seq, column_names):
+ """
+ Validates a sequence against a list of column names. It checks that "..."
+ is used at most once, either at the head or tail, and that if it's not
+ used, every column is specified.
+
+ :raises: ValueError on error.
+ """
+ if (seq.count("...") > 1 or (seq.count("...") == 1 and seq[0] != "..."
+ and seq[-1] != "...")):
+ raise ValueError("'...' must be used at most once in 'sequence', "
+ "either at the head or tail.")
+
+ if "..." in seq:
+ extra = (set(seq) - set(("...", ))).difference(column_names)
+ if extra:
+ raise ValueError(u"'sequence' defined but names columns that do "
+ u"not exist in the table. Remove '%s'."
+ % "', '".join(extra))
+ else:
+ diff = set(seq) ^ set(column_names)
+ if diff:
+ print 'seq', seq
+ print 'column_names', column_names
+ raise ValueError(u"'sequence' defined but does not match columns. "
+ u"Fix '%s' or possibly add '...' to head or tail."
+ % "', '".join(diff))
+
class TableData(object):
"""
Exposes a consistent API for :term:`table data`. It currently supports a
def __new__(cls, name, bases, attrs):
"""Ughhh document this :)"""
# extract declared columns
- columns = [(name, attrs.pop(name)) for name, column in attrs.items()
+ columns = [(name_, attrs.pop(name_)) for name_, column in attrs.items()
if isinstance(column, Column)]
columns.sort(lambda x, y: cmp(x[1].creation_counter,
y[1].creation_counter))
for ex in opts.exclude:
if ex in attrs["base_columns"]:
attrs["base_columns"].pop(ex)
+ if opts.sequence:
+ _validate_sequence(opts.sequence, attrs["base_columns"].keys())
+ if "..." not in opts.sequence:
+ attrs["base_columns"] = SortedDict(((s, attrs["base_columns"][s]) for s in opts.sequence))
+ elif opts.sequence[0] == "...":
+ for s in opts.sequence[1:]:
+ attrs["base_columns"][s] = attrs["base_columns"].pop(s) # append
+ elif opts.sequence[-1] == "...":
+ for s in opts.sequence[:-1]:
+ attrs["base_columns"].insert(0, s, attrs["base_columns"].pop(s))
return type.__new__(cls, name, bases, attrs)
if isinstance(order_by, basestring):
order_by = (order_by, )
self.order_by = OrderByTuple(order_by)
+ self.sequence = getattr(options, "sequence", None)
self.sortable = getattr(options, "sortable", True)
TableDataClass = TableData
def __init__(self, data, order_by=None, sortable=None, empty_text=None,
- exclude=None, attrs=None):
+ exclude=None, attrs=None, sequence=None):
self._rows = BoundRows(self)
self._columns = BoundColumns(self)
self._data = self.TableDataClass(data=data, table=self)
self.attrs = attrs
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
# 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.
for ex in self.exclude:
if ex in self.base_columns:
self.base_columns.pop(ex)
+ self.sequence = sequence
+ if order_by is None:
+ self.order_by = self._meta.order_by
+ else:
+ self.order_by = order_by
+
def __unicode__(self):
return self.as_html()
self._order_by = order_by
self._data.order_by(order_by)
+ @property
+ def sequence(self):
+ return (self._sequence if self._sequence is not None
+ else self._meta.sequence)
+
+ @sequence.setter
+ def sequence(self, value):
+ if value:
+ _validate_sequence(value, self.base_columns.keys())
+ self._sequence = value
+
@property
def sortable(self):
return (self._sortable if self._sortable is not None
:meth:`~django_tables.tables.Table.as_html` or the
:ref:`template-tags.render_table` template tag.
+ :type: ``dict``
+ :default: ``{}``
+
This is typically used to enable a theme for a table (which is done by
adding a CSS class to the ``<table>`` element). i.e.::
class Meta:
attrs = {"class": "paleblue"}
- :type: ``dict``
-
- Default: ``{}``
-
.. note::
This functionality is also available via the ``attrs`` keyword
Defines the text to display when the table has no rows.
+ :type: ``string``
+ :default: ``None``
+
+ If the table is empty and ``bool(empty_text)`` is ``True``, a row is
+ displayed containing ``empty_text``. This is allows a message such as
+ *There are currently no FOO.* to be displayed.
+
.. note::
This functionality is also available via the ``empty_text`` keyword
.. attribute:: sortable
- The default value for determining if a :class:`.Column` is sortable.
+ Whether columns are by default sortable, or not. i.e. the fallback for
+ value for a column's sortable value.
If the ``Table`` and ``Column`` don't specify a value, a column's
``sortable`` value will fallback to this. object specify. This provides
from django_attest import TransactionTestContext
from django.test.client import RequestFactory
from django.template import Context, Template
+from django.core.exceptions import ImproperlyConfigured
import django_tables as tables
from django_tables import utils, A
from .testapp.models import Person
Assert(SimpleTable([]).columns['name'].sortable) is True
+@general.test
+def sequence():
+ """
+ Ensures that the sequence of columns is configurable.
+ """
+ class TestTable(tables.Table):
+ a = tables.Column()
+ b = tables.Column()
+ c = tables.Column()
+ Assert(["a", "b", "c"]) == TestTable([]).columns.names()
+ Assert(["b", "a", "c"]) == TestTable([], sequence=("b", "a", "c")).columns.names()
+
+ class TestTable2(TestTable):
+ class Meta:
+ sequence = ("b", "a", "c")
+ Assert(["b", "a", "c"]) == TestTable2([]).columns.names()
+ Assert(["a", "b", "c"]) == TestTable2([], sequence=("a", "b", "c")).columns.names()
+
+ # BAD, all columns must be specified, or must use "..."
+ with Assert.raises(ValueError):
+ class TestTable3(TestTable):
+ class Meta:
+ sequence = ("a", )
+ with Assert.raises(ValueError):
+ TestTable([], sequence=("a", ))
+
+ # GOOD, using a single "..." allows you to only specify some columns. The
+ # remaining columns are ordered based on their definition order
+ class TestTable4(TestTable):
+ class Meta:
+ sequence = ("b", "...")
+ Assert(["b", "a", "c"]) == TestTable4([]).columns.names()
+ Assert(["b", "a", "c"]) == TestTable([], sequence=("b", "...")).columns.names()
+
+ class TestTable5(TestTable):
+ class Meta:
+ sequence = ("...", "b")
+ Assert(["a", "c", "b"]) == TestTable5([]).columns.names()
+ Assert(["a", "c", "b"]) == TestTable([], sequence=("...", "b")).columns.names()
+
+ class TestTable6(tables.Table):
+ class Meta:
+ sequence = ("...")
+ Assert(["a", "b", "c"]) == TestTable6([]).columns.names()
+ Assert(["a", "b", "c"]) == TestTable([], sequence=("...")).columns.names()
+
+
linkcolumn = Tests()
linkcolumn.context(TransactionTestContext())