Tables and Pagination\r
---------------------\r
\r
- table = MyTable(queryset)\r
- p = Paginator(table.rows, 10) # paginator will need to be able to handle our modelproxy\r
+If your table has a large number of rows, you probably want to paginate\r
+the output. There are two distinct approaches.\r
\r
- or\r
+First, you can just paginate over ``rows`` as you would do with any other\r
+data:\r
\r
table = MyTable(queryset)\r
- table.pagination = Paginator(10, padding=2)\r
+ paginator = Paginator(table.rows, 10)\r
+ page = paginator.page(1)\r
\r
- or\r
+You're not necessarily restricted to Django's own paginator (or subclasses) -\r
+any paginator should work with this approach, so long it only requires\r
+``rows`` to implement ``len()``, slicing, and, in the case of ModelTables, a\r
+``count()`` method. The latter means that the ``QuerySetPaginator`` also\r
+works as expected.\r
\r
- table.paginate(DiggPaginator, 10, padding=2)\r
+Alternatively, you may use the ``paginate`` feature:\r
\r
+ table = MyTable(queryset)\r
+ table.paginate(QuerySetPaginator, page=1, 10, padding=2)\r
+ for row in table.rows.page():\r
+ pass\r
+ table.paginator # new attributes\r
+ table.page\r
+\r
+The table will automatically create an instance of ``QuerySetPaginator``,\r
+passing it's own data as the first argument and additionally any arguments\r
+you have specified, except for ``page``. You may use any paginator, as long\r
+as it follows the Django protocol:\r
+\r
+ * Take data as first argument.\r
+ * Support a page() method returning an object with an ``object_list``\r
+ attribute, exposing the paginated data.\r
+\r
+Note that due to the abstraction layer that django-tables represents, it is\r
+not necessary to use Django's QuerySetPaginator with ModelTables. Since the\r
+table knows that it holds a queryset, it will automatically choose to use\r
+count() to determine the data length (which is exactly what\r
+QuerySetPaginator would do).\r
\r
Ordering Syntax\r
---------------\r
the template engine handle it\r
- tests could use some refactoring, they are currently all over the place\r
- what happens if duplicate column names are used? we currently don't\r
- check for that at all.
\ No newline at end of file
+ check for that at all
\ No newline at end of file
from django.core.exceptions import FieldError\r
from django.utils.datastructures import SortedDict\r
-from tables import BaseTable, DeclarativeColumnsMetaclass, Column, BoundRow\r
+from tables import BaseTable, DeclarativeColumnsMetaclass, \\r
+ Column, BoundRow, Rows\r
\r
__all__ = ('BaseModelTable', 'ModelTable')\r
\r
self.queryset = data\r
\r
super(BaseModelTable, self).__init__(self.queryset, *args, **kwargs)\r
+ self._rows = ModelRows(self)\r
\r
def _validate_column_name(self, name, purpose):\r
"""Overridden. Only allow model-based fields and valid model\r
\r
# reset caches\r
self._columns._reset()\r
- self._rows = None\r
+ self._rows._reset()\r
\r
queryset = self.queryset\r
if self.order_by:\r
class ModelTable(BaseModelTable):\r
__metaclass__ = ModelTableMetaclass\r
\r
+class ModelRows(Rows):\r
+ def __init__(self, *args, **kwargs):\r
+ super(ModelRows, self).__init__(*args, **kwargs)\r
+ self.row_klass = BoundModelRow\r
+\r
+ def _reset(self):\r
+ self._length = None\r
+\r
+ def __len__(self):\r
+ """Use the queryset count() method to get the length, instead of\r
+ loading all results into memory. This allows, for example,\r
+ smart paginators that use len() to perform better.\r
+ """\r
+ if getattr(self, '_length', None) is None:\r
+ self._length = self.table.data.count()\r
+ return self._length\r
+\r
+ # for compatibility with QuerySetPaginator\r
+ count = __len__\r
+\r
class BoundModelRow(BoundRow):\r
"""Special version of the BoundRow class that can handle model instances\r
as data.\r
"""\r
self._data = data\r
self._snapshot = None # will store output dataset (ordered...)\r
- self._rows = None # will store BoundRow objects\r
+ self._rows = Rows(self)\r
self._columns = Columns(self)\r
self._order_by = order_by\r
\r
\r
# reset caches\r
self._columns._reset()\r
- self._rows = None\r
+ self._rows._reset()\r
\r
snapshot = copy.copy(self._data)\r
for row in snapshot:\r
raise KeyError('Key %r not found in Table' % name)\r
return BoundColumn(self, column, name)\r
\r
- columns = property(lambda s: s._columns) # just to make it readonly\r
-\r
- def _get_rows(self):\r
- for row in self.data:\r
- yield BoundRow(self, row)\r
- rows = property(lambda s: s._get_rows())\r
+ # just to make those readonly\r
+ columns = property(lambda s: s._columns)\r
+ rows = property(lambda s: s._rows)\r
\r
def as_html(self):\r
pass\r
"""\r
self._build_snapshot()\r
\r
+ def paginate(self, klass, *args, **kwargs):\r
+ page = kwargs.pop('page', 1)\r
+ self.paginator = klass(self.rows, *args, **kwargs)\r
+ self.page = self.paginator.page(page)\r
+\r
+\r
class Table(BaseTable):\r
"A collection of columns, plus their associated data rows."\r
# This is a separate class from BaseTable in order to abstract the way\r
This is bound to a table and provides it's ``columns`` property. It\r
provides access to those columns in different ways (iterator,\r
item-based, filtered and unfiltered etc)., stuff that would not be\r
- possible with a simple iterator on the table class.\r
+ possible with a simple iterator in the table class.\r
\r
Note that when you define your column using a name override, e.g.\r
``author_name = tables.Column(name="author")``, then the column will\r
def as_html(self):\r
pass\r
\r
+class Rows(object):\r
+ """Container for spawning BoundRows.\r
+\r
+ This is bound to a table and provides it's ``rows`` property. It\r
+ provides functionality that would not be possible with a simple\r
+ iterator in the table class.\r
+ """\r
+ def __init__(self, table, row_klass=None):\r
+ self.table = table\r
+ self.row_klass = row_klass and row_klass or BoundRow\r
+\r
+ def _reset(self):\r
+ pass # we currently don't use a cache\r
+\r
+ def all(self):\r
+ """Return all rows."""\r
+ for row in self.table.data:\r
+ yield self.row_klass(self.table, row)\r
+\r
+ def page(self):\r
+ """Return rows on current page (if paginated)."""\r
+ if not hasattr(self.table, 'page'):\r
+ return None\r
+ return iter(self.table.page.object_list)\r
+\r
+ def __iter__(self):\r
+ return iter(self.all())\r
+\r
+ def __len__(self):\r
+ return len(self.table.data)\r
+\r
+ def __getitem__(self, key):\r
+ if isinstance(key, slice):\r
+ result = list()\r
+ for row in self.table.data[key]:\r
+ result.append(self.row_klass(self.table, row))\r
+ return result\r
+ else:\r
+ return self.row_klass(self, table, self.table.data[key])\r
+\r
class BoundRow(object):\r
"""Represents a single row of data, bound to a table.\r
\r
\r
from math import sqrt\r
from py.test import raises\r
+from django.core.paginator import Paginator\r
import django_tables as tables\r
\r
def test_declaration():\r
\r
# data function is called while sorting\r
math.order_by = ('sqrt',)\r
- assert [row['sqrt'] for row in math] == [1,1,2]
\ No newline at end of file
+ assert [row['sqrt'] for row in math] == [1,1,2]\r
+\r
+def test_pagination():\r
+ class BookTable(tables.Table):\r
+ name = tables.Column()\r
+\r
+ # create some sample data\r
+ data = []\r
+ for i in range(1,101):\r
+ data.append({'name': 'Book Nr. %d'%i})\r
+ books = BookTable(data)\r
+\r
+ # external paginator\r
+ paginator = Paginator(books.rows, 10)\r
+ assert paginator.num_pages == 10\r
+ page = paginator.page(1)\r
+ assert len(page.object_list) == 10\r
+ assert page.has_previous() == False\r
+ assert page.has_next() == True\r
+\r
+ # integrated paginator\r
+ books.paginate(Paginator, 10, page=1)\r
+ # rows is now paginated\r
+ assert len(list(books.rows.page())) == 10\r
+ assert len(list(books.rows.all())) == 100\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
\r
from py.test import raises\r
from django.conf import settings\r
+from django.core.paginator import *\r
import django_tables as tables\r
\r
def setup_module(module):\r
countries.order_by = ('custom1', 'custom2')\r
assert countries.order_by == ()\r
\r
-def test_pagination():\r
- # TODO: support pagination\r
- pass\r
-\r
def test_callable():\r
"""Some of the callable code is reimplemented for modeltables, so\r
test some specifics again.\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',)
\ No newline at end of file
+ assert countries.order_by == ('name',)\r
+\r
+def test_pagination():\r
+ """Pretty much the same as static table pagination, but make sure we\r
+ provide the capability, at least for paginators that use it, to not\r
+ have the complete queryset loaded (by use of a count() query).\r
+\r
+ Note: This test changes the available cities, make sure it is last,\r
+ or that tests that follow are written appropriately.\r
+ """\r
+ from django.db import connection\r
+\r
+ class CityTable(tables.ModelTable):\r
+ class Meta:\r
+ model = City\r
+ columns = ['name']\r
+ cities = CityTable()\r
+\r
+ # add some sample data\r
+ City.objects.all().delete()\r
+ for i in range(1,101):\r
+ City.objects.create(name="City %d"%i)\r
+\r
+ # for query logging\r
+ settings.DEBUG = True\r
+\r
+ # external paginator\r
+ start_querycount = len(connection.queries)\r
+ paginator = Paginator(cities.rows, 10)\r
+ assert paginator.num_pages == 10\r
+ page = paginator.page(1)\r
+ assert len(page.object_list) == 10\r
+ assert page.has_previous() == False\r
+ assert page.has_next() == True\r
+ # Make sure the queryset is not loaded completely - there must be two\r
+ # queries, one a count(). This check is far from foolproof...\r
+ assert len(connection.queries)-start_querycount == 2\r
+\r
+ # using a queryset paginator is possible as well (although unnecessary)\r
+ paginator = QuerySetPaginator(cities.rows, 10)\r
+ assert paginator.num_pages == 10\r
+\r
+ # integrated paginator\r
+ start_querycount = len(connection.queries)\r
+ cities.paginate(Paginator, 10, page=1)\r
+ # rows is now paginated\r
+ assert len(list(cities.rows.page())) == 10\r
+ assert len(list(cities.rows.all())) == 100\r
+ # new attributes\r
+ assert cities.paginator.num_pages == 10\r
+ assert cities.page.has_previous() == False\r
+ assert cities.page.has_next() == True\r
+ assert len(connection.queries)-start_querycount == 2\r
+\r
+ # reset\r
+ settings.DEBUG = False
\ No newline at end of file