No longer allow the Column's 'data' argument to be callable. This was tedious to...
authorMichael Elsdoerfer <michael@elsdoerfer.com>
Tue, 1 Jun 2010 15:28:35 +0000 (17:28 +0200)
committerMichael Elsdoerfer <michael@elsdoerfer.com>
Tue, 1 Jun 2010 16:18:16 +0000 (18:18 +0200)
Also, these render methods will receive no the BoundRow as an argument, but rather the actual underlining data row. This makes much more sense, and prevents users from having to fall back to the undocument ``BoundRow.data`` attribute to avoid infinite recursion.

django_tables/base.py
django_tables/columns.py
django_tables/models.py
tests/test_memory.py
tests/test_models.py

index 686328ad40481926001ece285fa866a89d885923..bd8ac9fb0071c3c85a75e1a0edf45b8e11c81aef 100644 (file)
@@ -76,6 +76,7 @@ class DeclarativeColumnsMetaclass(type):
         attrs['_meta'] = TableOptions(attrs.get('Meta', None))\r
         return type.__new__(cls, name, bases, attrs)\r
 \r
+\r
 def rmprefix(s):\r
     """Normalize a column name by removing a potential sort prefix"""\r
     return (s[:1]=='-' and [s[1:]] or [s])[0]\r
@@ -277,6 +278,13 @@ class BoundColumn(StrAndUnicode):
         # expose some attributes of the column more directly\r
         self.visible = column.visible\r
 \r
+    @property\r
+    def accessor(self):\r
+        """The key to use when accessing this column's values in the\r
+        source data.\r
+        """\r
+        return self.column.data if self.column.data else self.declared_name\r
+\r
     def _get_sortable(self):\r
         if self.column.sortable is not None:\r
             return self.column.sortable\r
@@ -343,9 +351,19 @@ class BoundRow(object):
         """Returns this row's value for a column. All other access methods,\r
         e.g. __iter__, lead ultimately to this."""\r
 \r
-        # We are supposed to return ``name``, but the column might be\r
-        # named differently in the source data.\r
-        result =  self.data[self.table._cols_to_fields([name])[0]]\r
+        column = self.table.columns[name]\r
+\r
+        render_func = getattr(self.table, 'render_%s' % name, False)\r
+        if render_func:\r
+            return render_func(self.data)\r
+        else:\r
+            return self._default_render(column)\r
+\r
+    def _default_render(self, column):\r
+        """Returns a cell's content. This is used unless the user\r
+        provides a custom ``render_FOO`` method.\r
+        """\r
+        result = self.data[column.accessor]\r
 \r
         # if the field we are pointing to is a callable, remove it\r
         if callable(result):\r
@@ -523,11 +541,7 @@ class BaseTable(object):
                 prefix = ''\r
             # find the field name\r
             column = self.columns[name]\r
-            if column.column.data and not callable(column.column.data):\r
-                name_in_source = column.column.data\r
-            else:\r
-                name_in_source = column.declared_name\r
-            result.append(prefix + name_in_source)\r
+            result.append(prefix + column.accessor)\r
         return result\r
 \r
     def _validate_column_name(self, name, purpose):\r
index 5c329cb502acef5fd4806298550722818f04af99..571502925629b89cf8534ee158862c3f8fa3e6c1 100644 (file)
@@ -53,6 +53,11 @@ class Column(object):
         self.name = name\r
         self.default = default\r
         self.data = data\r
+        if callable(self.data):\r
+            raise DeprecationWarning(('The Column "data" argument may no '+\r
+                                      'longer be a callable. Add  a '+\r
+                                      '``render_%s`` method to your '+\r
+                                      'table instead.') % (name or 'FOO'))\r
         self.visible = visible\r
         self.inaccessible = inaccessible\r
         self.sortable = sortable\r
@@ -72,6 +77,7 @@ class Column(object):
 \r
     direction = property(lambda s: s._direction, _set_direction)\r
 \r
+\r
 class TextColumn(Column):\r
     pass\r
 \r
index eeb4ba7780c0752c5424bbe16c833328dcfb7640..7c7e87f09d37d612e56178f6d9f826415ba9a817 100644 (file)
@@ -47,33 +47,13 @@ class BoundModelRow(BoundRow):
     with the instance converted to a dict instead. However, this way allows\r
     us to support non-field attributes and methods on the model as well.\r
     """\r
-    def __getitem__(self, name):\r
-        """Overridden. Return this row's data for a certain column, with\r
-        custom handling for model tables.\r
-        """\r
-\r
-        # find the column for the requested field, for reference\r
-        boundcol = self.table._columns[name]\r
-\r
-        # If the column has a name override (we know then that is was also\r
-        # used for access, e.g. if the condition is true, then\r
-        # ``boundcol.column.name == name``), we need to make sure we use the\r
-        # declaration name to access the model field.\r
-        if boundcol.column.data:\r
-            if callable(boundcol.column.data):\r
-                result = boundcol.column.data(self)\r
-                if not result:\r
-                    if boundcol.column.default is not None:\r
-                        return boundcol.get_default(self)\r
-                return result\r
-            else:\r
-                name = boundcol.column.data\r
-        else:\r
-            name = boundcol.declared_name\r
-\r
 \r
+    def _default_render(self, boundcol):\r
+        """In the case of a model table, the accessor may use ``__`` to\r
+        span instances. We need to resolve this.\r
+        """\r
         # try to resolve relationships spanning attributes\r
-        bits = name.split('__')\r
+        bits = boundcol.accessor.split('__')\r
         current = self.data\r
         for bit in bits:\r
             # note the difference between the attribute being None and not\r
index 5c9cfddc831bb256cda13a9e2a714e23f9eb9e19..cfb2bfe05c77816caf6c84383c0d453340d715cc 100644 (file)
@@ -69,6 +69,34 @@ def test_basic():
     finally:\r
         tables.options.IGNORE_INVALID_OPTIONS = True\r
 \r
+\r
+class TestRender:\r
+    """Test use of the render_* methods.\r
+    """\r
+\r
+    def test(self):\r
+        class TestTable(tables.MemoryTable):\r
+            private_name = tables.Column(name='public_name')\r
+            def render_public_name(self, data):\r
+                # We are given the actual data dict and have direct access\r
+                # to additional values for which no field is defined.\r
+                return "%s:%s" % (data['private_name'], data['additional'])\r
+\r
+        table = TestTable([{'private_name': 'FOO', 'additional': 'BAR'}])\r
+        assert table.rows[0]['public_name'] == 'FOO:BAR'\r
+\r
+    def test_not_sorted(self):\r
+        """The render methods are not considered when sorting.\r
+        """\r
+        class TestTable(tables.MemoryTable):\r
+            foo = tables.Column()\r
+            def render_foo(self, data):\r
+                return -data['foo']  # try to cause a reverse sort\r
+        table = TestTable([{'foo': 1}, {'foo': 2}], order_by='asc')\r
+        # Result is properly sorted, and the render function has never been called\r
+        assert [r['foo'] for r in table.rows] == [-1, -2]\r
+\r
+\r
 def test_caches():\r
     """Ensure the various caches are effective.\r
     """\r
@@ -117,6 +145,7 @@ def test_meta_sortable():
         global_table._meta.sortable = default_sortable\r
         assert [c.sortable for c in global_table.columns] == list(results)\r
 \r
+\r
 def test_sort():\r
     class BookTable(tables.MemoryTable):\r
         id = tables.Column(direction='desc')\r
@@ -204,8 +233,9 @@ def test_sort():
     assert 'name' in books.order_by\r
     assert not 'language' in books.order_by\r
 \r
+\r
 def test_callable():\r
-    """Data fields, ``default`` and ``data`` options can be callables.\r
+    """Data fields and the ``default`` option can be callables.\r
     """\r
 \r
     class MathTable(tables.MemoryTable):\r
@@ -213,7 +243,6 @@ def test_callable():
         rhs = tables.Column()\r
         op = tables.Column(default='+')\r
         sum = tables.Column(default=lambda d: calc(d['op'], d['lhs'], d['rhs']))\r
-        sqrt = tables.Column(data=lambda d: int(sqrt(d['sum'])))\r
 \r
     math = MathTable([\r
         {'lhs': 1, 'rhs': lambda x: x['lhs']*3},              # 1+3\r
@@ -236,10 +265,6 @@ def test_callable():
     math.order_by = ('sum',)\r
     assert [row['sum'] for row in math] == [1,3,4]\r
 \r
-    # data function is called while sorting\r
-    math.order_by = ('sqrt',)\r
-    assert [row['sqrt'] for row in math] == [1,1,2]\r
-\r
 \r
 # TODO: all the column stuff might warrant it's own test file\r
 def test_columns():\r
index 1c53b587480d9ff431b5e72b13161e50a1cd7642..cfb5a401a181e5a7c6a5ee6a634b222eb15ceb71 100644 (file)
@@ -8,6 +8,7 @@ from django.conf import settings
 from django.core.paginator import *\r
 import django_tables as tables\r
 \r
+\r
 def setup_module(module):\r
     settings.configure(**{\r
         'DATABASE_ENGINE': 'sqlite3',\r
@@ -50,6 +51,7 @@ def setup_module(module):
     Country(name="France", tld="fr", population=64, system="republic").save()\r
     Country(name="Netherlands", tld="nl", population=16, system="monarchy", capital=amsterdam).save()\r
 \r
+\r
 def test_declaration():\r
     """Test declaration, declared columns and default model field columns.\r
     """\r
@@ -90,6 +92,7 @@ def test_declaration():
     assert not 'population' in CityTable.base_columns  # not in Meta:columns\r
     assert 'capital' in CityTable.base_columns  # in exclude, but only works on model fields (is that the right behaviour?)\r
 \r
+\r
 def test_basic():\r
     """Some tests here are copied from ``test_basic.py`` but need to be\r
     rerun with a ModelTable, as the implementation is different."""\r
@@ -268,30 +271,6 @@ def test_relationships():
     countries.order_by = 'invalid'\r
     assert countries.order_by == ()\r
 \r
-def test_column_data():\r
-    """Further test the ``data`` column property in a ModelTable scenario.\r
-    Other tests already touched on this, for example ``test_relationships``.\r
-    """\r
-\r
-    class CountryTable(tables.ModelTable):\r
-        name = tables.Column(data=lambda d: "hidden")\r
-        tld = tables.Column(data='example_domain', name="domain")\r
-        default_and_data = tables.Column(data=lambda d: None, default=4)\r
-        class Meta:\r
-            model = Country\r
-    countries = CountryTable(Country)\r
-\r
-    # callable data works, even with a default set\r
-    assert [row['default_and_data'] for row in countries] == [4,4,4,4]\r
-\r
-    # neato trick: a callable data= column is sortable, if otherwise refers\r
-    # to correct model column; can be used to rewrite what is displayed\r
-    countries.order_by = 'name'\r
-    assert countries.order_by == ('name',)\r
-    # [bug 282964] this trick also works if the callable is an attribute\r
-    # and we refer to it per string, rather than giving a function object\r
-    countries.order_by = 'domain'\r
-    assert countries.order_by == ('domain',)\r
 \r
 def test_pagination():\r
     """Pretty much the same as static table pagination, but make sure we\r