* Added pagination v0.4.0.beta3
authorBradley Ayers <bradley.ayers@enigmainteractive.com>
Mon, 4 Apr 2011 05:12:09 +0000 (15:12 +1000)
committerBradley Ayers <bradley.ayers@enigmainteractive.com>
Mon, 4 Apr 2011 05:12:09 +0000 (15:12 +1000)
* TemplateColumn now renders with a RequestContext if using {% render_table %}
* Column accessors no longer need to successfully resolve. If the resolve fails, the default value will be used.

13 files changed:
django_tables/columns.py
django_tables/proxies.py [new file with mode: 0644]
django_tables/rows.py
django_tables/static/django_tables/themes/paleblue/css/screen.css
django_tables/static/django_tables/themes/paleblue/img/header-bg.gif [moved from django_tables/static/django_tables/themes/paleblue/img/nav-bg.gif with 100% similarity]
django_tables/static/django_tables/themes/paleblue/img/pagination-bg.gif [new file with mode: 0644]
django_tables/tables.py
django_tables/templates/django_tables/table.html
django_tables/templatetags/django_tables.py
docs/conf.py
docs/index.rst
setup.py
tests/core.py

index 24001b5d0ea5de1f730eaafbf0ed4b1c5e19bf4b..9bb29e97639b38042633be0c08b211dfb7428a14 100644 (file)
@@ -4,7 +4,7 @@ from django.utils.encoding import force_unicode, StrAndUnicode
 from django.utils.datastructures import SortedDict
 from django.utils.text import capfirst
 from django.utils.safestring import mark_safe
-from django.template import Context, Template
+from django.template import RequestContext, Context, Template
 from .utils import OrderBy, A, AttributeDict
 
 
@@ -119,7 +119,15 @@ class CheckBoxColumn(Column):
     used as the value for the checkbox, i.e. ``<input type="checkbox"
     value="..." />``
 
-    By default this column is not sortable.
+    This class implements some sensible defaults:
+
+    - The ``name`` attribute of the input is the name of the :term:`column
+      name` (can be overriden via ``attrs`` argument).
+    - The ``sortable`` parameter defaults to :const:`False`.
+    - The ``type`` attribute of the input is ``checkbox`` (can be overriden via
+      ``attrs`` argument).
+    - The header checkbox is left bare, i.e. ``<input type="checkbox"/>`` (use
+      the ``header_attrs`` argument to customise).
 
     .. note:: The "apply some operation onto the selection" functionality is
         not implemented in this column, and requires manually implemention.
@@ -127,17 +135,24 @@ class CheckBoxColumn(Column):
     :param attrs:
         a :class:`dict` of HTML attributes that are added to the rendered
         ``<input type="checkbox" .../>`` tag
+    :param header_attrs:
+        same as *attrs*, but applied **only** to the header checkbox
 
     """
-    def __init__(self, attrs=None, **extra):
+    def __init__(self, attrs=None, header_attrs=None, **extra):
         params = {'sortable': False}
         params.update(extra)
         super(CheckBoxColumn, self).__init__(**params)
         self.attrs = attrs or {}
+        self.header_attrs = header_attrs or {}
 
     @property
     def header(self):
-        return mark_safe('<input type="checkbox"/>')
+        attrs = AttributeDict({
+            'type': 'checkbox',
+        })
+        attrs.update(self.header_attrs)
+        return mark_safe('<input %s/>' % attrs.as_html())
 
     def render(self, value, bound_column, **kwargs):
         attrs = AttributeDict({
@@ -261,14 +276,24 @@ class TemplateColumn(Column):
 
     Both columns will have the same output.
 
+
+    .. important::
+        In order to use template tags or filters that require a
+        ``RequestContext``, the table **must** be rendered via
+        :ref:`{% render_table %} <template-tags.render_table>`.
+
     """
     def __init__(self, template_code=None, **extra):
         super(TemplateColumn, self).__init__(**extra)
         self.template_code = template_code
 
-    def render(self, record, **kwargs):
+    def render(self, record, table, **kwargs):
         t = Template(self.template_code)
-        return t.render(Context({'record': record}))
+        if hasattr(table, 'request'):
+            context = RequestContext(table.request, {'record': record})
+        else:
+            context = Context({'record': record})
+        return t.render(context)
 
 
 class BoundColumn(object):
@@ -334,7 +359,7 @@ class BoundColumn(object):
         column.
 
         """
-        return self.verbose_name
+        return self.column.header or self.verbose_name
 
     @property
     def name(self):
diff --git a/django_tables/proxies.py b/django_tables/proxies.py
new file mode 100644 (file)
index 0000000..95b919d
--- /dev/null
@@ -0,0 +1,154 @@
+from django.utils.functional import Promise
+
+
+class AbstractProxy(object):
+    """Delegates all operations (except ``.__subject__``) to another object"""
+    __slots__ = ()
+
+    #def __call__(self, *args, **kw):
+    #    return self.__subject__(*args, **kw)
+
+    def __getattribute__(self, attr, oga=object.__getattribute__):
+        subject = oga(self,'__subject__')
+        if attr=='__subject__':
+            return subject
+        return getattr(subject,attr)
+
+    def __setattr__(self, attr, val, osa=object.__setattr__):
+        if attr == '__subject__':
+            osa(self, attr, val)
+        else:
+            setattr(self.__subject__, attr, val)
+
+    def __delattr__(self, attr, oda=object.__delattr__):
+        if attr=='__subject__':
+            oda(self,attr)
+        else:
+            delattr(self.__subject__, attr)
+
+    def __nonzero__(self):
+        return bool(self.__subject__)
+
+    def __getitem__(self, arg):
+        return self.__subject__[arg]
+
+    def __setitem__(self, arg, val):
+        self.__subject__[arg] = val
+
+    def __delitem__(self, arg):
+        del self.__subject__[arg]
+
+    def __getslice__(self, i, j):
+        return self.__subject__[i:j]
+
+
+    def __setslice__(self, i, j, val):
+        self.__subject__[i:j] = val
+
+    def __delslice__(self, i, j):
+        del self.__subject__[i:j]
+
+    def __contains__(self, ob):
+        return ob in self.__subject__
+
+    for name in 'repr str hash len abs complex int long float iter oct hex'.split():
+        exec "def __%s__(self): return %s(self.__subject__)" % (name, name)
+
+    for name in 'cmp', 'coerce', 'divmod':
+        exec "def __%s__(self,ob): return %s(self.__subject__,ob)" % (name, name)
+
+    for name, op in [
+        ('lt','<'), ('gt','>'), ('le','<='), ('ge','>='),
+        ('eq','=='), ('ne','!=')
+    ]:
+        exec "def __%s__(self,ob): return self.__subject__ %s ob" % (name, op)
+
+    for name, op in [('neg','-'), ('pos','+'), ('invert','~')]:
+        exec "def __%s__(self): return %s self.__subject__" % (name, op)
+
+    for name, op in [
+        ('or','|'),  ('and','&'), ('xor','^'), ('lshift','<<'), ('rshift','>>'),
+        ('add','+'), ('sub','-'), ('mul','*'), ('div','/'), ('mod','%'),
+        ('truediv','/'), ('floordiv','//')
+    ]:
+        exec (
+            "def __%(name)s__(self,ob):\n"
+            "    return self.__subject__ %(op)s ob\n"
+            "\n"
+            "def __r%(name)s__(self,ob):\n"
+            "    return ob %(op)s self.__subject__\n"
+            "\n"
+            "def __i%(name)s__(self,ob):\n"
+            "    self.__subject__ %(op)s=ob\n"
+            "    return self\n"
+        )  % locals()
+
+    del name, op
+
+    # Oddball signatures
+
+    def __rdivmod__(self,ob):
+        return divmod(ob, self.__subject__)
+
+    def __pow__(self, *args):
+        return pow(self.__subject__, *args)
+
+    def __ipow__(self, ob):
+        self.__subject__ **= ob
+        return self
+
+    def __rpow__(self, ob):
+        return pow(ob, self.__subject__)
+
+
+class ObjectProxy(AbstractProxy):
+    """Proxy for a specific object"""
+
+    __slots__ = "__subject__"
+
+    def __init__(self, subject):
+        self.__subject__ = subject
+
+
+class CallbackProxy(AbstractProxy):
+    """Proxy for a dynamically-chosen object"""
+
+    __slots__ = '__callback__'
+
+    def __init__(self, func):
+        set_callback(self, func)
+
+set_callback = CallbackProxy.__callback__.__set__
+get_callback = CallbackProxy.__callback__.__get__
+CallbackProxy.__subject__ = property(lambda self, gc=get_callback: gc(self)())
+
+
+class LazyProxy(CallbackProxy):
+    """Proxy for a lazily-obtained object, that is cached on first use"""
+    __slots__ = "__cache__"
+
+get_cache = LazyProxy.__cache__.__get__
+set_cache = LazyProxy.__cache__.__set__
+
+def __subject__(self, get_cache=get_cache, set_cache=set_cache):
+    try:
+        return get_cache(self)
+    except AttributeError:
+        set_cache(self, get_callback(self)())
+        return get_cache(self)
+
+LazyProxy.__subject__ = property(__subject__, set_cache)
+del __subject__
+
+
+class TemplateSafeLazyProxy(LazyProxy):
+    """
+    A version of LazyProxy suitable for use in Django templates.
+
+    It's important that an ``alters_data`` attribute returns :const:`False`.
+
+    """
+    def __getattribute__(self, attr, *args, **kwargs):
+        if attr == 'alters_data':
+            return False
+        return LazyProxy.__getattribute__(self, attr, *args, **kwargs)
index 6fab20264be04d7f23ca4302c44cb957f292113b..1b662b99d80a94b0512bbc6d36a1c0d47fe38b4a 100644 (file)
@@ -1,14 +1,17 @@
 # -*- coding: utf-8 -*-
 from django.utils.safestring import EscapeUnicode, SafeData
+from .proxies import TemplateSafeLazyProxy
+import itertools
 
 
 class BoundRow(object):
-    """Represents a *specific* row in a table.
+    """
+    Represents a *specific* row in a table.
 
-    :class:`BoundRow` objects are a container that make it easy to access the
+    :class:`.BoundRow` objects are a container that make it easy to access the
     final 'rendered' values for cells in a row. You can simply iterate over a
-    :class:`BoundRow` object and it will take care to return values rendered
-    using the correct method (e.g. :meth:`Column.render_FOO`)
+    :class:`.BoundRow` object and it will take care to return values rendered
+    using the correct method (e.g. :meth:`.Column.render_FOO`)
 
     To access the rendered value of each cell in a row, just iterate over it:
 
@@ -55,37 +58,36 @@ class BoundRow(object):
         ...
         KeyError: 'c'
 
+    :param table: is the :class:`Table` in which this row exists.
+    :param record: a single record from the :term:`table data` that is used to
+        populate the row. A record could be a :class:`Model` object, a
+        :class:`dict`, or something else.
+
     """
     def __init__(self, table, record):
-        """Initialise a new :class:`BoundRow` object where:
-
-        * *table* is the :class:`Table` in which this row exists.
-        * *record* is a single record from the data source that is posed to
-          populate the row. A record could be a :class:`Model` object, a
-          ``dict``, or something else.
-
-        """
         self._table = table
         self._record = record
 
     @property
     def table(self):
-        """The associated :term:`table`."""
+        """The associated :class:`.Table` object."""
         return self._table
 
     @property
     def record(self):
-        """The data record from the data source which is used to populate this
-        row with data.
+        """
+        The data record from the data source which is used to populate this row
+        with data.
 
         """
         return self._record
 
     def __iter__(self):
-        """Iterate over the rendered values for cells in the row.
+        """
+        Iterate over the rendered values for cells in the row.
 
-        Under the hood this method just makes a call to :meth:`__getitem__` for
-        each cell.
+        Under the hood this method just makes a call to
+        :meth:`.BoundRow.__getitem__` for each cell.
 
         """
         for column in self.table.columns:
@@ -94,14 +96,22 @@ class BoundRow(object):
             yield self[column.name]
 
     def __getitem__(self, name):
-        """Returns the final rendered value for a cell in the row, given the
-        name of a column.
+        """
+        Returns the final rendered value for a cell in the row, given the name
+        of a column.
 
         """
         bound_column = self.table.columns[name]
-        raw = bound_column.accessor.resolve(self.record)
+
+        def value():
+            try:
+                raw = bound_column.accessor.resolve(self.record)
+            except (TypeError, AttributeError, KeyError, ValueError) as e:
+                raw = None
+            return raw if raw is not None else bound_column.default
+
         kwargs = {
-            'value': raw if raw is not None else bound_column.default,
+            'value': TemplateSafeLazyProxy(value),
             'record': self.record,
             'column': bound_column.column,
             'bound_column': bound_column,
@@ -137,22 +147,20 @@ class BoundRows(object):
     """
     Container for spawning :class:`.BoundRow` objects.
 
-    The :attr:`.tables.Table.rows` attribute is a :class:`.BoundRows` object.
+    The :attr:`.Table.rows` attribute is a :class:`.BoundRows` object.
     It provides functionality that would not be possible with a simple iterator
     in the table class.
 
+    :type table: :class:`.Table` object
+    :param table: the table in which the rows exist.
+
     """
     def __init__(self, table):
-        """
-        Initialise a :class:`Rows` object. *table* is the :class:`Table` object
-        in which the rows exist.
-
-        """
         self.table = table
 
     def all(self):
         """
-        Return an iterable for all :class:`BoundRow` objects in the table.
+        Return an iterable for all :class:`.BoundRow` objects in the table.
 
         """
         for record in self.table.data:
@@ -161,8 +169,9 @@ class BoundRows(object):
     def page(self):
         """
         If the table is paginated, return an iterable of :class:`.BoundRow`
-        objects that appear on the current page, otherwise :const:`None`.
+        objects that appear on the current page.
 
+        :rtype: iterable of :class:`.BoundRow` objects, or :const:`None`.
         """
         if not hasattr(self.table, 'page'):
             return None
@@ -182,10 +191,8 @@ class BoundRows(object):
     def __getitem__(self, key):
         """Allows normal list slicing syntax to be used."""
         if isinstance(key, slice):
-            result = list()
-            for row in self.table.data[key]:
-                result.append(BoundRow(self.table, row))
-            return result
+            return itertools.imap(lambda record: BoundRow(self.table, record),
+                                  self.table.data[key])
         elif isinstance(key, int):
             return BoundRow(self.table, self.table.data[key])
         else:
index edede86bfa0e280c9f58874e65f12f80b046c448..767e0413ccc383ad786a23402a46e02fb47c969a 100644 (file)
@@ -6,7 +6,8 @@ table.paleblue {
 }
 
 table.paleblue a:link,
-table.paleblue a:visited {
+table.paleblue a:visited,
+table.paleblue + ul.pagination > li > a {
     color: #5B80B2;
     text-decoration: none;
     font-weight: bold;
@@ -31,7 +32,7 @@ table.paleblue thead td:first-child {
 
 table.paleblue thead th,
 table.paleblue thead td {
-    background: #FCFCFC url(../img/nav-bg.gif) top left repeat-x;
+    background: #FCFCFC url(../img/header-bg.gif) top left repeat-x;
     border-bottom: 1px solid #DDD;
     padding: 2px 5px;
     font-size: 11px;
@@ -64,3 +65,24 @@ table.paleblue tr.odd {
 table.paleblue tr.even {
     background-color: white;
 }
+
+table.paleblue + ul.pagination {
+    background: white url(../img/pagination-bg.gif) left 180% repeat-x;
+    overflow: auto;
+    padding: 10px;
+    border: 1px solid #DDD;
+}
+
+table.paleblue + ul.pagination > li {
+    float: left;
+    line-height: 22px;
+    margin-left: 10px;
+}
+
+table.paleblue + ul.pagination > li:first-child {
+    margin-left: 0;
+}
+
+div.table-container {
+    display: inline-block;
+}
diff --git a/django_tables/static/django_tables/themes/paleblue/img/pagination-bg.gif b/django_tables/static/django_tables/themes/paleblue/img/pagination-bg.gif
new file mode 100644 (file)
index 0000000..f8402b8
Binary files /dev/null and b/django_tables/static/django_tables/themes/paleblue/img/pagination-bg.gif differ
index cba5d5f6dc900946e837263ee38043052bbf321d..0b3efa06fd35dc3fdaf52b218b7db76a402d642f 100644 (file)
@@ -27,18 +27,12 @@ class TableData(object):
         if isinstance(data, QuerySet):
             self.queryset = data
         elif isinstance(data, list):
-            self.list = data
+            self.list = data[:]
         else:
             raise ValueError('data must be a list or QuerySet object, not %s'
                              % data.__class__.__name__)
         self._table = table
 
-        # work with a copy of the data that has missing values populated with
-        # defaults.
-        if hasattr(self, 'list'):
-            self.list = copy.copy(self.list)
-            self._populate_missing_values(self.list)
-
     def __len__(self):
         # Use the queryset count() method to get the length, instead of
         # loading all results into memory. This allows, for example,
@@ -77,41 +71,6 @@ class TableData(object):
             translated.append(prefix + column.accessor)
         return OrderByTuple(translated)
 
-    def _populate_missing_values(self, data):
-        """
-        Populates self._data with missing values based on the default value
-        for each column. It will create new items in the dataset (not modify
-        existing ones).
-
-        """
-        for i, item in enumerate(data):
-            # add data that is missing from the source. we do this now
-            # so that the column's ``default`` values can affect
-            # sorting (even when callables are used)!
-            #
-            # This is a design decision - the alternative would be to
-            # resolve the values when they are accessed, and either do
-            # not support sorting them at all, or run the callables
-            # during sorting.
-            modified_item = None
-            for bound_column in self._table.columns.all():
-                # the following will be True if:
-                # * the source does not provide a value for the column
-                #   or the value is None
-                # * the column did provide a data callable that
-                #   returned None
-                accessor = Accessor(bound_column.accessor)
-                try:
-                    if accessor.resolve(item) is None:  # may raise ValueError
-                        raise ValueError('None values also need replacing')
-                except ValueError:
-                    if modified_item is None:
-                        modified_item = copy.copy(item)
-                    modified_item[accessor.bits[0]] = bound_column.default
-            if modified_item is not None:
-                data[i] = modified_item
-
-
     def __getitem__(self, index):
         return (self.list if hasattr(self, 'list') else self.queryset)[index]
 
@@ -230,8 +189,10 @@ class Table(StrAndUnicode):
 
     @order_by.setter
     def order_by(self, value):
-        """Order the rows of the table based columns. ``value`` must be a
-        sequence of column names.
+        """
+        Order the rows of the table based columns. ``value`` must be a sequence
+        of column names.
+
         """
         # accept both string and tuple instructions
         order_by = value.split(',') if isinstance(value, basestring) else value
@@ -271,13 +232,13 @@ class Table(StrAndUnicode):
         """The attributes that should be applied to the ``<table>`` tag when
         rendering HTML.
 
-        :returns: :class:`~.utils.AttributeDict` object.
+        :rtype: :class:`~.utils.AttributeDict` object.
 
         """
         return self._meta.attrs
 
-    def paginate(self, klass=Paginator, page=1, *args, **kwargs):
-        self.paginator = klass(self.rows, *args, **kwargs)
+    def paginate(self, klass=Paginator, per_page=25, page=1, *args, **kwargs):
+        self.paginator = klass(self.rows, per_page, *args, **kwargs)
         try:
             self.page = self.paginator.page(page)
         except Exception as e:
index 9ecf79ac4ede8dc74e42f77370f73c60ff2ad181..c6d12c0b1c9227e356ce04d50e4aa939cec45867 100644 (file)
@@ -1,5 +1,8 @@
 {% spaceless %}
 {% load django_tables %}
+{% if table.page %}
+<div class="table-container">
+{% endif %}
 <table{% if table.attrs %} {{ table.attrs.as_html }}{% endif %}>
     <thead>
         <tr class="{% cycle "odd" "even" %}">
             <th class="{% spaceless %}{% if column.sortable %}sortable {% endif %}{% if ob %}{% if ob.is_descending %}desc{% else %}asc{% endif %}{% endif %}{% endspaceless %}"><a href="{% if ob %}{% set_url_param sort=ob.opposite %}{% else %}{% set_url_param sort=column.name %}{% endif %}">{{ column.header }}</a></th>
             {% endwith %}
         {% else %}
-            <th>{{ column.verbose_name }}</th>
+            <th>{{ column.header }}</th>
         {% endif %}
         {% endfor %}
         </tr>
     </thead>
     <tbody>
-        {% for row in table.rows %}
+        {% for row in table.page.object_list|default:table.rows %} {# support pagination #}
         <tr class="{% cycle "odd" "even" %}">
             {% for cell in row %}
                 <td>{{ cell }}</td>
         {% endfor %}
     </tbody>
 </table>
+{% if table.page %}
+<ul class="pagination">
+    {% if table.page.has_previous %}
+    <li class="previous"><a href="{% set_url_param page=table.page.previous_page_number %}">Previous</a>
+    {% endif %}
+    <li class="current">Page {{ table.page.number }} of {{ table.paginator.num_pages }}</li>
+    {% if table.page.has_next %}
+    <li class="next"><a href="{% set_url_param page=table.page.next_page_number %}">Next</a>
+    {% endif %}
+    </span>
+</div>
+</div>
+{% endif %}
 {% endspaceless %}
index b914f4e4045592bb25aebf7de422cec50a723e40..3bb1f09c10d17a43376ba10ec7fc6164a4a97bb8 100644 (file)
@@ -89,11 +89,15 @@ class RenderTableNode(template.Node):
         self.table_var = template.Variable(table_var_name)
 
     def render(self, context):
-        context = template.Context({
-            'request': context.get('request', None),
-            'table': self.table_var.resolve(context)
-        })
-        return get_template('django_tables/table.html').render(context)
+        table = self.table_var.resolve(context)
+        request = context.get('request', None)
+        context = template.Context({'request': request, 'table': table})
+        try:
+            table.request = request
+            return get_template('django_tables/table.html').render(context)
+        finally:
+            pass
+            #del table.request
 
 
 @register.tag
index 36f0aabe393bdc51f67fd4eed4b6f722853561fa..67eef8ac761b4b30fb610aef058765bd36d476cf 100644 (file)
@@ -52,7 +52,7 @@ project = u'django-tables'
 # The short X.Y version.
 version = '0.4.0'
 # The full version, including alpha/beta/rc tags.
-release = '0.4.0.beta2'
+release = '0.4.0.beta3'
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
index 37f2c061407f3a02f9116f52b9a47758d885ff96..3e38c2646546163ee271bf2508d7485687aa92bc 100644 (file)
@@ -172,6 +172,26 @@ by modified.
     table.order_by = 'name,-population'  # equivalant
 
 
+.. _pagination:
+
+Pagination
+==========
+
+Pagination is easy, just call :meth:`.Table.paginate` and pass in the current
+page number, e.g.
+
+.. code-block:: python
+
+    def people_listing(request):
+        table = PeopleTable(Person.objects.all())
+        table.paginate(page=request.GET.get('page', 1))
+        return render_to_response('people_listing.html', {'table': table},
+                                  context_instance=RequestContext(request))
+
+The last set is to render the table. :meth:`.Table.as_html` doesn't support
+pagination, so you must use :ref:`{% render_table %}
+<template-tags.render_table>`.
+
 .. _custom-rendering:
 
 Custom rendering
@@ -371,6 +391,11 @@ Sample usage:
     {% load django_tables %}
     {% render_table table %}
 
+This tag temporarily modifies the :class:`.Table` object while it is being
+rendered. It adds a ``request`` attribute to the table, which allows
+:class:`Column` objects to have access to a ``RequestContext``. See
+:class:`.TemplateColumn` for an example.
+
 
 .. _template-tags.set_url_param:
 
@@ -531,14 +556,14 @@ API Reference
 --------------------------
 
 .. autoclass:: django_tables.rows.BoundRows
-    :members: __init__, all, page, __iter__, __len__, count
+    :members: all, page, __iter__, __len__, count
 
 
 :class:`BoundRow` Objects
 -------------------------
 
 .. autoclass:: django_tables.rows.BoundRow
-    :members: __init__, __getitem__, __contains__, __iter__, record, table
+    :members: __getitem__, __contains__, __iter__, record, table
 
 
 :class:`AttributeDict` Objects
index 0f96d61d659bc4bb85cae9390b661f349fe73c12..1430d1586735859521d33ff97ebcdd6b718b753a 100755 (executable)
--- a/setup.py
+++ b/setup.py
@@ -4,7 +4,7 @@ from setuptools import setup, find_packages
 
 setup(
     name='django-tables',
-    version='0.4.0.beta2',
+    version='0.4.0.beta3',
     description='Table framework for Django',
 
     author='Bradley Ayers',
index e37dee8ab3e176689a4b6a829a8f0ab2a10a2e02..8341bdb3621fce5cec87d96fac5fe9b2fdf80724 100644 (file)
@@ -174,7 +174,6 @@ def pagination():
     paginator = Paginator(books.rows, 10)
     assert paginator.num_pages == 10
     page = paginator.page(1)
-    assert len(page.object_list) == 10
     assert page.has_previous() == False
     assert page.has_next() == True