* Added the ability to explicitly specify the sequence of columns. This resolves #11
from django.template import Context
from django.utils.encoding import StrAndUnicode
from django.db.models.query import QuerySet
+from itertools import chain
from .utils import OrderBy, OrderByTuple, Accessor, AttributeDict
from .rows import BoundRows, BoundRow
from .columns import BoundColumns, Column
QUERYSET_ACCESSOR_SEPARATOR = '__'
-def _validate_sequence(seq, column_names):
+class Sequence(list):
"""
- 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.
+ Represents a column sequence, e.g. ("first_name", "...", "last_name")
- :raises: ValueError on error.
+ This is used to represent ``Table.Meta.sequence`` or the Table
+ constructors's ``sequence`` keyword argument.
+
+ The sequence must be a list of column names and is used to specify the
+ order of the columns on a table. Optionally a "..." item can be inserted,
+ which is treated as a *catch-all* for column names that aren't explicitly
+ specified.
"""
- 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))
+ def expand(self, columns):
+ """
+ Expands the "..." item in the sequence into the appropriate column
+ names that should be placed there.
+
+ :raises: ``ValueError`` if the sequence is invalid for the columns.
+ """
+ # validation
+ if self.count("...") > 1:
+ raise ValueError("'...' must be used at most once in a sequence.")
+ elif "..." in self:
+ # Check for columns in the sequence that don't exist in *columns*
+ extra = (set(self) - set(("...", ))).difference(columns)
+ if extra:
+ raise ValueError(u"sequence contains columns that do not exist"
+ u" in the table. Remove '%s'."
+ % "', '".join(extra))
+ else:
+ diff = set(self) ^ set(columns)
+ if diff:
+ raise ValueError(u"sequence does not match columns. Fix '%s' "
+ u"or possibly add '...'." % "', '".join(diff))
+ # everything looks good, let's expand the "..." item
+ columns = columns[:] # don't modify
+ head = []
+ tail = []
+ target = head # start by adding things to the head
+ for name in self:
+ if name == "...":
+ # now we'll start adding elements to the tail
+ target = tail
+ continue
+ else:
+ target.append(columns.pop(columns.index(name)))
+ self[:] = list(chain(head, columns, tail))
+
class TableData(object):
"""
"""Ughhh document this :)"""
# extract declared columns
columns = [(name_, attrs.pop(name_)) for name_, column in attrs.items()
- if isinstance(column, Column)]
+ if isinstance(column, Column)]
columns.sort(lambda x, y: cmp(x[1].creation_counter,
y[1].creation_counter))
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))
+ opts.sequence.expand(attrs["base_columns"].keys())
+ attrs["base_columns"] = SortedDict(((x, attrs["base_columns"][x]) for x in opts.sequence))
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.sequence = Sequence(getattr(options, "sequence", ()))
self.sortable = getattr(options, "sortable", True)
# 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.
- self.base_columns = copy.deepcopy(type(self).base_columns)
+ self.base_columns = copy.deepcopy(self.__class__.base_columns)
self.exclude = exclude or ()
for ex in self.exclude:
if ex in self.base_columns:
@sequence.setter
def sequence(self, value):
if value:
- _validate_sequence(value, self.base_columns.keys())
+ value = Sequence(value)
+ value.expand(self.base_columns.keys())
self._sequence = value
@property
# The short X.Y version.
version = '0.5.0'
# The full version, including alpha/beta/rc tags.
-release = '0.5.0.dev'
+release = '0.5.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
Ordering
========
-Changing the way a table is ordered is easy and can be controlled via the
-:attr:`.Table.Meta.order_by` option. The following examples all achieve the
-same thing:
+.. note::
+
+ If you want to change the order in which columns are displayed, see
+ :attr:`Table.Meta.sequence`. Alternatively if you're interested in the
+ order of records within the table, read on.
+
+Changing the way records in a table are ordered is easy and can be controlled
+via the :attr:`.Table.Meta.order_by` option. The following examples all achieve
+the same thing:
.. code-block:: python
.. attribute:: exclude
Defines which columns should be excluded from the table. This is useful
- in subclasses to exclude columns in a parent. e.g.
+ in subclasses to exclude columns in a parent.
+
+ :type: tuple of ``string`` objects
+ :default: ``()``
+
+ Example::
>>> class Person(tables.Table):
... first_name = tables.Column()
>>> ForgetfulPerson.base_columns
{'first_name': <django_tables.columns.Column object at 0x10046df10>}
- :type: tuple of ``string`` objects
-
- Default: ``()``
-
.. note::
This functionality is also available via the ``exclude`` keyword
The default ordering. e.g. ``('name', '-age')``. A hyphen ``-`` can be
used to prefix a column name to indicate *descending* order.
- :type: :class:`tuple`
-
- Default: ``()``
+ :type: ``tuple``
+ :default: ``()``
.. note::
This functionality is also available via the ``order_by`` keyword
argument to a table's constructor.
+ .. attribute:: sequence
+
+ The sequence of the table columns. This allows the default order of
+ columns (the order they were defined in the Table) to be overridden.
+
+ :type: any iterable (e.g. ``tuple`` or ``list``)
+ :default: ``()``
+
+ The special item ``"..."`` can be used as a placeholder that will be
+ replaced with all the columns that weren't explicitly listed. This
+ allows you to add columns to the front or back when using inheritence.
+
+ Example::
+
+ >>> class Person(tables.Table):
+ ... first_name = tables.Column()
+ ... last_name = tables.Column()
+ ...
+ ... class Meta:
+ ... sequence = ("last_name", "...")
+ ...
+ >>> Person.base_columns.keys()
+ ['last_name', 'first_name']
+
+ The ``"..."`` item can be used at most once in the sequence value. If
+ it's not used, every column *must* be explicitly included. e.g. in the
+ above example, ``sequence = ("last_name", )`` would be **invalid**
+ because neither ``"..."`` or ``"first_name"`` where included.
+
+ .. note::
+
+ This functionality is also available via the ``sequence`` keyword
+ argument to a table's constructor.
+
.. attribute:: sortable
Whether columns are by default sortable, or not. i.e. the fallback for
value for a column's sortable value.
+ :type: ``bool``
+ :default: ``True``
+
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``.
- :type: :class:`bool`
-
- Default: :const:`True`
-
.. note::
This functionality is also available via the ``sortable`` keyword
setup(
name='django-tables',
- version='0.5.0.dev',
+ version='0.5.0',
description='Table framework for Django',
author='Bradley Ayers',
# 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 = ("...", )
+ Assert(["a", "b", "c"]) == TestTable4([]).columns.names()
+ Assert(["a", "b", "c"]) == TestTable([], sequence=("...", )).columns.names()
+
+ class TestTable5(TestTable):
class Meta:
sequence = ("b", "...")
- Assert(["b", "a", "c"]) == TestTable4([]).columns.names()
+ Assert(["b", "a", "c"]) == TestTable5([]).columns.names()
Assert(["b", "a", "c"]) == TestTable([], sequence=("b", "...")).columns.names()
- class TestTable5(TestTable):
+ class TestTable6(TestTable):
class Meta:
sequence = ("...", "b")
- Assert(["a", "c", "b"]) == TestTable5([]).columns.names()
+ Assert(["a", "c", "b"]) == TestTable6([]).columns.names()
Assert(["a", "c", "b"]) == TestTable([], sequence=("...", "b")).columns.names()
- class TestTable6(tables.Table):
+ class TestTable7(TestTable):
class Meta:
- sequence = ("...")
- Assert(["a", "b", "c"]) == TestTable6([]).columns.names()
- Assert(["a", "b", "c"]) == TestTable([], sequence=("...")).columns.names()
+ sequence = ("b", "...", "a")
+ Assert(["b", "c", "a"]) == TestTable7([]).columns.names()
+ Assert(["b", "c", "a"]) == TestTable([], sequence=("b", "...", "a")).columns.names()
+
+ # Let's test inheritence
+ class TestTable8(TestTable):
+ d = tables.Column()
+ e = tables.Column()
+ f = tables.Column()
+
+ class Meta:
+ sequence = ("d", "...")
+
+ class TestTable9(TestTable):
+ d = tables.Column()
+ e = tables.Column()
+ f = tables.Column()
+
+ Assert(["d", "a", "b", "c", "e", "f"]) == TestTable8([]).columns.names()
+ Assert(["d", "a", "b", "c", "e", "f"]) == TestTable9([], sequence=("d", "...")).columns.names()
linkcolumn = Tests()