added support for column default sort directions
authorMichael Elsdoerfer <michael@elsdoerfer.info>
Sun, 29 Jun 2008 22:42:41 +0000 (00:42 +0200)
committerMichael Elsdoerfer <michael@elsdoerfer.info>
Sun, 29 Jun 2008 22:42:41 +0000 (00:42 +0200)
README
django_tables/columns.py
django_tables/models.py
django_tables/tables.py
tests/test_basic.py
tests/test_models.py

diff --git a/README b/README
index 71ae0965df509240e040a991c34329b190a3d1c3..ecd8d07c0fb89741c3ea69f1569bdd9a8080d0a9 100644 (file)
--- a/README
+++ b/README
@@ -307,6 +307,15 @@ If you don't want a column to be sortable by the user:
 \r
     pubdate = tables.Column(sortable=False)\r
 \r
+Sorting is also affected by ``direction``, which can be used to change the\r
+*default* sort direction to descending. Note that this option only indirectly\r
+translates to the actual direction. Normal und reverse order, the terms\r
+django-tables exposes, now simply mean different things.\r
+\r
+    pubdate = tables.Column(direction='desc')\r
+\r
+Both variants are possible.\r
+\r
 If you don't want to expose a column (but still require it to exist, for\r
 example because it should be sortable nonetheless):\r
 \r
index b715f1462d9c08c9869d960abc03538e9adeb273..5c329cb502acef5fd4806298550722818f04af99 100644 (file)
@@ -34,14 +34,21 @@ class Column(object):
     to the user, set ``inaccessible`` to True.\r
 \r
     Setting ``sortable`` to False will result in this column being unusable\r
-    in ordering.\r
+    in ordering. You can further change the *default* sort direction to\r
+    descending using ``direction``. Note that this option changes the actual\r
+    direction only indirectly. Normal und reverse order, the terms\r
+    django-tables exposes, now simply mean different things.\r
     """\r
 \r
+    ASC = 1\r
+    DESC = 2\r
+\r
     # Tracks each time a Column instance is created. Used to retain order.\r
     creation_counter = 0\r
 \r
     def __init__(self, verbose_name=None, name=None, default=None, data=None,\r
-                 visible=True, inaccessible=False, sortable=None):\r
+                 visible=True, inaccessible=False, sortable=None,\r
+                 direction=ASC):\r
         self.verbose_name = verbose_name\r
         self.name = name\r
         self.default = default\r
@@ -49,10 +56,22 @@ class Column(object):
         self.visible = visible\r
         self.inaccessible = inaccessible\r
         self.sortable = sortable\r
+        self.direction = direction\r
 \r
         self.creation_counter = Column.creation_counter\r
         Column.creation_counter += 1\r
 \r
+    def _set_direction(self, value):\r
+        if isinstance(value, basestring):\r
+            if value in ('asc', 'desc'):\r
+                self._direction = (value == 'asc') and Column.ASC or Column.DESC\r
+            else:\r
+                raise ValueError('Invalid direction value: %s' % value)\r
+        else:\r
+            self._direction = value\r
+\r
+    direction = property(lambda s: s._direction, _set_direction)\r
+\r
 class TextColumn(Column):\r
     pass\r
 \r
index 3f4e7945d0d4a65ea90b87f3ca940b7d25806efb..5f07a44339b93b482f3176d8ee83433c01d42004 100644 (file)
@@ -1,7 +1,7 @@
 from django.core.exceptions import FieldError\r
 from django.utils.datastructures import SortedDict\r
 from tables import BaseTable, DeclarativeColumnsMetaclass, \\r
-    Column, BoundRow, Rows, TableOptions\r
+    Column, BoundRow, Rows, TableOptions, rmprefix, toggleprefix\r
 \r
 __all__ = ('BaseModelTable', 'ModelTable')\r
 \r
@@ -122,7 +122,8 @@ class BaseModelTable(BaseTable):
 \r
         queryset = self.queryset\r
         if self.order_by:\r
-            queryset = queryset.order_by(*self._cols_to_fields(self.order_by))\r
+            actual_order_by = self._resolve_sort_directions(self.order_by)\r
+            queryset = queryset.order_by(*self._cols_to_fields(actual_order_by))\r
         self._snapshot = queryset\r
 \r
     def _get_rows(self):\r
index 56a9e3351a5db3f5282b578843cc7b80c0d3ac54..b7035c7ccc530ff58e67c2613ba737530acf1909 100644 (file)
@@ -95,6 +95,10 @@ def rmprefix(s):
     """Normalize a column name by removing a potential sort prefix"""\r
     return (s[:1]=='-' and [s[1:]] or [s])[0]\r
 \r
+def toggleprefix(s):\r
+    """Remove - prefix is existing, or add if missing."""\r
+    return ((s[:1] == '-') and [s[1:]] or ["-"+s])[0]\r
+\r
 class OrderByTuple(tuple, StrAndUnicode):\r
         """Stores 'order by' instructions; Used to render output in a format\r
         we understand as input (see __unicode__) - especially useful in\r
@@ -169,7 +173,7 @@ class OrderByTuple(tuple, StrAndUnicode):
                             or ((o[:1] == '-') and [o[1:]] or ["-"+o])\r
                       )[0]\r
                     for o in self]\r
-                    +  # !!!: test for addition\r
+                    +\r
                     [name for name in names if not name in self]\r
             )\r
 \r
@@ -256,7 +260,8 @@ class BaseTable(object):
                     row[name_in_source] = column.get_default(BoundRow(self, row))\r
 \r
         if self.order_by:\r
-            sort_table(snapshot, self._cols_to_fields(self.order_by))\r
+            actual_order_by = self._resolve_sort_directions(self.order_by)\r
+            sort_table(snapshot, self._cols_to_fields(actual_order_by))\r
         self._snapshot = snapshot\r
 \r
     def _get_data(self):\r
@@ -265,6 +270,18 @@ class BaseTable(object):
         return self._snapshot\r
     data = property(lambda s: s._get_data())\r
 \r
+    def _resolve_sort_directions(self, order_by):\r
+        """Given an ``order_by`` tuple, this will toggle the hyphen-prefixes\r
+        according to each column's ``direction`` option, e.g. it translates\r
+        between the ascending/descending and the straight/reverse terminology.\r
+        """\r
+        result = []\r
+        for inst in order_by:\r
+            if self.columns[rmprefix(inst)].column.direction == Column.DESC:\r
+                inst = toggleprefix(inst)\r
+            result.append(inst)\r
+        return result\r
+\r
     def _cols_to_fields(self, names):\r
         """Utility function. Given a list of column names (as exposed to\r
         the user), converts column names to the names we have to use to\r
index 48c67a33b018fe25059732951bf70ea5770e206b..f24ec58e44df5c093d5f5dec47f8371fcf7f8ca4 100644 (file)
@@ -154,7 +154,7 @@ def test_meta_sortable():
 \r
 def test_sort():\r
     class BookTable(tables.Table):\r
-        id = tables.Column()\r
+        id = tables.Column(direction='desc')\r
         name = tables.Column()\r
         pages = tables.Column(name='num_pages')  # test rewritten names\r
         language = tables.Column(default='en')   # default affects sorting\r
@@ -195,6 +195,16 @@ def test_sort():
     # sort by column with "data" option\r
     test_order('rating', [4,2,3,1])\r
 \r
+    # test the column with a default ``direction`` set to descending\r
+    test_order('id', [4,3,2,1])\r
+    test_order('-id', [1,2,3,4])\r
+    # changing the direction afterwards is fine too\r
+    books.base_columns['id'].direction = 'asc'\r
+    test_order('id', [1,2,3,4])\r
+    test_order('-id', [4,3,2,1])\r
+    # a invalid direction string raises an exception\r
+    raises(ValueError, "books.base_columns['id'].direction = 'blub'")\r
+\r
     # [bug] test alternative order formats if passed to constructor\r
     BookTable([], 'language,-num_pages')\r
 \r
index cb7c3a6bd62ba003247c7f638b48c3ee6777dd86..a2aff04d7d697a81fc702822619bf87156af71e1 100644 (file)
@@ -162,6 +162,7 @@ def test_caches():
 def test_sort():\r
     class CountryTable(tables.ModelTable):\r
         tld = tables.Column(name="domain")\r
+        population = tables.Column()\r
         system = tables.Column(default="republic")\r
         custom1 = tables.Column()\r
         custom2 = tables.Column(sortable=True)\r
@@ -169,9 +170,9 @@ def test_sort():
             model = Country\r
     countries = CountryTable()\r
 \r
-    def test_order(order, result):\r
-        countries.order_by = order\r
-        assert [r['id'] for r in countries.rows] == result\r
+    def test_order(order, result, table=countries):\r
+        table.order_by = order\r
+        assert [r['id'] for r in table.rows] == result\r
 \r
     # test various orderings\r
     test_order(('population',), [1,4,3,2])\r
@@ -184,10 +185,19 @@ def test_sort():
     # test multiple order instructions; note: one row is missing a "system"\r
     # value, but has a default set; however, that has no effect on sorting.\r
     test_order(('system', '-population'), [2,4,3,1])\r
-    # using a simple string (for convinience as well as querystring passing\r
+    # using a simple string (for convinience as well as querystring passing)\r
     test_order('-population', [2,3,4,1])\r
     test_order('system,-population', [2,4,3,1])\r
 \r
+    # test column with a default ``direction`` set to descending\r
+    class CityTable(tables.ModelTable):\r
+        name = tables.Column(direction='desc')\r
+        class Meta:\r
+            model = City\r
+    cities = CityTable()\r
+    test_order('name', [1,2], table=cities)   # Berlin to Amsterdam\r
+    test_order('-name', [2,1], table=cities)  # Amsterdam to Berlin\r
+\r
     # test invalid order instructions...\r
     countries.order_by = 'invalid_field,population'\r
     assert countries.order_by == ('population',)\r