Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lazy paginator #604

Merged
merged 6 commits into from
Aug 24, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ You can also run the tests only in your current environment, using

To generate a html coverage report:
```
covarage run --source=django_tables2 manage.py test -s
cov
coverage run --source=django_tables2 manage.py test
coverage html
```

## Building the documentation
Expand Down
2 changes: 2 additions & 0 deletions django_tables2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
)
from .config import RequestConfig
from .utils import A
from .paginators import LazyPaginator
from .views import SingleTableMixin, SingleTableView, MultiTableMixin


Expand Down Expand Up @@ -46,4 +47,5 @@
"SingleTableMixin",
"SingleTableView",
"MultiTableMixin",
"LazyPaginator",
)
70 changes: 70 additions & 0 deletions django_tables2/paginators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
from __future__ import unicode_literals

from django.core.paginator import EmptyPage, Page, PageNotAnInteger, Paginator
from django.utils.translation import ugettext as _


class LazyPaginator(Paginator):
"""
Implement lazy pagination, preventing any count() queries.

Usage with SingleTableView::

class UserListView(SingleTableView):
table_class = UserTable
table_data = User.objects.all()
table_pagination = {
"klass": LazyPaginator
}
"""

def __init__(self, object_list, per_page, **kwargs):
self._num_pages = None

super(LazyPaginator, self).__init__(object_list, per_page, **kwargs)

def validate_number(self, number):
"""Validate the given 1-based page number."""
try:
if isinstance(number, float) and not number.is_integer():
raise ValueError
number = int(number)
except (TypeError, ValueError):
raise PageNotAnInteger(_("That page number is not an integer"))
if number < 1:
raise EmptyPage(_("That page number is less than 1"))
return number

def page(self, number):
number = self.validate_number(number)
bottom = (number - 1) * self.per_page
top = bottom + self.per_page
# Retrieve more objects to check if there is a next page.
objects = list(self.object_list[bottom : top + self.orphans + 1])
objects_count = len(objects)
if objects_count > (self.per_page + self.orphans):
# If another page is found, increase the total number of pages.
self._num_pages = number + 1
# In any case, return only objects for this page.
objects = objects[: self.per_page]
elif (number != 1) and (objects_count <= self.orphans):
raise EmptyPage(_("That page contains no results"))
else:
# This is the last page.
self._num_pages = number
return Page(objects, number, self)

def _get_count(self):
raise NotImplementedError

count = property(_get_count)

def _get_num_pages(self):
return self._num_pages

num_pages = property(_get_num_pages)

def _get_page_range(self):
raise NotImplementedError

page_range = property(_get_page_range)
2 changes: 1 addition & 1 deletion django_tables2/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class SingleTableMixin(TableMixinBase):
`~.tables.Table.paginate`.

If you want to use a non-standard paginator for example, you can add a key
`klass` to the dict, containing a custom `Pagintor` class.
`klass` to the dict, containing a custom `Paginator` class.

This mixin plays nice with the Django's ``.MultipleObjectMixin`` by using
``.get_queryset`` as a fall back for the table data source.
Expand Down
3 changes: 2 additions & 1 deletion example/app/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from django_filters.views import FilterView
from django_tables2 import MultiTableMixin, RequestConfig, SingleTableMixin, SingleTableView
from django_tables2.paginators import LazyPaginator

from .data import COUNTRIES
from .filters import PersonFilter
Expand Down Expand Up @@ -109,7 +110,7 @@ def bootstrap(request):

create_fake_data()
table = BootstrapTable(Person.objects.all().select_related("country"), order_by="-name")
RequestConfig(request, paginate={"per_page": 10}).configure(table)
RequestConfig(request, paginate={"klass": LazyPaginator, "per_page": 10}).configure(table)

return render(request, "bootstrap_template.html", {"table": table})

Expand Down
2 changes: 1 addition & 1 deletion tests/test_ordering.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from django.utils import six

import django_tables2 as tables
from django_tables2.tables import RequestConfig
from django_tables2 import RequestConfig

from .app.models import Person
from .utils import build_request
Expand Down
63 changes: 63 additions & 0 deletions tests/test_paginators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from __future__ import absolute_import, unicode_literals

from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.test import TestCase

from django_tables2 import LazyPaginator


class FakeQuerySet:
objects = range(1, 10 ** 6)

def count(self):
raise AssertionError("LazyPaginator should not call QuerySet.count()")

def __getitem__(self, key):
return self.objects[key]

def __iter__(self):
yield next(self.objects)


class LazyPaginatorTest(TestCase):
def test_compare_to_default_paginator(self):
objects = list(range(1, 1000))

paginator = Paginator(objects, 10)
lazy_paginator = LazyPaginator(objects, 10)
self.assertEqual(paginator.page(1).object_list, lazy_paginator.page(1).object_list)
self.assertEqual(paginator.page(10).object_list, lazy_paginator.page(10).object_list)
self.assertEqual(paginator.page(100).object_list, lazy_paginator.page(100).object_list)

def test_no_count_call(self):
paginator = LazyPaginator(FakeQuerySet(), 10)
# num_pages initially is None, but is page_number + 1 after requesting a page.
self.assertEqual(paginator.num_pages, None)

paginator.page(1)
self.assertEqual(paginator.num_pages, 2)
paginator.page(3)
self.assertEqual(paginator.num_pages, 4)

paginator.page(1.0)
# and again decreases when a lower page nu
self.assertEqual(paginator.num_pages, 2)

with self.assertRaises(PageNotAnInteger):
paginator.page(1.5)

with self.assertRaises(EmptyPage):
paginator.page(-1)

with self.assertRaises(NotImplementedError):
paginator.count()

with self.assertRaises(NotImplementedError):
paginator.page_range()

# last page
last_page_number = 10 ** 5
paginator.page(last_page_number)

with self.assertRaises(EmptyPage):
paginator.page(last_page_number + 1)
46 changes: 25 additions & 21 deletions tests/test_templatetags.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
from __future__ import unicode_literals

from django.core.exceptions import ImproperlyConfigured
from django.core.paginator import Paginator
from django.template import Context, RequestContext, Template, TemplateSyntaxError
from django.test import SimpleTestCase, TestCase, override_settings
from django.utils import six
from django.utils.six.moves.urllib.parse import parse_qs

from django_tables2 import RequestConfig, Table, TemplateColumn
from django_tables2 import LazyPaginator, RequestConfig, Table, TemplateColumn
from django_tables2.templatetags.django_tables2 import table_page_range
from django_tables2.utils import AttributeDict

from .app.models import Region
Expand Down Expand Up @@ -242,23 +244,25 @@ def test_render_attributes_test(self):
self.assertEqual(html, 'class="table table-striped"')


class TitleTagTest(SimpleTestCase):
def test_should_only_apply_to_words_without_uppercase_letters(self):
expectations = {
"a brown fox": "A Brown Fox",
"a brown foX": "A Brown foX",
"black FBI": "Black FBI",
"f.b.i": "F.B.I",
"start 6pm": "Start 6pm",
# Some cyrillic samples
"руда лисиця": "Руда Лисиця",
"руда лисицЯ": "Руда лисицЯ",
"діяльність СБУ": "Діяльність СБУ",
"а.б.в": "А.Б.В",
"вага 6кг": "Вага 6кг",
"у 80-их роках": "У 80-их Роках",
}

for raw, expected in expectations.items():
template = Template("{% load django_tables2 %}{{ x|title }}")
assert template.render(Context({"x": raw})), expected
class TablePageRangeTest(SimpleTestCase):
def test_table_page_range(self):
paginator = Paginator(range(1, 1000), 10)
self.assertEqual(
table_page_range(paginator.page(1), paginator), [1, 2, 3, 4, 5, 6, 7, 8, "...", 100]
)
self.assertEqual(
table_page_range(paginator.page(10), paginator),
[1, "...", 7, 8, 9, 10, 11, 12, "...", 100],
)
self.assertEqual(
table_page_range(paginator.page(100), paginator),
[1, "...", 93, 94, 95, 96, 97, 98, 99, 100],
)

def test_table_page_range_lazy(self):
paginator = LazyPaginator(range(1, 1000), 10)

self.assertEqual(table_page_range(paginator.page(1), paginator), range(1, 3))
self.assertEqual(
table_page_range(paginator.page(10), paginator), [1, "...", 4, 5, 6, 7, 8, 9, 10, 11]
)
2 changes: 1 addition & 1 deletion tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def build_request(uri="/", user=None):
"""
Return a fresh HTTP GET / request.

This is essentially a heavily cutdown version of Django 1.3's
This is essentially a heavily cutdown version of Django's
`~django.test.client.RequestFactory`.
"""
path, _, querystring = uri.partition("?")
Expand Down
5 changes: 3 additions & 2 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ envlist =
docs,
flake8,
isort,
potypo
potypo,
black

[travis]
python:
Expand Down Expand Up @@ -50,7 +51,6 @@ commands =
deps =
-r{toxinidir}/docs/requirements.txt


[testenv:flake8]
basepython = python3.6
deps = flake8
Expand All @@ -65,6 +65,7 @@ max-line-length = 120
basepython = python3.6
deps = black
commands = black --check

[testenv:isort]
basepython = python3.6
deps = isort==4.2.15
Expand Down