currently broken
authorBradley Ayers <bradley.ayers@gmail.com>
Mon, 6 Jun 2011 23:12:52 +0000 (09:12 +1000)
committerBradley Ayers <bradley.ayers@gmail.com>
Mon, 6 Jun 2011 23:12:52 +0000 (09:12 +1000)
django_tables/columns.py
django_tables/tables.py
docs/index.rst
tests/columns.py

index 504312051b400dcd924f285d4ce7b8528efd5ee4..e3626d2a6b74f8906c970340c0c9601f0e259875 100644 (file)
@@ -480,23 +480,37 @@ class BoundColumns(object):
                 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.
@@ -508,7 +522,10 @@ class BoundColumns(object):
         """
         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.
@@ -517,11 +534,14 @@ class BoundColumns(object):
         """
         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):
         """
@@ -530,21 +550,19 @@ class BoundColumns(object):
         *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):
         """
index fc9fc5d987d90ccb8acdc6e629897e8f9fc3a108..d9fe7aea7425fa62aaf8709498dde77551468edf 100644 (file)
@@ -15,6 +15,34 @@ from .columns import BoundColumns, Column
 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
@@ -94,7 +122,7 @@ class DeclarativeColumnsMetaclass(type):
     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))
@@ -119,6 +147,16 @@ class DeclarativeColumnsMetaclass(type):
         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)
 
 
@@ -140,6 +178,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.sortable = getattr(options, "sortable", True)
 
 
@@ -184,17 +223,13 @@ class Table(StrAndUnicode):
     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.
@@ -203,6 +238,12 @@ class Table(StrAndUnicode):
         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()
@@ -236,6 +277,17 @@ class Table(StrAndUnicode):
         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
index be8b6071bc3c24f02295d41690a40715cd6384e1..afda7fbd6090b090bdc32c817e4a420ea57a373e 100644 (file)
@@ -705,6 +705,9 @@ API Reference
         :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.::
 
@@ -714,10 +717,6 @@ API Reference
                 class Meta:
                     attrs = {"class": "paleblue"}
 
-        :type: ``dict``
-
-        Default: ``{}``
-
         .. note::
 
             This functionality is also available via the ``attrs`` keyword
@@ -727,6 +726,13 @@ API Reference
 
         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
@@ -776,7 +782,8 @@ API Reference
 
     .. 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
index 939a588d11a85388fd9b4771e1b256d9dae901a1..f154586107a40697a3f90c7f40333594a375ecec 100644 (file)
@@ -4,6 +4,7 @@ from attest import Tests, Assert
 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
@@ -32,6 +33,53 @@ def sortable():
     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())