Added the ability to exclude columns from an entire Table class or from a specific...
authorBradley Ayers <bradley.ayers@gmail.com>
Thu, 2 Jun 2011 12:31:13 +0000 (22:31 +1000)
committerBradley Ayers <bradley.ayers@gmail.com>
Thu, 2 Jun 2011 12:31:13 +0000 (22:31 +1000)
django_tables/columns.py
django_tables/tables.py
docs/index.rst
tests/core.py

index 23734dc57a42454c43190c72b46a558e948c378b..cc94116d28a478d1f99aaba84f0e481572d532dd 100644 (file)
@@ -434,17 +434,17 @@ class BoundColumn(object):
 
 class BoundColumns(object):
     """
-    Container for spawning BoundColumns.
+    Container for spawning :class:`.BoundColumn` objects.
 
     This is bound to a table and provides its :attr:`.Table.columns` property.
     It provides access to those columns in different ways (iterator,
     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
+    A ``BoundColumns`` object is a container for holding
+    ``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
+    ``dict``. ``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
index f3ba21e9ab59d2bb0ea6e9cccf913d6ff95082e1..7d516c5b3f6c9b91c31d99d5be2cb177e3d60f2a 100644 (file)
@@ -19,7 +19,7 @@ class TableData(object):
     Exposes a consistent API for :term:`table data`. It currently supports a
     :class:`QuerySet`, or a :class:`list` of :class:`dict` objects.
 
-    This class is used by :class:.Table` to wrap any
+    This class is used by :class:`.Table` to wrap any
     input table data.
     """
 
@@ -92,7 +92,7 @@ class DeclarativeColumnsMetaclass(type):
     as well.
     """
 
-    def __new__(cls, name, bases, attrs, parent_cols_from=None):
+    def __new__(cls, name, bases, attrs):
         """Ughhh document this :)"""
         # extract declared columns
         columns = [(name, attrs.pop(name)) for name, column in attrs.items()
@@ -104,22 +104,22 @@ class DeclarativeColumnsMetaclass(type):
         # well. Note that we loop over the bases in *reverse* - this is
         # necessary to preserve the correct order of columns.
         for base in bases[::-1]:
-            cols_attr = (parent_cols_from if (parent_cols_from and
-                                             hasattr(base, parent_cols_from))
-                                          else 'base_columns')
-            if hasattr(base, cols_attr):
-                columns = getattr(base, cols_attr).items() + columns
+            if hasattr(base, "base_columns"):
+                columns = base.base_columns.items() + columns
         # Note that we are reusing an existing ``base_columns`` attribute.
         # This is because in certain inheritance cases (mixing normal and
         # ModelTables) this metaclass might be executed twice, and we need
         # to avoid overriding previous data (because we pop() from attrs,
         # the second time around columns might not be registered again).
         # An example would be:
-        #    class MyNewTable(MyOldNonModelTable, tables.ModelTable): pass
-        if not 'base_columns' in attrs:
-            attrs['base_columns'] = SortedDict()
-        attrs['base_columns'].update(SortedDict(columns))
-        attrs['_meta'] = TableOptions(attrs.get('Meta', None))
+        #    class MyNewTable(MyOldNonTable, tables.Table): pass
+        if not "base_columns" in attrs:
+            attrs["base_columns"] = SortedDict()
+        attrs["base_columns"].update(SortedDict(columns))
+        attrs["_meta"] = opts = TableOptions(attrs.get("Meta", None))
+        for ex in opts.exclude:
+            if ex in attrs["base_columns"]:
+                attrs["base_columns"].pop(ex)
         return type.__new__(cls, name, bases, attrs)
 
 
@@ -127,23 +127,21 @@ class TableOptions(object):
     """
     Extracts and exposes options for a :class:`.Table` from a ``class Meta``
     when the table is defined.
+
+    :param options: options for a table
+    :type options: :class:`Meta` on a :class:`.Table`
     """
 
     def __init__(self, options=None):
-        """
-
-        :param options: options for a table
-        :type options: :class:`Meta` on a :class:`.Table`
-
-        """
         super(TableOptions, self).__init__()
-        self.sortable = getattr(options, 'sortable', True)
-        order_by = getattr(options, 'order_by', ())
+        self.attrs = AttributeDict(getattr(options, "attrs", {}))
+        self.empty_text = getattr(options, "empty_text", None)
+        self.exclude = getattr(options, "exclude", ())
+        order_by = getattr(options, "order_by", ())
         if isinstance(order_by, basestring):
             order_by = (order_by, )
         self.order_by = OrderByTuple(order_by)
-        self.attrs = AttributeDict(getattr(options, 'attrs', {}))
-        self.empty_text = getattr(options, 'empty_text', None)
+        self.sortable = getattr(options, "sortable", True)
 
 
 class Table(StrAndUnicode):
@@ -186,9 +184,10 @@ class Table(StrAndUnicode):
     __metaclass__ = DeclarativeColumnsMetaclass
     TableDataClass = TableData
 
-    def __init__(self, data, order_by=None, sortable=None, empty_text=None):
-        self._rows = BoundRows(self)  # bound rows
-        self._columns = BoundColumns(self)  # bound columns
+    def __init__(self, data, order_by=None, sortable=None, empty_text=None,
+                 exclude=None):
+        self._rows = BoundRows(self)
+        self._columns = BoundColumns(self)
         self._data = self.TableDataClass(data=data, table=self)
         self.empty_text = empty_text
         self.sortable = sortable
@@ -196,11 +195,14 @@ class Table(StrAndUnicode):
             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.
         self.base_columns = copy.deepcopy(type(self).base_columns)
+        self.exclude = exclude or ()
+        for ex in self.exclude:
+            if ex in self.base_columns:
+                self.base_columns.pop(ex)
 
     def __unicode__(self):
         return self.as_html()
index 3abe9a5402e8c1d397911a635632c4761991f923..5915148765e57e9622d90325cb556ace89fe4968 100644 (file)
@@ -636,6 +636,9 @@ API Reference
 
 .. class:: Table.Meta
 
+    Provides a way to define *global* settings for table, as opposed to
+    defining them for each instance.
+
     .. attribute:: attrs
 
         Allows custom HTML attributes to be specified which will be added to
@@ -643,9 +646,54 @@ API Reference
         :meth:`~django_tables.tables.Table.as_html` or the
         :ref:`template-tags.render_table` template tag.
 
+        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 SimpleTable(tables.Table):
+                name = tables.Column()
+
+                class Meta:
+                    attrs = {"class": "paleblue"}
+
+        :type: ``dict``
+
         Default: ``{}``
 
-        :type: :class:`dict`
+    .. attribute:: empty_text
+
+        Defines the text to display when the table has no rows.
+
+    .. attribute:: exclude
+
+        Defines which columns should be excluded from the table. This is useful
+        in subclasses to exclude columns in a parent. e.g.
+
+            >>> class Person(tables.Table):
+            ...     first_name = tables.Column()
+            ...     last_name = tables.Column()
+            ...
+            >>> Person.base_columns
+            {'first_name': <django_tables.columns.Column object at 0x10046df10>,
+            'last_name': <django_tables.columns.Column object at 0x10046d8d0>}
+            >>> class ForgetfulPerson(Person):
+            ...     class Meta:
+            ...         exclude = ("last_name", )
+            ...
+            >>> ForgetfulPerson.base_columns
+            {'first_name': <django_tables.columns.Column object at 0x10046df10>}
+
+        :type: tuple of ``string`` objects
+
+        Default: ``()``
+
+    .. attribute:: order_by
+
+        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: ``()``
 
     .. attribute:: sortable
 
@@ -656,17 +704,9 @@ API Reference
         an easy mechanism to disable sorting on an entire table, without adding
         ``sortable=False`` to each ``Column`` in a ``Table``.
 
-        Default: :const:`True`
-
         :type: :class:`bool`
 
-    .. attribute:: order_by
-
-        The default ordering. e.g. ``('name', '-age')``
-
-        Default: ``()``
-
-        :type: :class:`tuple`
+        Default: :const:`True`
 
 
 :class:`TableData` Objects:
@@ -676,13 +716,6 @@ API Reference
     :members: __init__, order_by, __getitem__, __len__
 
 
-:class:`TableOptions` Objects:
-------------------------------
-
-.. autoclass:: django_tables.tables.TableOptions
-    :members:
-
-
 :class:`Column` Objects:
 ------------------------
 
index b329b077c0c5e431c601d3d4acb8da425b9ab21c..50af9ffbe928c7e108e60c22853dd5043b888e04 100644 (file)
@@ -6,34 +6,30 @@ from django.core.paginator import Paginator
 import django_tables as tables
 from django_tables import utils
 
+
 core = Tests()
 
 
-@core.context
-def context():
-    class Context(object):
-        memory_data = [
-            {'i': 2, 'alpha': 'b', 'beta': 'b'},
-            {'i': 1, 'alpha': 'a', 'beta': 'c'},
-            {'i': 3, 'alpha': 'c', 'beta': 'a'},
-        ]
+class UnsortedTable(tables.Table):
+    i = tables.Column()
+    alpha = tables.Column()
+    beta = tables.Column()
 
-        class UnsortedTable(tables.Table):
-            i = tables.Column()
-            alpha = tables.Column()
-            beta = tables.Column()
 
-        class SortedTable(UnsortedTable):
-            class Meta:
-                order_by = 'alpha'
+class SortedTable(UnsortedTable):
+    class Meta:
+        order_by = 'alpha'
 
-        table = UnsortedTable(memory_data)
 
-    yield Context
+MEMORY_DATA = [
+    {'i': 2, 'alpha': 'b', 'beta': 'b'},
+    {'i': 1, 'alpha': 'a', 'beta': 'c'},
+    {'i': 3, 'alpha': 'c', 'beta': 'a'},
+]
 
 
 @core.test
-def declarations(context):
+def declarations():
     """Test defining tables by declaration."""
     class GeoAreaTable(tables.Table):
         name = tables.Column()
@@ -61,60 +57,60 @@ def declarations(context):
 
 
 @core.test
-def datasource_untouched(context):
+def datasource_untouched():
     """Ensure that data that is provided to the table (the datasource) is not
     modified by table operations.
     """
-    original_data = copy.deepcopy(context.memory_data)
+    original_data = copy.deepcopy(MEMORY_DATA)
 
-    table = context.UnsortedTable(context.memory_data)
+    table = UnsortedTable(MEMORY_DATA)
     table.order_by = 'i'
     list(table.rows)
-    assert context.memory_data == Assert(original_data)
+    assert MEMORY_DATA == Assert(original_data)
 
-    table = context.UnsortedTable(context.memory_data)
+    table = UnsortedTable(MEMORY_DATA)
     table.order_by = 'beta'
     list(table.rows)
-    assert context.memory_data == Assert(original_data)
+    assert MEMORY_DATA == Assert(original_data)
 
 
 @core.test
-def sorting(ctx):
+def sorting():
     # fallback to Table.Meta
-    Assert(('alpha', )) == ctx.SortedTable([], order_by=None).order_by == ctx.SortedTable([]).order_by
+    Assert(('alpha', )) == SortedTable([], order_by=None).order_by == SortedTable([]).order_by
 
     # values of order_by are wrapped in tuples before being returned
-    Assert(ctx.SortedTable([], order_by='alpha').order_by)   == ('alpha', )
-    Assert(ctx.SortedTable([], order_by=('beta',)).order_by) == ('beta', )
+    Assert(SortedTable([], order_by='alpha').order_by)   == ('alpha', )
+    Assert(SortedTable([], order_by=('beta',)).order_by) == ('beta', )
 
     # "no sorting"
-    table = ctx.SortedTable([])
+    table = SortedTable([])
     table.order_by = []
-    Assert(()) == table.order_by == ctx.SortedTable([], order_by=[]).order_by
+    Assert(()) == table.order_by == SortedTable([], order_by=[]).order_by
 
-    table = ctx.SortedTable([])
+    table = SortedTable([])
     table.order_by = ()
-    Assert(()) == table.order_by == ctx.SortedTable([], order_by=()).order_by
+    Assert(()) == table.order_by == SortedTable([], order_by=()).order_by
 
-    table = ctx.SortedTable([])
+    table = SortedTable([])
     table.order_by = ''
-    Assert(()) == table.order_by == ctx.SortedTable([], order_by='').order_by
+    Assert(()) == table.order_by == SortedTable([], order_by='').order_by
 
     # apply a sorting
-    table = ctx.UnsortedTable([])
+    table = UnsortedTable([])
     table.order_by = 'alpha'
-    Assert(('alpha', )) == ctx.UnsortedTable([], order_by='alpha').order_by == table.order_by
+    Assert(('alpha', )) == UnsortedTable([], order_by='alpha').order_by == table.order_by
 
-    table = ctx.SortedTable([])
+    table = SortedTable([])
     table.order_by = 'alpha'
-    Assert(('alpha', )) == ctx.SortedTable([], order_by='alpha').order_by  == table.order_by
+    Assert(('alpha', )) == SortedTable([], order_by='alpha').order_by  == table.order_by
 
     # let's check the data
-    table = ctx.SortedTable(ctx.memory_data, order_by='beta')
+    table = SortedTable(MEMORY_DATA, order_by='beta')
     Assert(3) == table.rows[0]['i']
 
     # allow fallback to Table.Meta.order_by
-    table = ctx.SortedTable(ctx.memory_data)
+    table = SortedTable(MEMORY_DATA)
     Assert(1) == table.rows[0]['i']
 
     # column's can't be sorted if they're not allowed to be
@@ -147,7 +143,7 @@ def sorting(ctx):
 
 
 @core.test
-def column_count(context):
+def column_count():
     class SimpleTable(tables.Table):
         visible = tables.Column(visible=True)
         hidden = tables.Column(visible=False)
@@ -157,16 +153,50 @@ def column_count(context):
 
 
 @core.test
-def column_accessor(context):
-    class SimpleTable(context.UnsortedTable):
+def column_accessor():
+    class SimpleTable(UnsortedTable):
         col1 = tables.Column(accessor='alpha.upper.isupper')
         col2 = tables.Column(accessor='alpha.upper')
-    table = SimpleTable(context.memory_data)
+    table = SimpleTable(MEMORY_DATA)
     row = table.rows[0]
     Assert(row['col1']) is True
     Assert(row['col2']) == 'B'
 
 
+@core.test
+def exclude_columns():
+    """
+    Defining ``Table.Meta.exclude`` or providing an ``exclude`` argument when
+    instantiating a table should have the same effect -- exclude those columns
+    from the table. It should have the same effect as not defining the
+    columns originally.
+    """
+    # Table(..., exclude=...)
+    table = UnsortedTable([], exclude=("i"))
+    Assert([c.name for c in table.columns]) == ["alpha", "beta"]
+
+    # Table.Meta: exclude=...
+    class PartialTable(UnsortedTable):
+        class Meta:
+            exclude = ("alpha", )
+    table = PartialTable([])
+    Assert([c.name for c in table.columns]) == ["i", "beta"]
+
+    # Inheritence -- exclude in parent, add in child
+    class AddonTable(PartialTable):
+        added = tables.Column()
+    table = AddonTable([])
+    Assert([c.name for c in table.columns]) == ["i", "beta", "added"]
+
+    # Inheritence -- exclude in child
+    class ExcludeTable(UnsortedTable):
+        added = tables.Column()
+        class Meta:
+            exclude = ("alpha", )
+    table = ExcludeTable([])
+    Assert([c.name for c in table.columns]) == ["i", "beta", "added"]
+
+
 @core.test
 def pagination():
     class BookTable(tables.Table):
@@ -175,7 +205,7 @@ def pagination():
     # create some sample data
     data = []
     for i in range(100):
-        data.append({'name': 'Book No. %d' % i})
+        data.append({"name": "Book No. %d" % i})
     books = BookTable(data)
 
     # external paginator
@@ -187,7 +217,7 @@ def pagination():
 
     # integrated paginator
     books.paginate(page=1)
-    Assert(hasattr(books, 'page')) is True
+    Assert(hasattr(books, "page")) is True
 
     books.paginate(page=1, per_page=10)
     Assert(len(list(books.page.object_list))) == 10