count() to determine the data length (which is exactly what\r
QuerySetPaginator would do).\r
\r
-Ordering Syntax\r
----------------\r
+Ordering\r
+--------\r
\r
-Works exactly like in the Django database API. Order may be specified as\r
-a list (or tuple) of column names. If prefixed with a hypen, the ordering\r
-for that particular field will be in reverse order.\r
+The syntax is similar to that of the Django database API. Order may be\r
+specified a list (or tuple) of column names. If prefixed with a hyphen, the\r
+ordering for that particular column will be in reverse order.\r
\r
Random ordering is currently not supported.\r
\r
+Interacting with order\r
+~~~~~~~~~~~~~~~~~~~~~~\r
+\r
+Letting the user change the order of a table is a common scenario. With\r
+respect to Django, this means adding links to your table output that will\r
+send off the appropriate arguments to the server. django-tables attempts\r
+to help with you that.\r
+\r
+A bound column, that is a colum accessed through a table instance, provides\r
+the following attributes:\r
+\r
+ - ``name_reversed`` will simply return the column name prefixed with a\r
+ hyphen; this is useful in templates, where string concatenation can\r
+ at times be difficult.\r
+\r
+ - ``name_toggled`` checks the tables current order, and will then\r
+ return the column either prefixed with an hyphen (for reverse ordering)\r
+ or without, giving you the exact opposite order. If the column is\r
+ currently not ordered, it will start off in non-reversed order.\r
+\r
+It is easy to be confused about the difference between the ``reverse`` and\r
+``toggle`` terminology. django-tables tries to put normal/reverse-order\r
+abstraction on top of "ascending/descending", where as normal order could\r
+potentially mean either ascending or descending, depending on the column.\r
+\r
+Commonly, you see tables that indicate what columns they are currently\r
+ordered by using little arrows. To implement this:\r
+\r
+ - ``is_ordered``: Returns True if the column is in the current\r
+ ``order_by``, regardless of the polarity.\r
+\r
+ - ``is_ordered_reverse``, ``is_ordered_straight``: Returns True if the\r
+ column is ordered in reverse or non-reverse, respectively, otherwise\r
+ False.\r
+\r
+The above is usually enough for most simple cases, where tables are only\r
+ordered by a single column. For scenarios in which multi-column order is\r
+used, additional attributes are available:\r
+\r
+ - ``order_by``: Return the current order, but with the current column\r
+ set to normal ordering. If the current column is not already part of\r
+ the order, it is appended. Any existing columns in the order are\r
+ maintained as-is.\r
+\r
+ - ``order_by_reversed``, ``order_by_toggled``: Similarly, return the\r
+ table's current ``order_by`` with the column set to reversed or toggled,\r
+ respectively. Again, it is appended if not already ordered.\r
+\r
+Additionally, ``table.order_by.toggle()`` may also be useful in some cases:\r
+It will toggle all order columns and should thus give you the exact\r
+opposite order.\r
+\r
+The following is a simple example of single-column ordering. It shows a list\r
+of sortable columns, each clickable, and an up/down arrow next to the one\r
+that is currently used to sort the table.\r
+\r
+ Sort by:\r
+ {% for column in table.columns %}\r
+ {% if column.sortable %}\r
+ <a href="?sort={{ column.name_toggled }}">{{ column }}</a>\r
+ {% if column.is_ordered_straight %}<img src="down.png" />{% endif %}\r
+ {% if column.is_ordered_reverse %}<img src="up.png" />{% endif %}\r
+ {% endif %}\r
+ {% endfor %}\r
+\r
\r
Error handling\r
--------------\r
\r
return type.__new__(cls, name, bases, attrs)\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
+\r
class OrderByTuple(tuple, StrAndUnicode):\r
- """Stores 'order by' instructions; Currently only used to render\r
- to the output (especially in templates) in a format we understand\r
- as input.\r
+ """Stores 'order by' instructions; Used to render output in a format\r
+ we understand as input (see __unicode__) - especially useful in\r
+ templates.\r
+\r
+ Also supports some functionality to interact with and modify\r
+ the order.\r
"""\r
def __unicode__(self):\r
+ """Output in our input format."""\r
return ",".join(self)\r
\r
+ def __contains__(self, name):\r
+ """Determine whether a column is part of this order."""\r
+ for o in self:\r
+ if rmprefix(o) == name:\r
+ return True\r
+ return False\r
+\r
+ def is_reversed(self, name):\r
+ """Returns a bool indicating whether the column is ordered\r
+ reversed, None if it is missing."""\r
+ for o in self:\r
+ if o == '-'+name:\r
+ return True\r
+ return False\r
+ def is_straight(self, name):\r
+ """The opposite of is_reversed."""\r
+ for o in self:\r
+ if o == name:\r
+ return True\r
+ return False\r
+\r
+ def polarize(self, reverse, names=()):\r
+ """Return a new tuple with the columns from ``names`` set to\r
+ "reversed" (e.g. prefixed with a '-'). Note that the name is\r
+ ambiguous - do not confuse this with ``toggle()``.\r
+\r
+ If names is not specified, all columns are reversed. If a\r
+ column name is given that is currently not part of the order,\r
+ it is added.\r
+ """\r
+ prefix = reverse and '-' or ''\r
+ return OrderByTuple(\r
+ [\r
+ (\r
+ # add either untouched, or reversed\r
+ (names and rmprefix(o) not in names)\r
+ and [o]\r
+ or [prefix+rmprefix(o)]\r
+ )[0]\r
+ for o in self]\r
+ +\r
+ [prefix+name for name in names if not name in self]\r
+ )\r
+\r
+ def toggle(self, names=()):\r
+ """Return a new tuple with the columns from ``names`` toggled\r
+ with respect to their "reversed" state. E.g. a '-' prefix will\r
+ be removed is existing, or added if lacking. Do not confuse\r
+ with ``reverse()``.\r
+\r
+ If names is not specified, all columns are toggled. If a\r
+ column name is given that is currently not part of the order,\r
+ it is added in non-reverse form."""\r
+ return OrderByTuple(\r
+ [\r
+ (\r
+ # add either untouched, or toggled\r
+ (names and rmprefix(o) not in names)\r
+ and [o]\r
+ or ((o[:1] == '-') and [o[1:]] or ["-"+o])\r
+ )[0]\r
+ for o in self]\r
+ + # !!!: test for addition\r
+ [name for name in names if not name in self]\r
+ )\r
+\r
+\r
# A common use case is passing incoming query values directly into the\r
# table constructor - data that can easily be invalid, say if manually\r
# modified by a user. So by default, such errors will be silently\r
# validate, remove all invalid order instructions\r
validated_order_by = []\r
for o in order_by:\r
- if self._validate_column_name((o[:1]=='-' and [o[1:]] or [o])[0], "order_by"):\r
+ if self._validate_column_name(rmprefix(o), "order_by"):\r
validated_order_by.append(o)\r
elif not options.IGNORE_INVALID_OPTIONS:\r
raise ValueError('Column name %s is invalid.' % o)\r
if exposed_name in self._columns:\r
new_columns[exposed_name] = self._columns[exposed_name]\r
else:\r
- new_columns[exposed_name] = BoundColumn(self, column, decl_name)\r
+ new_columns[exposed_name] = BoundColumn(self.table, column, decl_name)\r
self._columns = new_columns\r
\r
def all(self):\r
self.visible = column.visible\r
\r
name = property(lambda s: s.column.name or s.declared_name)\r
+ name_reversed = property(lambda s: "-"+s.name)\r
+ def _get_name_toggled(self):\r
+ o = self.table.order_by\r
+ if (not self.name in o) or o.is_reversed(self.name): return self.name\r
+ else: return self.name_reversed\r
+ name_toggled = property(_get_name_toggled)\r
+\r
+ is_ordered = property(lambda s: s.name in s.table.order_by)\r
+ is_ordered_reverse = property(lambda s: s.table.order_by.is_reversed(s.name))\r
+ is_ordered_straight = property(lambda s: s.table.order_by.is_straight(s.name))\r
+ order_by = property(lambda s: s.table.order_by.polarize(False, [s.name]))\r
+ order_by_reversed = property(lambda s: s.table.order_by.polarize(True, [s.name]))\r
+ order_by_toggled = property(lambda s: s.table.order_by.toggle([s.name]))\r
\r
def get_default(self, row):\r
"""Since a column's ``default`` property may be a callable, we need\r
this function to resolve it when needed.\r
\r
- Make sure ``row`` is a ``BoundRow`` objects, since that is what\r
+ Make sure ``row`` is a ``BoundRow`` object, since that is what\r
we promise the callable will get.\r
"""\r
if callable(self.column.default):\r
{'id': 4, 'pages': 110, 'language': 'fr', 'name': 'A: The Book, French Edition'}, # rating (with data option) is missing\r
])\r
\r
- def test_order(order, result):\r
- books.order_by = order\r
- assert [b['id'] for b in books.rows] == result\r
-\r
# None is normalized to an empty order by tuple, ensuring iterability;\r
# it also supports all the cool methods that we offer for order_by.\r
# This is true for the default case...\r
assert books.order_by == ()\r
iter(books.order_by)\r
- assert hasattr(books.order_by, 'reverse')\r
+ assert hasattr(books.order_by, 'toggle')\r
# ...as well as when explicitly set to None.\r
books.order_by = None\r
assert books.order_by == ()\r
iter(books.order_by)\r
- assert hasattr(books.order_by, 'reverse')\r
+ assert hasattr(books.order_by, 'toggle')\r
\r
# test various orderings\r
+ def test_order(order, result):\r
+ books.order_by = order\r
+ assert [b['id'] for b in books.rows] == result\r
test_order(('num_pages',), [1,3,2,4])\r
test_order(('-num_pages',), [4,2,3,1])\r
test_order(('name',), [2,4,3,1])\r
books = BookTable([], order_by='name')\r
assert books.order_by == ('name',)\r
\r
+ # test table.order_by extensions\r
+ books.order_by = ''\r
+ assert books.order_by.polarize(False) == ()\r
+ assert books.order_by.polarize(True) == ()\r
+ assert books.order_by.toggle() == ()\r
+ assert books.order_by.polarize(False, ['id']) == ('id',)\r
+ assert books.order_by.polarize(True, ['id']) == ('-id',)\r
+ assert books.order_by.toggle(['id']) == ('id',)\r
+ books.order_by = 'id,-name'\r
+ assert books.order_by.polarize(False, ['name']) == ('id', 'name')\r
+ assert books.order_by.polarize(True, ['name']) == ('id', '-name')\r
+ assert books.order_by.toggle(['name']) == ('id', 'name')\r
+ # ``in`` operator works\r
+ books.order_by = 'name'\r
+ assert 'name' in books.order_by\r
+ books.order_by = '-name'\r
+ assert 'name' in books.order_by\r
+ assert not 'language' in books.order_by\r
+\r
def test_callable():\r
"""Data fields, ``default`` and ``data`` options can be callables.\r
"""\r
# new attributes\r
assert books.paginator.num_pages == 10\r
assert books.page.has_previous() == False\r
- assert books.page.has_next() == True
\ No newline at end of file
+ assert books.page.has_next() == True\r
+\r
+def test_columns():\r
+ """Test specific column features.\r
+\r
+ Might warrant it's own test file."""\r
+\r
+ class BookTable(tables.Table):\r
+ id = tables.Column()\r
+ name = tables.Column()\r
+ pages = tables.Column()\r
+ language = tables.Column()\r
+ books = BookTable([])\r
+\r
+ # the basic name property is a no-brainer\r
+ books.order_by = ''\r
+ assert [c.name for c in books.columns] == ['id','name','pages','language']\r
+\r
+ # name_reversed will always reverse, no matter what\r
+ for test in ['', 'name', '-name']:\r
+ books.order_by = test\r
+ assert [c.name_reversed for c in books.columns] == ['-id','-name','-pages','-language']\r
+\r
+ # name_toggled will always toggle\r
+ books.order_by = ''\r
+ assert [c.name_toggled for c in books.columns] == ['id','name','pages','language']\r
+ books.order_by = 'id'\r
+ assert [c.name_toggled for c in books.columns] == ['-id','name','pages','language']\r
+ books.order_by = '-name'\r
+ assert [c.name_toggled for c in books.columns] == ['id','name','pages','language']\r
+ # other columns in an order_by will be dismissed\r
+ books.order_by = '-id,name'\r
+ assert [c.name_toggled for c in books.columns] == ['id','-name','pages','language']\r
+\r
+ # with multi-column order, this is slightly more complex\r
+ books.order_by = ''\r
+ assert [str(c.order_by) for c in books.columns] == ['id','name','pages','language']\r
+ assert [str(c.order_by_reversed) for c in books.columns] == ['-id','-name','-pages','-language']\r
+ assert [str(c.order_by_toggled) for c in books.columns] == ['id','name','pages','language']\r
+ books.order_by = 'id'\r
+ assert [str(c.order_by) for c in books.columns] == ['id','id,name','id,pages','id,language']\r
+ assert [str(c.order_by_reversed) for c in books.columns] == ['-id','id,-name','id,-pages','id,-language']\r
+ assert [str(c.order_by_toggled) for c in books.columns] == ['-id','id,name','id,pages','id,language']\r
+ books.order_by = '-pages,id'\r
+ assert [str(c.order_by) for c in books.columns] == ['-pages,id','-pages,id,name','pages,id','-pages,id,language']\r
+ assert [str(c.order_by_reversed) for c in books.columns] == ['-pages,-id','-pages,id,-name','-pages,id','-pages,id,-language']\r
+ assert [str(c.order_by_toggled) for c in books.columns] == ['-pages,-id','-pages,id,name','pages,id','-pages,id,language']\r
+\r
+ # querying whether a column is ordered is possible\r
+ books.order_by = ''\r
+ assert [c.is_ordered for c in books.columns] == [False, False, False, False]\r
+ books.order_by = 'name'\r
+ assert [c.is_ordered for c in books.columns] == [False, True, False, False]\r
+ assert [c.is_ordered_reverse for c in books.columns] == [False, False, False, False]\r
+ assert [c.is_ordered_straight for c in books.columns] == [False, True, False, False]\r
+ books.order_by = '-pages'\r
+ assert [c.is_ordered for c in books.columns] == [False, False, True, False]\r
+ assert [c.is_ordered_reverse for c in books.columns] == [False, False, True, False]\r
+ assert [c.is_ordered_straight for c in books.columns] == [False, False, False, False]\r
+ # and even works with multi-column ordering\r
+ books.order_by = 'id,-pages'\r
+ assert [c.is_ordered for c in books.columns] == [True, False, True, False]\r
+ assert [c.is_ordered_reverse for c in books.columns] == [False, False, True, False]\r
+ assert [c.is_ordered_straight for c in books.columns] == [True, False, False, False]
\ No newline at end of file