Rendered tables now include empty_text
authorBradley Ayers <bradley.ayers@gmail.com>
Sun, 10 Apr 2011 00:02:06 +0000 (10:02 +1000)
committerBradley Ayers <bradley.ayers@gmail.com>
Sun, 10 Apr 2011 00:02:06 +0000 (10:02 +1000)
.gitignore
django_tables/columns.py
django_tables/tables.py
django_tables/templates/django_tables/basic_table.html
django_tables/templates/django_tables/table.html
docs/index.rst
tests/__init__.py
tests/columns.py [new file with mode: 0644]
tests/core.py
tests/templates.py

index 6eabac012a9c3e2a01aa739b0a606d17e00dc21e..3aacb656b01796c65f6fa608facd21108479ac03 100644 (file)
@@ -1,5 +1,6 @@
 *.pyc
 /*.komodoproject
+/*.tmproj
 /*.egg-info/
 /*.egg
 /MANIFEST
index 9bb29e97639b38042633be0c08b211dfb7428a14..cf46a63c1cfe091ca68c1a3da395467090031699 100644 (file)
@@ -297,32 +297,30 @@ class TemplateColumn(Column):
 
 
 class BoundColumn(object):
-    """A *runtime* version of :class:`Column`. The difference between
-    :class:`BoundColumn` and :class:`Column`, is that :class:`BoundColumn`
-    objects are of the relationship between a :class:`Column` and a
-    :class:`Table`. This means that it knows the *name* given to the
-    :class:`Column`.
+    """A *runtime* version of :class:`.Column`. The difference between
+    ``BoundColumn`` and ``Column``, is that ``BoundColumn`` objects are of
+    the relationship between a ``Column`` and a :class:`.Table`. This
+    means that it knows the *name* given to the ``Column``.
 
-    For convenience, all :class:`Column` properties are available from this
+    For convenience, all :class:`.Column` properties are available from this
     class.
 
-
-    :type table: :class:`Table` object
+    :type table: :class:`.Table` object
     :param table: the table in which this column exists
 
-    :type column: :class:`Column` object
+    :type column: :class:`.Column` object
     :param column: the type of column
 
-    :type name: :class:`basestring` object
+    :type name: ``basestring`` object
     :param name: the variable name of the column used to when defining the
-        :class:`Table`. Example:
+        :class:`.Table`. Example:
 
         .. code-block:: python
 
             class SimpleTable(tables.Table):
                 age = tables.Column()
 
-        `age` is the name.
+        ``age`` is the name.
 
     """
     def __init__(self, table, column, name):
@@ -333,11 +331,6 @@ class BoundColumn(object):
     def __unicode__(self):
         return self.verbose_name
 
-    @property
-    def column(self):
-        """Returns the :class:`Column` object for this column."""
-        return self._column
-
     @property
     def accessor(self):
         """
@@ -347,6 +340,11 @@ class BoundColumn(object):
         """
         return self.column.accessor or A(self.name)
 
+    @property
+    def column(self):
+        """Returns the :class:`.Column` object for this column."""
+        return self._column
+
     @property
     def default(self):
         """Returns the default value for this column."""
@@ -355,8 +353,7 @@ class BoundColumn(object):
     @property
     def header(self):
         """
-        Return the value that should be used in the header cell for this
-        column.
+        The value that should be used in the header cell for this column.
 
         """
         return self.column.header or self.verbose_name
@@ -370,7 +367,7 @@ class BoundColumn(object):
     def order_by(self):
         """
         If this column is sorted, return the associated :class:`.OrderBy`
-        instance, otherwise :const:`None`.
+        instance, otherwise ``None``.
 
         """
         try:
@@ -380,17 +377,10 @@ class BoundColumn(object):
 
     @property
     def sortable(self):
-        """
-        Return a :class:`bool` depending on whether this column is
-        sortable.
-
-        """
+        """Return a ``bool`` depending on whether this column is sortable."""
         if self.column.sortable is not None:
             return self.column.sortable
-        elif self.table._meta.sortable is not None:
-            return self.table._meta.sortable
-        else:
-            return True  # the default value
+        return self.table.sortable
 
     @property
     def table(self):
@@ -425,11 +415,11 @@ class BoundColumns(object):
     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
-    columns easier than if they were stored in a :class:`list` or
-    :class:`dict`. :class:`Columns` has a similar API to a :class:`dict` (it
-    actually uses a :class:`SortedDict` interally).
+    A :class:`.BoundColumns` object is a container for holding
+    :class:`.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
+    actually uses a ``SortedDict`` interally).
 
     At the moment you'll only come across this class when you access a
     :attr:`.Table.columns` property.
@@ -480,16 +470,10 @@ class BoundColumns(object):
         for r in self._columns.items():
             yield r
 
-    def names(self):
-        """Return an iterator of column names."""
-        self._spawn_columns()
-        for r in self._columns.keys():
-            yield r
-
     def sortable(self):
         """
-        Same as :meth:`.BoundColumns.all` but only returns sortable :class:`BoundColumn`
-        objects.
+        Same as :meth:`.BoundColumns.all` but only returns sortable
+        :class:`.BoundColumn` objects.
 
         This is useful in templates, where iterating over the full
         set and checking ``{% if column.sortable %}`` can be problematic in
@@ -518,15 +502,18 @@ class BoundColumns(object):
         return self.visible()
 
     def __contains__(self, item):
-        """Check if a column is contained within a :class:`Columns` object.
+        """Check if a column is contained within a :class:`.Columns` object.
 
-        *item* can either be a :class:`BoundColumn` object, or the name of a
+        *item* can either be a :class:`.BoundColumn` object, or the name of a
         column.
 
         """
         self._spawn_columns()
         if isinstance(item, basestring):
-            return item in self.names()
+            for key in self._columns.keys():
+                if item == key:
+                    return True
+            return False
         else:
             return item in self.all()
 
index 91fcbdf0da3d48960f9e9d9ac74ade98c6c84820..bc92e9e6c484750444b44a98d0a03e0daa90d6a7 100644 (file)
@@ -83,6 +83,7 @@ class TableData(object):
     def __getitem__(self, index):
         """Forwards indexing accesses to underlying data"""
         return (self.list if hasattr(self, 'list') else self.queryset)[index]
+        
 
 
 class DeclarativeColumnsMetaclass(type):
@@ -138,7 +139,7 @@ class TableOptions(object):
 
         """
         super(TableOptions, self).__init__()
-        self.sortable = getattr(options, 'sortable', None)
+        self.sortable = getattr(options, 'sortable', True)
         order_by = getattr(options, 'order_by', ())
         if isinstance(order_by, basestring):
             order_by = (order_by, )
@@ -184,19 +185,16 @@ class Table(StrAndUnicode):
         self._rows = BoundRows(self)  # bound rows
         self._columns = BoundColumns(self)  # bound columns
         self._data = self.TableDataClass(data=data, table=self)
-
+        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
 
-        self.sortable = sortable
-        self.empty_text = empty_text
-
         # 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. See the
-        # ``Table`` class docstring for more information.
+        # copy is made available in a ``fields`` attribute.
         self.base_columns = copy.deepcopy(type(self).base_columns)
 
     def __unicode__(self):
@@ -217,15 +215,17 @@ class Table(StrAndUnicode):
         of column names.
 
         """
-        # accept both string and tuple instructions
+        # accept string
         order_by = value.split(',') if isinstance(value, basestring) else value
+        # accept None
         order_by = () if order_by is None else order_by
         new = []
-        # validate, raise exception on failure
+        # everything's been converted to a iterable, accept iterable!
         for o in order_by:
-            name = OrderBy(o).bare
+            ob = OrderBy(o)
+            name = ob.bare
             if name in self.columns and self.columns[name].sortable:
-                new.append(o)
+                new.append(ob)
         order_by = OrderByTuple(new)
         self._order_by = order_by
         self._data.order_by(order_by)
index a564e7bc396e29212cdfa7510eff0ffc81b5cf3c..a3ada0bf354133efa3e929e3f4475cd8149d287c 100644 (file)
                 <td>{{ value }}</td>
             {% endfor %}
         </tr>
+               {% empty %}
+               {% if table.empty_text %}
+               <tr><td colspan="{{ table.columns|length }}">{{ table.empty_text }}</td></tr>
+               {% endif %}
         {% endfor %}
     </tbody>
 </table>
index c6d12c0b1c9227e356ce04d50e4aa939cec45867..21699bbfb6c62cd1df50308012a5ef4c932cd18d 100644 (file)
                 <td>{{ cell }}</td>
             {% endfor %}
         </tr>
+               {% empty %}
+               {% if table.empty_text %}
+               <tr><td colspan="{{ table.columns|length }}">{{ table.empty_text }}</td></tr>
+               {% endif %}
         {% endfor %}
     </tbody>
 </table>
index ac3622b81172ea4e74331f85ce0dd2f42a280e69..23b198f0d08265bc6e588053b8090909f2e27479 100644 (file)
@@ -486,7 +486,12 @@ API Reference
 
     .. attribute:: sortable
 
-        Does the table support ordering?
+        The default value for determining if a :class:`.Column` is sortable.
+
+        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``.
 
         Default: :const:`True`
 
index 5b434e4ab63a262890c38e69ebb569d3b4660664..0774bec03bbe9301cceba3f6e656d180f1279e8f 100644 (file)
@@ -27,6 +27,7 @@ from .templates import templates
 from .models import models
 from .utils import utils
 from .rows import rows
+from .columns import columns
 
 
-everything = Tests([core, templates, models, utils, rows])
+everything = Tests([core, templates, models, utils, rows, columns])
diff --git a/tests/columns.py b/tests/columns.py
new file mode 100644 (file)
index 0000000..d429ec9
--- /dev/null
@@ -0,0 +1,30 @@
+"""Test the core table functionality."""
+from attest import Tests, Assert
+import django_tables as tables
+from django_tables import utils
+
+
+columns = Tests()
+
+
+@columns.test
+def sortable():
+    class SimpleTable(tables.Table):
+        name = tables.Column()
+    Assert(SimpleTable([]).columns['name'].sortable) is True
+
+    class SimpleTable(tables.Table):
+        name = tables.Column()
+        
+        class Meta:
+            sortable = False
+    Assert(SimpleTable([]).columns['name'].sortable) is False
+
+    class SimpleTable(tables.Table):
+        name = tables.Column()
+
+        class Meta:
+            sortable = True
+    Assert(SimpleTable([]).columns['name'].sortable) is True
+
+
index 66cbe4fb11240cd283149d7727ee7093ddb878b6..b329b077c0c5e431c601d3d4acb8da425b9ab21c 100644 (file)
@@ -117,6 +117,34 @@ def sorting(ctx):
     table = ctx.SortedTable(ctx.memory_data)
     Assert(1) == table.rows[0]['i']
 
+    # column's can't be sorted if they're not allowed to be
+    class TestTable(tables.Table):
+        a = tables.Column(sortable=False)
+        b = tables.Column()
+
+    table = TestTable([], order_by='a')
+    Assert(table.order_by) == ()
+
+    table = TestTable([], order_by='b')
+    Assert(table.order_by) == ('b', )
+
+    # sorting disabled by default
+    class TestTable(tables.Table):
+        a = tables.Column(sortable=True)
+        b = tables.Column()
+
+        class Meta:
+            sortable = False
+
+    table = TestTable([], order_by='a')
+    Assert(table.order_by) == ('a', )
+
+    table = TestTable([], order_by='b')
+    Assert(table.order_by) == ()
+
+    table = TestTable([], sortable=True, order_by='b')
+    Assert(table.order_by) == ('b', )
+
 
 @core.test
 def column_count(context):
@@ -173,3 +201,24 @@ def pagination():
     with Assert.raises(Http404) as error:
         books.paginate(Paginator, page=9999, per_page=10)
         books.paginate(Paginator, page='abc', per_page=10)
+
+
+@core.test
+def empty_text():
+    class TestTable(tables.Table):
+        a = tables.Column()
+
+    table = TestTable([])
+    Assert(table.empty_text) is None
+
+    class TestTable(tables.Table):
+        a = tables.Column()
+
+        class Meta:
+            empty_text = 'nothing here'
+
+    table = TestTable([])
+    Assert(table.empty_text) == 'nothing here'
+
+    table = TestTable([], empty_text='still nothing')
+    Assert(table.empty_text) == 'still nothing'
index f6f1700eb17c4ab40b2e59832fb54d031c6368f7..2a5f482d6ae9fb8bd05e4eb8d728d2edba318e58 100644 (file)
@@ -11,6 +11,7 @@ from django.template import Template, Context
 from django.http import HttpRequest
 import django_tables as tables
 from attest import Tests, Assert
+from xml.etree import ElementTree as ET
 
 templates = Tests()
 
@@ -41,8 +42,29 @@ def context():
 
 @templates.test
 def as_html(context):
-    countries = context.CountryTable(context.data)
-    countries.as_html()
+    table = context.CountryTable(context.data)
+    root = ET.fromstring(table.as_html())    
+    Assert(len(root.findall('.//thead/tr'))) == 1
+    Assert(len(root.findall('.//thead/tr/th'))) == 4
+    Assert(len(root.findall('.//tbody/tr'))) == 4
+    Assert(len(root.findall('.//tbody/tr/td'))) == 16
+    
+    # no data with no empty_text
+    table = context.CountryTable([])
+    root = ET.fromstring(table.as_html())
+    Assert(1) == len(root.findall('.//thead/tr'))
+    Assert(4) == len(root.findall('.//thead/tr/th'))
+    Assert(0) == len(root.findall('.//tbody/tr'))
+    
+    # no data WITH empty_text
+    table = context.CountryTable([], empty_text='this table is empty')
+    root = ET.fromstring(table.as_html())
+    Assert(1) == len(root.findall('.//thead/tr'))
+    Assert(4) == len(root.findall('.//thead/tr/th'))
+    Assert(1) == len(root.findall('.//tbody/tr'))
+    Assert(1) == len(root.findall('.//tbody/tr/td'))
+    Assert(int(root.find('.//tbody/tr/td').attrib['colspan'])) == len(root.findall('.//thead/tr/th'))
+    Assert(root.find('.//tbody/tr/td').text) == 'this table is empty'
 
 
 @templates.test
@@ -69,7 +91,36 @@ def custom_rendering(context):
 @templates.test
 def templatetag(context):
     # ensure it works with a multi-order-by
-    countries = context.CountryTable(context.data,
-                                     order_by=('name', 'population'))
+    table = context.CountryTable(context.data, order_by=('name', 'population'))
+    t = Template('{% load django_tables %}{% render_table table %}')
+    html = t.render(Context({'request': HttpRequest(), 'table': table}))
+    
+    root = ET.fromstring(html)    
+    Assert(len(root.findall('.//thead/tr'))) == 1
+    Assert(len(root.findall('.//thead/tr/th'))) == 4
+    Assert(len(root.findall('.//tbody/tr'))) == 4
+    Assert(len(root.findall('.//tbody/tr/td'))) == 16
+    
+    # no data with no empty_text
+    table = context.CountryTable([])
+    t = Template('{% load django_tables %}{% render_table table %}')
+    html = t.render(Context({'request': HttpRequest(), 'table': table}))
+    root = ET.fromstring(html)    
+    Assert(len(root.findall('.//thead/tr'))) == 1
+    Assert(len(root.findall('.//thead/tr/th'))) == 4
+    Assert(len(root.findall('.//tbody/tr'))) == 0
+    
+    # no data WITH empty_text
+    table = context.CountryTable([], empty_text='this table is empty')
     t = Template('{% load django_tables %}{% render_table table %}')
-    t.render(Context({'request': HttpRequest(), 'table': countries}))
+    html = t.render(Context({'request': HttpRequest(), 'table': table}))
+    root = ET.fromstring(html)    
+    Assert(len(root.findall('.//thead/tr'))) == 1
+    Assert(len(root.findall('.//thead/tr/th'))) == 4
+    Assert(len(root.findall('.//tbody/tr'))) == 1
+    Assert(len(root.findall('.//tbody/tr/td'))) == 1
+    Assert(int(root.find('.//tbody/tr/td').attrib['colspan'])) == len(root.findall('.//thead/tr/th'))
+    Assert(root.find('.//tbody/tr/td').text) == 'this table is empty'
+    
+    
+