Fixed #25513 -- Extracted admin pagination to Paginator.get_elided_page_range().
This commit is contained in:
parent
f35840c196
commit
0a306f7da6
|
@ -24,16 +24,14 @@ from .base import InclusionAdminNode
|
|||
|
||||
register = Library()
|
||||
|
||||
DOT = '.'
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def paginator_number(cl, i):
|
||||
"""
|
||||
Generate an individual page index link in a paginated list.
|
||||
"""
|
||||
if i == DOT:
|
||||
return '… '
|
||||
if i == cl.paginator.ELLIPSIS:
|
||||
return format_html('{} ', cl.paginator.ELLIPSIS)
|
||||
elif i == cl.page_num:
|
||||
return format_html('<span class="this-page">{}</span> ', i)
|
||||
else:
|
||||
|
@ -49,39 +47,8 @@ def pagination(cl):
|
|||
"""
|
||||
Generate the series of links to the pages in a paginated list.
|
||||
"""
|
||||
paginator, page_num = cl.paginator, cl.page_num
|
||||
|
||||
pagination_required = (not cl.show_all or not cl.can_show_all) and cl.multi_page
|
||||
if not pagination_required:
|
||||
page_range = []
|
||||
else:
|
||||
ON_EACH_SIDE = 3
|
||||
ON_ENDS = 2
|
||||
|
||||
# If there are 10 or fewer pages, display links to every page.
|
||||
# Otherwise, do some fancy
|
||||
if paginator.num_pages <= 10:
|
||||
page_range = range(1, paginator.num_pages + 1)
|
||||
else:
|
||||
# Insert "smart" pagination links, so that there are always ON_ENDS
|
||||
# links at either end of the list of pages, and there are always
|
||||
# ON_EACH_SIDE links at either end of the "current page" link.
|
||||
page_range = []
|
||||
if page_num > (1 + ON_EACH_SIDE + ON_ENDS) + 1:
|
||||
page_range += [
|
||||
*range(1, ON_ENDS + 1), DOT,
|
||||
*range(page_num - ON_EACH_SIDE, page_num + 1),
|
||||
]
|
||||
else:
|
||||
page_range.extend(range(1, page_num + 1))
|
||||
if page_num < (paginator.num_pages - ON_EACH_SIDE - ON_ENDS) - 1:
|
||||
page_range += [
|
||||
*range(page_num + 1, page_num + ON_EACH_SIDE + 1), DOT,
|
||||
*range(paginator.num_pages - ON_ENDS + 1, paginator.num_pages + 1)
|
||||
]
|
||||
else:
|
||||
page_range.extend(range(page_num + 1, paginator.num_pages + 1))
|
||||
|
||||
page_range = cl.paginator.get_elided_page_range(cl.page_num) if pagination_required else []
|
||||
need_show_all_link = cl.can_show_all and not cl.show_all and cl.multi_page
|
||||
return {
|
||||
'cl': cl,
|
||||
|
|
|
@ -25,6 +25,9 @@ class EmptyPage(InvalidPage):
|
|||
|
||||
|
||||
class Paginator:
|
||||
# Translators: String used to replace omitted page numbers in elided page
|
||||
# range generated by paginators, e.g. [1, 2, '…', 5, 6, 7, '…', 9, 10].
|
||||
ELLIPSIS = _('…')
|
||||
|
||||
def __init__(self, object_list, per_page, orphans=0,
|
||||
allow_empty_first_page=True):
|
||||
|
@ -128,6 +131,37 @@ class Paginator:
|
|||
stacklevel=3
|
||||
)
|
||||
|
||||
def get_elided_page_range(self, number=1, *, on_each_side=3, on_ends=2):
|
||||
"""
|
||||
Return a 1-based range of pages with some values elided.
|
||||
|
||||
If the page range is larger than a given size, the whole range is not
|
||||
provided and a compact form is returned instead, e.g. for a paginator
|
||||
with 50 pages, if page 43 were the current page, the output, with the
|
||||
default arguments, would be:
|
||||
|
||||
1, 2, …, 40, 41, 42, 43, 44, 45, 46, …, 49, 50.
|
||||
"""
|
||||
number = self.validate_number(number)
|
||||
|
||||
if self.num_pages <= (on_each_side + on_ends) * 2:
|
||||
yield from self.page_range
|
||||
return
|
||||
|
||||
if number > (1 + on_each_side + on_ends) + 1:
|
||||
yield from range(1, on_ends + 1)
|
||||
yield self.ELLIPSIS
|
||||
yield from range(number - on_each_side, number + 1)
|
||||
else:
|
||||
yield from range(1, number + 1)
|
||||
|
||||
if number < (self.num_pages - on_each_side - on_ends) - 1:
|
||||
yield from range(number + 1, number + on_each_side + 1)
|
||||
yield self.ELLIPSIS
|
||||
yield from range(self.num_pages - on_ends + 1, self.num_pages + 1)
|
||||
else:
|
||||
yield from range(number + 1, self.num_pages + 1)
|
||||
|
||||
|
||||
class Page(collections.abc.Sequence):
|
||||
|
||||
|
|
|
@ -78,9 +78,40 @@ Methods
|
|||
Returns a :class:`Page` object with the given 1-based index. Raises
|
||||
:exc:`InvalidPage` if the given page number doesn't exist.
|
||||
|
||||
.. method:: Paginator.get_elided_page_range(number, *, on_each_side=3, on_ends=2)
|
||||
|
||||
.. versionadded:: 3.2
|
||||
|
||||
Returns a 1-based list of page numbers similar to
|
||||
:attr:`Paginator.page_range`, but may add an ellipsis to either or both
|
||||
sides of the current page number when :attr:`Paginator.num_pages` is large.
|
||||
|
||||
The number of pages to include on each side of the current page number is
|
||||
determined by the ``on_each_side`` argument which defaults to 3.
|
||||
|
||||
The number of pages to include at the beginning and end of page range is
|
||||
determined by the ``on_ends`` argument which defaults to 2.
|
||||
|
||||
For example, with the default values for ``on_each_side`` and ``on_ends``,
|
||||
if the current page is 10 and there are 50 pages, the page range will be
|
||||
``[1, 2, '…', 7, 8, 9, 10, 11, 12, 13, '…', 49, 50]``. This will result in
|
||||
pages 4, 5, and 6 to the left of and 8, 9, and 10 to the right of the
|
||||
current page as well as pages 1 and 2 at the start and 49 and 50 at the
|
||||
end.
|
||||
|
||||
Raises :exc:`InvalidPage` if the given page number doesn't exist.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
|
||||
.. attribute:: Paginator.ELLIPSIS
|
||||
|
||||
.. versionadded:: 3.2
|
||||
|
||||
A translatable string used as a substitute for elided page numbers in the
|
||||
page range returned by :meth:`~Paginator.get_elided_page_range`. Default is
|
||||
``'…'``.
|
||||
|
||||
.. attribute:: Paginator.count
|
||||
|
||||
The total number of objects, across all pages.
|
||||
|
|
|
@ -273,6 +273,14 @@ Models
|
|||
expressions that don't need to be selected but are used for filtering,
|
||||
ordering, or as a part of complex expressions.
|
||||
|
||||
Pagination
|
||||
~~~~~~~~~~
|
||||
|
||||
* The new :meth:`django.core.paginator.Paginator.get_elided_page_range` method
|
||||
allows generating a page range with some of the values elided. If there are a
|
||||
large number of pages, this can be helpful for generating a reasonable number
|
||||
of page links in a template.
|
||||
|
||||
Requests and Responses
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
@ -1242,32 +1242,32 @@ class ChangeListTests(TestCase):
|
|||
request = self.factory.get('/group/')
|
||||
request.user = self.superuser
|
||||
cl = m.get_changelist_instance(request)
|
||||
per_page = cl.list_per_page = 10
|
||||
cl.list_per_page = 10
|
||||
|
||||
for page_num, objects_count, expected_page_range in [
|
||||
(1, per_page, []),
|
||||
(1, per_page * 2, [1, 2]),
|
||||
(6, per_page * 11, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]),
|
||||
(6, per_page * 12, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]),
|
||||
(6, per_page * 13, [1, 2, 3, 4, 5, 6, 7, 8, 9, '.', 12, 13]),
|
||||
(7, per_page * 12, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]),
|
||||
(7, per_page * 13, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]),
|
||||
(7, per_page * 14, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, '.', 13, 14]),
|
||||
(8, per_page * 13, [1, 2, '.', 5, 6, 7, 8, 9, 10, 11, 12, 13]),
|
||||
(8, per_page * 14, [1, 2, '.', 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]),
|
||||
(8, per_page * 15, [1, 2, '.', 5, 6, 7, 8, 9, 10, 11, '.', 14, 15]),
|
||||
ELLIPSIS = cl.paginator.ELLIPSIS
|
||||
for number, pages, expected in [
|
||||
(1, 1, []),
|
||||
(1, 2, [1, 2]),
|
||||
(6, 11, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]),
|
||||
(6, 12, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]),
|
||||
(6, 13, [1, 2, 3, 4, 5, 6, 7, 8, 9, ELLIPSIS, 12, 13]),
|
||||
(7, 12, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]),
|
||||
(7, 13, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]),
|
||||
(7, 14, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ELLIPSIS, 13, 14]),
|
||||
(8, 13, [1, 2, ELLIPSIS, 5, 6, 7, 8, 9, 10, 11, 12, 13]),
|
||||
(8, 14, [1, 2, ELLIPSIS, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]),
|
||||
(8, 15, [1, 2, ELLIPSIS, 5, 6, 7, 8, 9, 10, 11, ELLIPSIS, 14, 15]),
|
||||
]:
|
||||
with self.subTest(number=number, pages=pages):
|
||||
# assuming exactly `objects_count` objects
|
||||
# assuming exactly `pages * cl.list_per_page` objects
|
||||
Group.objects.all().delete()
|
||||
for i in range(objects_count):
|
||||
for i in range(pages * cl.list_per_page):
|
||||
Group.objects.create(name='test band')
|
||||
|
||||
# setting page number and calculating page range
|
||||
cl.page_num = page_num
|
||||
cl.page_num = number
|
||||
cl.get_results(request)
|
||||
real_page_range = pagination(cl)['page_range']
|
||||
self.assertEqual(expected_page_range, list(real_page_range))
|
||||
self.assertEqual(list(pagination(cl)['page_range']), expected)
|
||||
|
||||
def test_object_tools_displayed_no_add_permission(self):
|
||||
"""
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import collections.abc
|
||||
import unittest.mock
|
||||
import warnings
|
||||
from datetime import datetime
|
||||
|
||||
|
@ -304,6 +306,125 @@ class PaginationTests(SimpleTestCase):
|
|||
with self.subTest(page=page):
|
||||
self.assertEqual(expected, list(next(page_iterator)))
|
||||
|
||||
def test_get_elided_page_range(self):
|
||||
# Paginator.validate_number() must be called:
|
||||
paginator = Paginator([1, 2, 3], 2)
|
||||
with unittest.mock.patch.object(paginator, 'validate_number') as mock:
|
||||
mock.assert_not_called()
|
||||
list(paginator.get_elided_page_range(2))
|
||||
mock.assert_called_with(2)
|
||||
|
||||
ELLIPSIS = Paginator.ELLIPSIS
|
||||
|
||||
# Range is not elided if not enough pages when using default arguments:
|
||||
paginator = Paginator(range(10 * 100), 100)
|
||||
page_range = paginator.get_elided_page_range(1)
|
||||
self.assertIsInstance(page_range, collections.abc.Generator)
|
||||
self.assertNotIn(ELLIPSIS, page_range)
|
||||
paginator = Paginator(range(10 * 100 + 1), 100)
|
||||
self.assertIsInstance(page_range, collections.abc.Generator)
|
||||
page_range = paginator.get_elided_page_range(1)
|
||||
self.assertIn(ELLIPSIS, page_range)
|
||||
|
||||
# Range should be elided if enough pages when using default arguments:
|
||||
tests = [
|
||||
# on_each_side=3, on_ends=2
|
||||
(1, [1, 2, 3, 4, ELLIPSIS, 49, 50]),
|
||||
(6, [1, 2, 3, 4, 5, 6, 7, 8, 9, ELLIPSIS, 49, 50]),
|
||||
(7, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ELLIPSIS, 49, 50]),
|
||||
(8, [1, 2, ELLIPSIS, 5, 6, 7, 8, 9, 10, 11, ELLIPSIS, 49, 50]),
|
||||
(43, [1, 2, ELLIPSIS, 40, 41, 42, 43, 44, 45, 46, ELLIPSIS, 49, 50]),
|
||||
(44, [1, 2, ELLIPSIS, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50]),
|
||||
(45, [1, 2, ELLIPSIS, 42, 43, 44, 45, 46, 47, 48, 49, 50]),
|
||||
(50, [1, 2, ELLIPSIS, 47, 48, 49, 50]),
|
||||
]
|
||||
paginator = Paginator(range(5000), 100)
|
||||
for number, expected in tests:
|
||||
with self.subTest(number=number):
|
||||
page_range = paginator.get_elided_page_range(number)
|
||||
self.assertIsInstance(page_range, collections.abc.Generator)
|
||||
self.assertEqual(list(page_range), expected)
|
||||
|
||||
# Range is not elided if not enough pages when using custom arguments:
|
||||
tests = [
|
||||
(6, 2, 1, 1), (8, 1, 3, 1), (8, 4, 0, 1), (4, 1, 1, 1),
|
||||
# When on_each_side and on_ends are both <= 1 but not both == 1 it
|
||||
# is a special case where the range is not elided until an extra
|
||||
# page is added.
|
||||
(2, 0, 1, 2), (2, 1, 0, 2), (1, 0, 0, 2),
|
||||
]
|
||||
for pages, on_each_side, on_ends, elided_after in tests:
|
||||
for offset in range(elided_after + 1):
|
||||
with self.subTest(pages=pages, offset=elided_after, on_each_side=on_each_side, on_ends=on_ends):
|
||||
paginator = Paginator(range((pages + offset) * 100), 100)
|
||||
page_range = paginator.get_elided_page_range(
|
||||
1,
|
||||
on_each_side=on_each_side,
|
||||
on_ends=on_ends,
|
||||
)
|
||||
self.assertIsInstance(page_range, collections.abc.Generator)
|
||||
if offset < elided_after:
|
||||
self.assertNotIn(ELLIPSIS, page_range)
|
||||
else:
|
||||
self.assertIn(ELLIPSIS, page_range)
|
||||
|
||||
# Range should be elided if enough pages when using custom arguments:
|
||||
tests = [
|
||||
# on_each_side=2, on_ends=1
|
||||
(1, 2, 1, [1, 2, 3, ELLIPSIS, 50]),
|
||||
(4, 2, 1, [1, 2, 3, 4, 5, 6, ELLIPSIS, 50]),
|
||||
(5, 2, 1, [1, 2, 3, 4, 5, 6, 7, ELLIPSIS, 50]),
|
||||
(6, 2, 1, [1, ELLIPSIS, 4, 5, 6, 7, 8, ELLIPSIS, 50]),
|
||||
(45, 2, 1, [1, ELLIPSIS, 43, 44, 45, 46, 47, ELLIPSIS, 50]),
|
||||
(46, 2, 1, [1, ELLIPSIS, 44, 45, 46, 47, 48, 49, 50]),
|
||||
(47, 2, 1, [1, ELLIPSIS, 45, 46, 47, 48, 49, 50]),
|
||||
(50, 2, 1, [1, ELLIPSIS, 48, 49, 50]),
|
||||
# on_each_side=1, on_ends=3
|
||||
(1, 1, 3, [1, 2, ELLIPSIS, 48, 49, 50]),
|
||||
(5, 1, 3, [1, 2, 3, 4, 5, 6, ELLIPSIS, 48, 49, 50]),
|
||||
(6, 1, 3, [1, 2, 3, 4, 5, 6, 7, ELLIPSIS, 48, 49, 50]),
|
||||
(7, 1, 3, [1, 2, 3, ELLIPSIS, 6, 7, 8, ELLIPSIS, 48, 49, 50]),
|
||||
(44, 1, 3, [1, 2, 3, ELLIPSIS, 43, 44, 45, ELLIPSIS, 48, 49, 50]),
|
||||
(45, 1, 3, [1, 2, 3, ELLIPSIS, 44, 45, 46, 47, 48, 49, 50]),
|
||||
(46, 1, 3, [1, 2, 3, ELLIPSIS, 45, 46, 47, 48, 49, 50]),
|
||||
(50, 1, 3, [1, 2, 3, ELLIPSIS, 49, 50]),
|
||||
# on_each_side=4, on_ends=0
|
||||
(1, 4, 0, [1, 2, 3, 4, 5, ELLIPSIS]),
|
||||
(5, 4, 0, [1, 2, 3, 4, 5, 6, 7, 8, 9, ELLIPSIS]),
|
||||
(6, 4, 0, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ELLIPSIS]),
|
||||
(7, 4, 0, [ELLIPSIS, 3, 4, 5, 6, 7, 8, 9, 10, 11, ELLIPSIS]),
|
||||
(44, 4, 0, [ELLIPSIS, 40, 41, 42, 43, 44, 45, 46, 47, 48, ELLIPSIS]),
|
||||
(45, 4, 0, [ELLIPSIS, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50]),
|
||||
(46, 4, 0, [ELLIPSIS, 42, 43, 44, 45, 46, 47, 48, 49, 50]),
|
||||
(50, 4, 0, [ELLIPSIS, 46, 47, 48, 49, 50]),
|
||||
# on_each_side=0, on_ends=1
|
||||
(1, 0, 1, [1, ELLIPSIS, 50]),
|
||||
(2, 0, 1, [1, 2, ELLIPSIS, 50]),
|
||||
(3, 0, 1, [1, 2, 3, ELLIPSIS, 50]),
|
||||
(4, 0, 1, [1, ELLIPSIS, 4, ELLIPSIS, 50]),
|
||||
(47, 0, 1, [1, ELLIPSIS, 47, ELLIPSIS, 50]),
|
||||
(48, 0, 1, [1, ELLIPSIS, 48, 49, 50]),
|
||||
(49, 0, 1, [1, ELLIPSIS, 49, 50]),
|
||||
(50, 0, 1, [1, ELLIPSIS, 50]),
|
||||
# on_each_side=0, on_ends=0
|
||||
(1, 0, 0, [1, ELLIPSIS]),
|
||||
(2, 0, 0, [1, 2, ELLIPSIS]),
|
||||
(3, 0, 0, [ELLIPSIS, 3, ELLIPSIS]),
|
||||
(48, 0, 0, [ELLIPSIS, 48, ELLIPSIS]),
|
||||
(49, 0, 0, [ELLIPSIS, 49, 50]),
|
||||
(50, 0, 0, [ELLIPSIS, 50]),
|
||||
]
|
||||
paginator = Paginator(range(5000), 100)
|
||||
for number, on_each_side, on_ends, expected in tests:
|
||||
with self.subTest(number=number, on_each_side=on_each_side, on_ends=on_ends):
|
||||
page_range = paginator.get_elided_page_range(
|
||||
number,
|
||||
on_each_side=on_each_side,
|
||||
on_ends=on_ends,
|
||||
)
|
||||
self.assertIsInstance(page_range, collections.abc.Generator)
|
||||
self.assertEqual(list(page_range), expected)
|
||||
|
||||
|
||||
class ModelPaginationTests(TestCase):
|
||||
"""
|
||||
|
|
Loading…
Reference in New Issue