pagination.py
1059 lines
| 38.3 KiB
| text/x-python
|
PythonLexer
r5608 | # Copyright (C) 2007-2012 Christoph Haas <email@christoph-haas.de> | |||
r4091 | # NOTE: MIT license based code, backported and edited by RhodeCode GmbH | |||
""" | ||||
paginate: helps split up large collections into individual pages | ||||
================================================================ | ||||
What is pagination? | ||||
--------------------- | ||||
This module helps split large lists of items into pages. The user is shown one page at a time and | ||||
can navigate to other pages. Imagine you are offering a company phonebook and let the user search | ||||
the entries. The entire search result may contains 23 entries but you want to display no more than | ||||
10 entries at once. The first page contains entries 1-10, the second 11-20 and the third 21-23. | ||||
Each "Page" instance represents the items of one of these three pages. | ||||
See the documentation of the "Page" class for more information. | ||||
How do I use it? | ||||
------------------ | ||||
A page of items is represented by the *Page* object. A *Page* gets initialized with these arguments: | ||||
- The collection of items to pick a range from. Usually just a list. | ||||
- The page number you want to display. Default is 1: the first page. | ||||
Now we can make up a collection and create a Page instance of it:: | ||||
# Create a sample collection of 1000 items | ||||
>> my_collection = range(1000) | ||||
# Create a Page object for the 3rd page (20 items per page is the default) | ||||
>> my_page = Page(my_collection, page=3) | ||||
# The page object can be printed as a string to get its details | ||||
>> str(my_page) | ||||
Page: | ||||
Collection type: <type 'range'> | ||||
Current page: 3 | ||||
First item: 41 | ||||
Last item: 60 | ||||
First page: 1 | ||||
Last page: 50 | ||||
Previous page: 2 | ||||
Next page: 4 | ||||
Items per page: 20 | ||||
Number of items: 1000 | ||||
Number of pages: 50 | ||||
# Print a list of items on the current page | ||||
>> my_page.items | ||||
[40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59] | ||||
# The *Page* object can be used as an iterator: | ||||
>> for my_item in my_page: print(my_item) | ||||
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | ||||
# The .pager() method returns an HTML fragment with links to surrounding pages. | ||||
>> my_page.pager(url="http://example.org/foo/page=$page") | ||||
<a href="http://example.org/foo/page=1">1</a> | ||||
<a href="http://example.org/foo/page=2">2</a> | ||||
3 | ||||
<a href="http://example.org/foo/page=4">4</a> | ||||
<a href="http://example.org/foo/page=5">5</a> | ||||
.. | ||||
<a href="http://example.org/foo/page=50">50</a>' | ||||
# Without the HTML it would just look like: | ||||
# 1 2 [3] 4 5 .. 50 | ||||
# The pager can be customized: | ||||
>> my_page.pager('$link_previous ~3~ $link_next (Page $page of $page_count)', | ||||
url="http://example.org/foo/page=$page") | ||||
<a href="http://example.org/foo/page=2"><</a> | ||||
<a href="http://example.org/foo/page=1">1</a> | ||||
<a href="http://example.org/foo/page=2">2</a> | ||||
3 | ||||
<a href="http://example.org/foo/page=4">4</a> | ||||
<a href="http://example.org/foo/page=5">5</a> | ||||
<a href="http://example.org/foo/page=6">6</a> | ||||
.. | ||||
<a href="http://example.org/foo/page=50">50</a> | ||||
<a href="http://example.org/foo/page=4">></a> | ||||
(Page 3 of 50) | ||||
# Without the HTML it would just look like: | ||||
# 1 2 [3] 4 5 6 .. 50 > (Page 3 of 50) | ||||
# The url argument to the pager method can be omitted when an url_maker is | ||||
# given during instantiation: | ||||
>> my_page = Page(my_collection, page=3, | ||||
url_maker=lambda p: "http://example.org/%s" % p) | ||||
>> page.pager() | ||||
There are some interesting parameters that customize the Page's behavior. See the documentation on | ||||
``Page`` and ``Page.pager()``. | ||||
Notes | ||||
------- | ||||
Page numbers and item numbers start at 1. This concept has been used because users expect that the | ||||
first page has number 1 and the first item on a page also has number 1. So if you want to use the | ||||
page's items by their index number please note that you have to subtract 1. | ||||
""" | ||||
import re | ||||
from string import Template | ||||
from webhelpers2.html import literal | ||||
def make_html_tag(tag, text=None, **params): | ||||
"""Create an HTML tag string. | ||||
tag | ||||
The HTML tag to use (e.g. 'a', 'span' or 'div') | ||||
text | ||||
The text to enclose between opening and closing tag. If no text is specified then only | ||||
the opening tag is returned. | ||||
Example:: | ||||
make_html_tag('a', text="Hello", href="/another/page") | ||||
-> <a href="/another/page">Hello</a> | ||||
To use reserved Python keywords like "class" as a parameter prepend it with | ||||
an underscore. Instead of "class='green'" use "_class='green'". | ||||
Warning: Quotes and apostrophes are not escaped.""" | ||||
params_string = "" | ||||
# Parameters are passed. Turn the dict into a string like "a=1 b=2 c=3" string. | ||||
for key, value in sorted(params.items()): | ||||
# Strip off a leading underscore from the attribute's key to allow attributes like '_class' | ||||
# to be used as a CSS class specification instead of the reserved Python keyword 'class'. | ||||
key = key.lstrip("_") | ||||
r4973 | params_string += ' {0}="{1}"'.format(key, value) | |||
r4091 | ||||
# Create the tag string | ||||
r4973 | tag_string = "<{0}{1}>".format(tag, params_string) | |||
r4091 | ||||
# Add text and closing tag if required. | ||||
if text: | ||||
r4973 | tag_string += "{0}</{1}>".format(text, tag) | |||
r4091 | ||||
return tag_string | ||||
# Since the items on a page are mainly a list we subclass the "list" type | ||||
class _Page(list): | ||||
"""A list/iterator representing the items on one page of a larger collection. | ||||
An instance of the "Page" class is created from a _collection_ which is any | ||||
list-like object that allows random access to its elements. | ||||
The instance works as an iterator running from the first item to the last item on the given | ||||
page. The Page.pager() method creates a link list allowing the user to go to other pages. | ||||
A "Page" does not only carry the items on a certain page. It gives you additional information | ||||
about the page in these "Page" object attributes: | ||||
item_count | ||||
Number of items in the collection | ||||
**WARNING:** Unless you pass in an item_count, a count will be | ||||
performed on the collection every time a Page instance is created. | ||||
page | ||||
Number of the current page | ||||
items_per_page | ||||
Maximal number of items displayed on a page | ||||
first_page | ||||
Number of the first page - usually 1 :) | ||||
last_page | ||||
Number of the last page | ||||
previous_page | ||||
Number of the previous page. If this is the first page it returns None. | ||||
next_page | ||||
Number of the next page. If this is the last page it returns None. | ||||
page_count | ||||
Number of pages | ||||
items | ||||
Sequence/iterator of items on the current page | ||||
first_item | ||||
Index of first item on the current page - starts with 1 | ||||
last_item | ||||
Index of last item on the current page | ||||
""" | ||||
def __init__( | ||||
self, | ||||
collection, | ||||
page=1, | ||||
items_per_page=20, | ||||
item_count=None, | ||||
wrapper_class=None, | ||||
url_maker=None, | ||||
bar_size=10, | ||||
**kwargs | ||||
): | ||||
"""Create a "Page" instance. | ||||
Parameters: | ||||
collection | ||||
Sequence representing the collection of items to page through. | ||||
page | ||||
The requested page number - starts with 1. Default: 1. | ||||
items_per_page | ||||
The maximal number of items to be displayed per page. | ||||
Default: 20. | ||||
item_count (optional) | ||||
The total number of items in the collection - if known. | ||||
If this parameter is not given then the paginator will count | ||||
the number of elements in the collection every time a "Page" | ||||
is created. Giving this parameter will speed up things. In a busy | ||||
real-life application you may want to cache the number of items. | ||||
url_maker (optional) | ||||
Callback to generate the URL of other pages, given its numbers. | ||||
Must accept one int parameter and return a URI string. | ||||
bar_size | ||||
maximum size of rendered pages numbers within radius | ||||
""" | ||||
if collection is not None: | ||||
if wrapper_class is None: | ||||
# Default case. The collection is already a list-type object. | ||||
self.collection = collection | ||||
else: | ||||
# Special case. A custom wrapper class is used to access elements of the collection. | ||||
self.collection = wrapper_class(collection) | ||||
else: | ||||
self.collection = [] | ||||
self.collection_type = type(collection) | ||||
if url_maker is not None: | ||||
self.url_maker = url_maker | ||||
else: | ||||
self.url_maker = self._default_url_maker | ||||
self.bar_size = bar_size | ||||
# Assign kwargs to self | ||||
self.kwargs = kwargs | ||||
# The self.page is the number of the current page. | ||||
# The first page has the number 1! | ||||
try: | ||||
self.page = int(page) # make it int() if we get it as a string | ||||
except (ValueError, TypeError): | ||||
self.page = 1 | ||||
# normally page should be always at least 1 but the original maintainer | ||||
# decided that for empty collection and empty page it can be...0? (based on tests) | ||||
# preserving behavior for BW compat | ||||
if self.page < 1: | ||||
self.page = 1 | ||||
self.items_per_page = items_per_page | ||||
# We subclassed "list" so we need to call its init() method | ||||
# and fill the new list with the items to be displayed on the page. | ||||
# We use list() so that the items on the current page are retrieved | ||||
# only once. In an SQL context that could otherwise lead to running the | ||||
# same SQL query every time items would be accessed. | ||||
# We do this here, prior to calling len() on the collection so that a | ||||
# wrapper class can execute a query with the knowledge of what the | ||||
# slice will be (for efficiency) and, in the same query, ask for the | ||||
# total number of items and only execute one query. | ||||
r4962 | ||||
r4091 | try: | |||
first = (self.page - 1) * items_per_page | ||||
last = first + items_per_page | ||||
self.items = list(self.collection[first:last]) | ||||
r4901 | except TypeError as err: | |||
r4091 | raise TypeError( | |||
r4962 | f"Your collection of type {type(self.collection)} cannot be handled " | |||
f"by paginate. ERROR:{err}" | ||||
r4091 | ) | |||
# Unless the user tells us how many items the collections has | ||||
# we calculate that ourselves. | ||||
if item_count is not None: | ||||
self.item_count = item_count | ||||
else: | ||||
self.item_count = len(self.collection) | ||||
# Compute the number of the first and last available page | ||||
if self.item_count > 0: | ||||
self.first_page = 1 | ||||
self.page_count = ((self.item_count - 1) // self.items_per_page) + 1 | ||||
self.last_page = self.first_page + self.page_count - 1 | ||||
# Make sure that the requested page number is the range of valid pages | ||||
if self.page > self.last_page: | ||||
self.page = self.last_page | ||||
elif self.page < self.first_page: | ||||
self.page = self.first_page | ||||
# Note: the number of items on this page can be less than | ||||
# items_per_page if the last page is not full | ||||
self.first_item = (self.page - 1) * items_per_page + 1 | ||||
self.last_item = min(self.first_item + items_per_page - 1, self.item_count) | ||||
# Links to previous and next page | ||||
if self.page > self.first_page: | ||||
self.previous_page = self.page - 1 | ||||
else: | ||||
self.previous_page = None | ||||
if self.page < self.last_page: | ||||
self.next_page = self.page + 1 | ||||
else: | ||||
self.next_page = None | ||||
# No items available | ||||
else: | ||||
self.first_page = None | ||||
self.page_count = 0 | ||||
self.last_page = None | ||||
self.first_item = None | ||||
self.last_item = None | ||||
self.previous_page = None | ||||
self.next_page = None | ||||
self.items = [] | ||||
# This is a subclass of the 'list' type. Initialise the list now. | ||||
list.__init__(self, self.items) | ||||
def __str__(self): | ||||
return ( | ||||
"Page:\n" | ||||
"Collection type: {0.collection_type}\n" | ||||
"Current page: {0.page}\n" | ||||
"First item: {0.first_item}\n" | ||||
"Last item: {0.last_item}\n" | ||||
"First page: {0.first_page}\n" | ||||
"Last page: {0.last_page}\n" | ||||
"Previous page: {0.previous_page}\n" | ||||
"Next page: {0.next_page}\n" | ||||
"Items per page: {0.items_per_page}\n" | ||||
"Total number of items: {0.item_count}\n" | ||||
"Number of pages: {0.page_count}\n" | ||||
).format(self) | ||||
def __repr__(self): | ||||
return "<paginate.Page: Page {0}/{1}>".format(self.page, self.page_count) | ||||
def pager( | ||||
self, | ||||
tmpl_format="~2~", | ||||
url=None, | ||||
show_if_single_page=False, | ||||
separator=" ", | ||||
symbol_first="<<", | ||||
symbol_last=">>", | ||||
symbol_previous="<", | ||||
symbol_next=">", | ||||
link_attr=None, | ||||
curpage_attr=None, | ||||
dotdot_attr=None, | ||||
link_tag=None, | ||||
): | ||||
""" | ||||
Return string with links to other pages (e.g. '1 .. 5 6 7 [8] 9 10 11 .. 50'). | ||||
tmpl_format: | ||||
Format string that defines how the pager is rendered. The string | ||||
can contain the following $-tokens that are substituted by the | ||||
string.Template module: | ||||
- $first_page: number of first reachable page | ||||
- $last_page: number of last reachable page | ||||
- $page: number of currently selected page | ||||
- $page_count: number of reachable pages | ||||
- $items_per_page: maximal number of items per page | ||||
- $first_item: index of first item on the current page | ||||
- $last_item: index of last item on the current page | ||||
- $item_count: total number of items | ||||
- $link_first: link to first page (unless this is first page) | ||||
- $link_last: link to last page (unless this is last page) | ||||
- $link_previous: link to previous page (unless this is first page) | ||||
- $link_next: link to next page (unless this is last page) | ||||
To render a range of pages the token '~3~' can be used. The | ||||
number sets the radius of pages around the current page. | ||||
Example for a range with radius 3: | ||||
'1 .. 5 6 7 [8] 9 10 11 .. 50' | ||||
Default: '~2~' | ||||
url | ||||
The URL that page links will point to. Make sure it contains the string | ||||
$page which will be replaced by the actual page number. | ||||
Must be given unless a url_maker is specified to __init__, in which | ||||
case this parameter is ignored. | ||||
symbol_first | ||||
String to be displayed as the text for the $link_first link above. | ||||
Default: '<<' (<<) | ||||
symbol_last | ||||
String to be displayed as the text for the $link_last link above. | ||||
Default: '>>' (>>) | ||||
symbol_previous | ||||
String to be displayed as the text for the $link_previous link above. | ||||
Default: '<' (<) | ||||
symbol_next | ||||
String to be displayed as the text for the $link_next link above. | ||||
Default: '>' (>) | ||||
separator: | ||||
String that is used to separate page links/numbers in the above range of pages. | ||||
Default: ' ' | ||||
show_if_single_page: | ||||
if True the navigator will be shown even if there is only one page. | ||||
Default: False | ||||
link_attr (optional) | ||||
A dictionary of attributes that get added to A-HREF links pointing to other pages. Can | ||||
be used to define a CSS style or class to customize the look of links. | ||||
Example: { 'style':'border: 1px solid green' } | ||||
Example: { 'class':'pager_link' } | ||||
curpage_attr (optional) | ||||
A dictionary of attributes that get added to the current page number in the pager (which | ||||
is obviously not a link). If this dictionary is not empty then the elements will be | ||||
wrapped in a SPAN tag with the given attributes. | ||||
Example: { 'style':'border: 3px solid blue' } | ||||
Example: { 'class':'pager_curpage' } | ||||
dotdot_attr (optional) | ||||
A dictionary of attributes that get added to the '..' string in the pager (which is | ||||
obviously not a link). If this dictionary is not empty then the elements will be wrapped | ||||
in a SPAN tag with the given attributes. | ||||
Example: { 'style':'color: #808080' } | ||||
Example: { 'class':'pager_dotdot' } | ||||
link_tag (optional) | ||||
A callable that accepts single argument `page` (page link information) | ||||
and generates string with html that represents the link for specific page. | ||||
Page objects are supplied from `link_map()` so the keys are the same. | ||||
""" | ||||
link_attr = link_attr or {} | ||||
curpage_attr = curpage_attr or {} | ||||
dotdot_attr = dotdot_attr or {} | ||||
self.curpage_attr = curpage_attr | ||||
self.separator = separator | ||||
self.link_attr = link_attr | ||||
self.dotdot_attr = dotdot_attr | ||||
self.url = url | ||||
self.link_tag = link_tag or self.default_link_tag | ||||
# Don't show navigator if there is no more than one page | ||||
if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page): | ||||
return "" | ||||
regex_res = re.search(r"~(\d+)~", tmpl_format) | ||||
if regex_res: | ||||
radius = regex_res.group(1) | ||||
else: | ||||
radius = 2 | ||||
self.radius = int(radius) | ||||
link_map = self.link_map( | ||||
tmpl_format=tmpl_format, | ||||
url=url, | ||||
show_if_single_page=show_if_single_page, | ||||
separator=separator, | ||||
symbol_first=symbol_first, | ||||
symbol_last=symbol_last, | ||||
symbol_previous=symbol_previous, | ||||
symbol_next=symbol_next, | ||||
link_attr=link_attr, | ||||
curpage_attr=curpage_attr, | ||||
dotdot_attr=dotdot_attr, | ||||
link_tag=link_tag, | ||||
) | ||||
links_markup = self._range(link_map, self.radius) | ||||
# Replace ~...~ in token tmpl_format by range of pages | ||||
result = re.sub(r"~(\d+)~", links_markup, tmpl_format) | ||||
link_first = ( | ||||
self.page > self.first_page and self.link_tag(link_map["first_page"]) or "" | ||||
) | ||||
link_last = ( | ||||
self.page < self.last_page and self.link_tag(link_map["last_page"]) or "" | ||||
) | ||||
link_previous = ( | ||||
self.previous_page and self.link_tag(link_map["previous_page"]) or "" | ||||
) | ||||
link_next = self.next_page and self.link_tag(link_map["next_page"]) or "" | ||||
# Interpolate '$' variables | ||||
result = Template(result).safe_substitute( | ||||
{ | ||||
"first_page": self.first_page, | ||||
"last_page": self.last_page, | ||||
"page": self.page, | ||||
"page_count": self.page_count, | ||||
"items_per_page": self.items_per_page, | ||||
"first_item": self.first_item, | ||||
"last_item": self.last_item, | ||||
"item_count": self.item_count, | ||||
"link_first": link_first, | ||||
"link_last": link_last, | ||||
"link_previous": link_previous, | ||||
"link_next": link_next, | ||||
} | ||||
) | ||||
return result | ||||
def _get_edges(self, cur_page, max_page, items): | ||||
cur_page = int(cur_page) | ||||
r4973 | edge = (items // 2) + 1 | |||
r4091 | if cur_page <= edge: | |||
r4973 | radius = max(items // 2, items - cur_page) | |||
r4091 | elif (max_page - cur_page) < edge: | |||
radius = (items - 1) - (max_page - cur_page) | ||||
else: | ||||
r4973 | radius = (items // 2) - 1 | |||
r4091 | ||||
left = max(1, (cur_page - radius)) | ||||
right = min(max_page, cur_page + radius) | ||||
return left, right | ||||
def link_map( | ||||
self, | ||||
tmpl_format="~2~", | ||||
url=None, | ||||
show_if_single_page=False, | ||||
separator=" ", | ||||
symbol_first="<<", | ||||
symbol_last=">>", | ||||
symbol_previous="<", | ||||
symbol_next=">", | ||||
link_attr=None, | ||||
curpage_attr=None, | ||||
dotdot_attr=None, | ||||
link_tag=None | ||||
): | ||||
""" Return map with links to other pages if default pager() function is not suitable solution. | ||||
tmpl_format: | ||||
Format string that defines how the pager would be normally rendered rendered. Uses same arguments as pager() | ||||
method, but returns a simple dictionary in form of: | ||||
{'current_page': {'attrs': {}, | ||||
'href': 'http://example.org/foo/page=1', | ||||
'value': 1}, | ||||
'first_page': {'attrs': {}, | ||||
'href': 'http://example.org/foo/page=1', | ||||
'type': 'first_page', | ||||
'value': 1}, | ||||
'last_page': {'attrs': {}, | ||||
'href': 'http://example.org/foo/page=8', | ||||
'type': 'last_page', | ||||
'value': 8}, | ||||
'next_page': {'attrs': {}, 'href': 'HREF', 'type': 'next_page', 'value': 2}, | ||||
'previous_page': None, | ||||
'range_pages': [{'attrs': {}, | ||||
'href': 'http://example.org/foo/page=1', | ||||
'type': 'current_page', | ||||
'value': 1}, | ||||
.... | ||||
{'attrs': {}, 'href': '', 'type': 'span', 'value': '..'}]} | ||||
The string can contain the following $-tokens that are substituted by the | ||||
string.Template module: | ||||
- $first_page: number of first reachable page | ||||
- $last_page: number of last reachable page | ||||
- $page: number of currently selected page | ||||
- $page_count: number of reachable pages | ||||
- $items_per_page: maximal number of items per page | ||||
- $first_item: index of first item on the current page | ||||
- $last_item: index of last item on the current page | ||||
- $item_count: total number of items | ||||
- $link_first: link to first page (unless this is first page) | ||||
- $link_last: link to last page (unless this is last page) | ||||
- $link_previous: link to previous page (unless this is first page) | ||||
- $link_next: link to next page (unless this is last page) | ||||
To render a range of pages the token '~3~' can be used. The | ||||
number sets the radius of pages around the current page. | ||||
Example for a range with radius 3: | ||||
'1 .. 5 6 7 [8] 9 10 11 .. 50' | ||||
Default: '~2~' | ||||
url | ||||
The URL that page links will point to. Make sure it contains the string | ||||
$page which will be replaced by the actual page number. | ||||
Must be given unless a url_maker is specified to __init__, in which | ||||
case this parameter is ignored. | ||||
symbol_first | ||||
String to be displayed as the text for the $link_first link above. | ||||
Default: '<<' (<<) | ||||
symbol_last | ||||
String to be displayed as the text for the $link_last link above. | ||||
Default: '>>' (>>) | ||||
symbol_previous | ||||
String to be displayed as the text for the $link_previous link above. | ||||
Default: '<' (<) | ||||
symbol_next | ||||
String to be displayed as the text for the $link_next link above. | ||||
Default: '>' (>) | ||||
separator: | ||||
String that is used to separate page links/numbers in the above range of pages. | ||||
Default: ' ' | ||||
show_if_single_page: | ||||
if True the navigator will be shown even if there is only one page. | ||||
Default: False | ||||
link_attr (optional) | ||||
A dictionary of attributes that get added to A-HREF links pointing to other pages. Can | ||||
be used to define a CSS style or class to customize the look of links. | ||||
Example: { 'style':'border: 1px solid green' } | ||||
Example: { 'class':'pager_link' } | ||||
curpage_attr (optional) | ||||
A dictionary of attributes that get added to the current page number in the pager (which | ||||
is obviously not a link). If this dictionary is not empty then the elements will be | ||||
wrapped in a SPAN tag with the given attributes. | ||||
Example: { 'style':'border: 3px solid blue' } | ||||
Example: { 'class':'pager_curpage' } | ||||
dotdot_attr (optional) | ||||
A dictionary of attributes that get added to the '..' string in the pager (which is | ||||
obviously not a link). If this dictionary is not empty then the elements will be wrapped | ||||
in a SPAN tag with the given attributes. | ||||
Example: { 'style':'color: #808080' } | ||||
Example: { 'class':'pager_dotdot' } | ||||
""" | ||||
link_attr = link_attr or {} | ||||
curpage_attr = curpage_attr or {} | ||||
dotdot_attr = dotdot_attr or {} | ||||
self.curpage_attr = curpage_attr | ||||
self.separator = separator | ||||
self.link_attr = link_attr | ||||
self.dotdot_attr = dotdot_attr | ||||
self.url = url | ||||
regex_res = re.search(r"~(\d+)~", tmpl_format) | ||||
if regex_res: | ||||
radius = regex_res.group(1) | ||||
else: | ||||
radius = 2 | ||||
self.radius = int(radius) | ||||
# Compute the first and last page number within the radius | ||||
# e.g. '1 .. 5 6 [7] 8 9 .. 12' | ||||
# -> leftmost_page = 5 | ||||
# -> rightmost_page = 9 | ||||
leftmost_page, rightmost_page = self._get_edges( | ||||
self.page, self.last_page, (self.radius * 2) + 1) | ||||
nav_items = { | ||||
"first_page": None, | ||||
"last_page": None, | ||||
"previous_page": None, | ||||
"next_page": None, | ||||
"current_page": None, | ||||
"radius": self.radius, | ||||
"range_pages": [], | ||||
} | ||||
if leftmost_page is None or rightmost_page is None: | ||||
return nav_items | ||||
nav_items["first_page"] = { | ||||
"type": "first_page", | ||||
r4973 | "value": str(symbol_first), | |||
r4091 | "attrs": self.link_attr, | |||
"number": self.first_page, | ||||
"href": self.url_maker(self.first_page), | ||||
} | ||||
# Insert dots if there are pages between the first page | ||||
# and the currently displayed page range | ||||
if leftmost_page - self.first_page > 1: | ||||
# Wrap in a SPAN tag if dotdot_attr is set | ||||
nav_items["range_pages"].append( | ||||
{ | ||||
"type": "span", | ||||
"value": "..", | ||||
"attrs": self.dotdot_attr, | ||||
"href": "", | ||||
"number": None, | ||||
} | ||||
) | ||||
for this_page in range(leftmost_page, rightmost_page + 1): | ||||
# Highlight the current page number and do not use a link | ||||
if this_page == self.page: | ||||
# Wrap in a SPAN tag if curpage_attr is set | ||||
nav_items["range_pages"].append( | ||||
{ | ||||
"type": "current_page", | ||||
r4973 | "value": str(this_page), | |||
r4091 | "number": this_page, | |||
"attrs": self.curpage_attr, | ||||
"href": self.url_maker(this_page), | ||||
} | ||||
) | ||||
nav_items["current_page"] = { | ||||
"value": this_page, | ||||
"attrs": self.curpage_attr, | ||||
"type": "current_page", | ||||
"href": self.url_maker(this_page), | ||||
} | ||||
# Otherwise create just a link to that page | ||||
else: | ||||
nav_items["range_pages"].append( | ||||
{ | ||||
"type": "page", | ||||
r4973 | "value": str(this_page), | |||
r4091 | "number": this_page, | |||
"attrs": self.link_attr, | ||||
"href": self.url_maker(this_page), | ||||
} | ||||
) | ||||
# Insert dots if there are pages between the displayed | ||||
# page numbers and the end of the page range | ||||
if self.last_page - rightmost_page > 1: | ||||
# Wrap in a SPAN tag if dotdot_attr is set | ||||
nav_items["range_pages"].append( | ||||
{ | ||||
"type": "span", | ||||
"value": "..", | ||||
"attrs": self.dotdot_attr, | ||||
"href": "", | ||||
"number": None, | ||||
} | ||||
) | ||||
# Create a link to the very last page (unless we are on the last | ||||
# page or there would be no need to insert '..' spacers) | ||||
nav_items["last_page"] = { | ||||
"type": "last_page", | ||||
r4973 | "value": str(symbol_last), | |||
r4091 | "attrs": self.link_attr, | |||
"href": self.url_maker(self.last_page), | ||||
"number": self.last_page, | ||||
} | ||||
nav_items["previous_page"] = { | ||||
"type": "previous_page", | ||||
r4973 | "value": str(symbol_previous), | |||
r4091 | "attrs": self.link_attr, | |||
"number": self.previous_page or self.first_page, | ||||
"href": self.url_maker(self.previous_page or self.first_page), | ||||
} | ||||
nav_items["next_page"] = { | ||||
"type": "next_page", | ||||
r4973 | "value": str(symbol_next), | |||
r4091 | "attrs": self.link_attr, | |||
"number": self.next_page or self.last_page, | ||||
"href": self.url_maker(self.next_page or self.last_page), | ||||
} | ||||
return nav_items | ||||
def _range(self, link_map, radius): | ||||
""" | ||||
Return range of linked pages to substitute placeholder in pattern | ||||
""" | ||||
# Compute the first and last page number within the radius | ||||
# e.g. '1 .. 5 6 [7] 8 9 .. 12' | ||||
# -> leftmost_page = 5 | ||||
# -> rightmost_page = 9 | ||||
leftmost_page, rightmost_page = self._get_edges( | ||||
self.page, self.last_page, (radius * 2) + 1) | ||||
nav_items = [] | ||||
# Create a link to the first page (unless we are on the first page | ||||
# or there would be no need to insert '..' spacers) | ||||
if self.first_page and self.page != self.first_page and self.first_page < leftmost_page: | ||||
page = link_map["first_page"].copy() | ||||
r4973 | page["value"] = str(page["number"]) | |||
r4091 | nav_items.append(self.link_tag(page)) | |||
for item in link_map["range_pages"]: | ||||
nav_items.append(self.link_tag(item)) | ||||
# Create a link to the very last page (unless we are on the last | ||||
# page or there would be no need to insert '..' spacers) | ||||
if self.last_page and self.page != self.last_page and rightmost_page < self.last_page: | ||||
page = link_map["last_page"].copy() | ||||
r4973 | page["value"] = str(page["number"]) | |||
r4091 | nav_items.append(self.link_tag(page)) | |||
return self.separator.join(nav_items) | ||||
def _default_url_maker(self, page_number): | ||||
if self.url is None: | ||||
raise Exception( | ||||
"You need to specify a 'url' parameter containing a '$page' placeholder." | ||||
) | ||||
if "$page" not in self.url: | ||||
raise Exception("The 'url' parameter must contain a '$page' placeholder.") | ||||
r4973 | return self.url.replace("$page", str(page_number)) | |||
r4091 | ||||
@staticmethod | ||||
def default_link_tag(item): | ||||
""" | ||||
Create an A-HREF tag that points to another page. | ||||
""" | ||||
text = item["value"] | ||||
target_url = item["href"] | ||||
if not item["href"] or item["type"] in ("span", "current_page"): | ||||
if item["attrs"]: | ||||
text = make_html_tag("span", **item["attrs"]) + text + "</span>" | ||||
return text | ||||
return make_html_tag("a", text=text, href=target_url, **item["attrs"]) | ||||
r5608 | # Below is RhodeCode custom code# Copyright (C) 2010-2024 RhodeCode GmbH | |||
r4091 | # | |||
# This program is free software: you can redistribute it and/or modify | ||||
# it under the terms of the GNU Affero General Public License, version 3 | ||||
# (only), as published by the Free Software Foundation. | ||||
# | ||||
# This program is distributed in the hope that it will be useful, | ||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
# GNU General Public License for more details. | ||||
# | ||||
# You should have received a copy of the GNU Affero General Public License | ||||
# along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||
# | ||||
# This program is dual-licensed. If you wish to learn more about the | ||||
# RhodeCode Enterprise Edition, including its added features, Support services, | ||||
# and proprietary license terms, please see https://rhodecode.com/licenses/ | ||||
PAGE_FORMAT = '$link_previous ~3~ $link_next' | ||||
class SqlalchemyOrmWrapper(object): | ||||
"""Wrapper class to access elements of a collection.""" | ||||
def __init__(self, pager, collection): | ||||
self.pager = pager | ||||
self.collection = collection | ||||
r4979 | def __getitem__(self, key): | |||
if not isinstance(key, slice): | ||||
raise ValueError('Pagination without a slice not supported') | ||||
r4091 | # Return a range of objects of an sqlalchemy.orm.query.Query object | |||
r4979 | return self.collection[key] | |||
r4091 | ||||
def __len__(self): | ||||
r4156 | # support empty types, without actually making a query. | |||
if self.collection is None or self.collection == []: | ||||
return 0 | ||||
r4091 | # Count the number of objects in an sqlalchemy.orm.query.Query object | |||
return self.collection.count() | ||||
class CustomPager(_Page): | ||||
@staticmethod | ||||
def disabled_link_tag(item): | ||||
""" | ||||
Create an A-HREF tag that is disabled | ||||
""" | ||||
text = item['value'] | ||||
attrs = item['attrs'].copy() | ||||
attrs['class'] = 'disabled ' + attrs['class'] | ||||
return make_html_tag('a', text=text, **attrs) | ||||
def render(self): | ||||
# Don't show navigator if there is no more than one page | ||||
if self.page_count == 0: | ||||
return "" | ||||
self.link_tag = self.default_link_tag | ||||
link_map = self.link_map( | ||||
tmpl_format=PAGE_FORMAT, url=None, | ||||
show_if_single_page=False, separator=' ', | ||||
symbol_first='<<', symbol_last='>>', | ||||
symbol_previous='<', symbol_next='>', | ||||
link_attr={'class': 'pager_link'}, | ||||
curpage_attr={'class': 'pager_curpage'}, | ||||
dotdot_attr={'class': 'pager_dotdot'}) | ||||
links_markup = self._range(link_map, self.radius) | ||||
link_first = ( | ||||
self.page > self.first_page and self.link_tag(link_map['first_page']) or '' | ||||
) | ||||
link_last = ( | ||||
self.page < self.last_page and self.link_tag(link_map['last_page']) or '' | ||||
) | ||||
link_previous = ( | ||||
self.previous_page and self.link_tag(link_map['previous_page']) | ||||
or self.disabled_link_tag(link_map['previous_page']) | ||||
) | ||||
link_next = ( | ||||
self.next_page and self.link_tag(link_map['next_page']) | ||||
or self.disabled_link_tag(link_map['next_page']) | ||||
) | ||||
# Interpolate '$' variables | ||||
# Replace ~...~ in token tmpl_format by range of pages | ||||
result = re.sub(r"~(\d+)~", links_markup, PAGE_FORMAT) | ||||
result = Template(result).safe_substitute( | ||||
{ | ||||
"links": links_markup, | ||||
"first_page": self.first_page, | ||||
"last_page": self.last_page, | ||||
"page": self.page, | ||||
"page_count": self.page_count, | ||||
"items_per_page": self.items_per_page, | ||||
"first_item": self.first_item, | ||||
"last_item": self.last_item, | ||||
"item_count": self.item_count, | ||||
"link_first": link_first, | ||||
"link_last": link_last, | ||||
"link_previous": link_previous, | ||||
"link_next": link_next, | ||||
} | ||||
) | ||||
return literal(result) | ||||
class Page(CustomPager): | ||||
""" | ||||
Custom pager to match rendering style with paginator | ||||
""" | ||||
def __init__(self, collection, page=1, items_per_page=20, item_count=None, | ||||
url_maker=None, **kwargs): | ||||
""" | ||||
r5116 | Special type of pager. We intercept a collection to wrap it in our custom | |||
r4091 | logic instead of using wrapper_class | |||
""" | ||||
super(Page, self).__init__(collection=collection, page=page, | ||||
items_per_page=items_per_page, item_count=item_count, | ||||
wrapper_class=None, url_maker=url_maker, **kwargs) | ||||
class SqlPage(CustomPager): | ||||
""" | ||||
Custom pager to match rendering style with paginator | ||||
""" | ||||
def __init__(self, collection, page=1, items_per_page=20, item_count=None, | ||||
url_maker=None, **kwargs): | ||||
""" | ||||
r5116 | Special type of pager. We intercept a collection to wrap it in our custom | |||
r4091 | logic instead of using wrapper_class | |||
""" | ||||
collection = SqlalchemyOrmWrapper(self, collection) | ||||
r5116 | super().__init__( | |||
collection=collection, | ||||
page=page, | ||||
items_per_page=items_per_page, | ||||
item_count=item_count, | ||||
wrapper_class=None, | ||||
url_maker=url_maker, | ||||
**kwargs | ||||
) | ||||
r4091 | ||||
class RepoCommitsWrapper(object): | ||||
"""Wrapper class to access elements of a collection.""" | ||||
def __init__(self, pager, collection): | ||||
self.pager = pager | ||||
self.collection = collection | ||||
r4979 | def __getitem__(self, key): | |||
r4091 | cur_page = self.pager.page | |||
items_per_page = self.pager.items_per_page | ||||
first_item = max(0, (len(self.collection) - (cur_page * items_per_page))) | ||||
last_item = ((len(self.collection) - 1) - items_per_page * (cur_page - 1)) | ||||
return reversed(list(self.collection[first_item:last_item + 1])) | ||||
def __len__(self): | ||||
return len(self.collection) | ||||
class RepoPage(CustomPager): | ||||
""" | ||||
Create a "RepoPage" instance. special pager for paging repository | ||||
""" | ||||
def __init__(self, collection, page=1, items_per_page=20, item_count=None, | ||||
url_maker=None, **kwargs): | ||||
""" | ||||
Special type of pager. We intercept collection to wrap it in our custom | ||||
logic instead of using wrapper_class | ||||
""" | ||||
collection = RepoCommitsWrapper(self, collection) | ||||
super(RepoPage, self).__init__(collection=collection, page=page, | ||||
items_per_page=items_per_page, item_count=item_count, | ||||
wrapper_class=None, url_maker=url_maker, **kwargs) | ||||