* Bumped version to 0.5.0
authorBradley Ayers <bradley.ayers@enigmainteractive.com>
Tue, 7 Jun 2011 02:53:53 +0000 (12:53 +1000)
committerBradley Ayers <bradley.ayers@enigmainteractive.com>
Tue, 7 Jun 2011 03:14:14 +0000 (13:14 +1000)
* Added the ability to explicitly specify the sequence of columns. This resolves #11

django_tables/tables.py
docs/conf.py
docs/index.rst
setup.py
tests/columns.py

index d9fe7aea7425fa62aaf8709498dde77551468edf..5f479dafa1e5c840263ae9294ea3aaa4e85fa44d 100644 (file)
@@ -7,6 +7,7 @@ from django.template.loader import get_template
 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
@@ -15,33 +16,54 @@ 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):
     """
@@ -123,7 +145,7 @@ class DeclarativeColumnsMetaclass(type):
         """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))
 
@@ -148,15 +170,8 @@ class DeclarativeColumnsMetaclass(type):
             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)
 
 
@@ -178,7 +193,7 @@ class TableOptions(object):
         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)
 
 
@@ -233,7 +248,7 @@ class Table(StrAndUnicode):
         # 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:
@@ -285,7 +300,8 @@ class Table(StrAndUnicode):
     @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
index 23ee16d628812d928009c5259ab9e4403465c07c..6cc411cd9283e1857771c1bcd695a0df7cea6391 100644 (file)
@@ -56,7 +56,7 @@ project = u'django-tables'
 # 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.
index afda7fbd6090b090bdc32c817e4a420ea57a373e..c40d661dedd3843f0406697878718bfd0d548545 100644 (file)
@@ -175,9 +175,15 @@ Any iterable can be used as table data, and there's builtin support for
 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
 
@@ -741,7 +747,12 @@ API Reference
     .. 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()
@@ -757,10 +768,6 @@ API Reference
             >>> 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
@@ -771,29 +778,61 @@ API Reference
         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
index ddfd3ac3819ed52fd9bcd33919721cf3a272f952..c4453b72158ae707a8c6d8cea81c725dd52d5e6a 100755 (executable)
--- a/setup.py
+++ b/setup.py
@@ -4,7 +4,7 @@ from setuptools import setup, find_packages
 
 setup(
     name='django-tables',
-    version='0.5.0.dev',
+    version='0.5.0',
     description='Table framework for Django',
 
     author='Bradley Ayers',
index f154586107a40697a3f90c7f40333594a375ecec..2af414266b33ca1f262a311461d32b146a4e02f0 100644 (file)
@@ -62,22 +62,45 @@ def sequence():
     # 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()