##// END OF EJS Templates
paginator: small fixes to code
super-admin -
r4979:29f30464 default
parent child Browse files
Show More
@@ -1,1061 +1,1057 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (c) 2007-2012 Christoph Haas <email@christoph-haas.de>
3 # Copyright (c) 2007-2012 Christoph Haas <email@christoph-haas.de>
4 # NOTE: MIT license based code, backported and edited by RhodeCode GmbH
4 # NOTE: MIT license based code, backported and edited by RhodeCode GmbH
5
5
6 """
6 """
7 paginate: helps split up large collections into individual pages
7 paginate: helps split up large collections into individual pages
8 ================================================================
8 ================================================================
9
9
10 What is pagination?
10 What is pagination?
11 ---------------------
11 ---------------------
12
12
13 This module helps split large lists of items into pages. The user is shown one page at a time and
13 This module helps split large lists of items into pages. The user is shown one page at a time and
14 can navigate to other pages. Imagine you are offering a company phonebook and let the user search
14 can navigate to other pages. Imagine you are offering a company phonebook and let the user search
15 the entries. The entire search result may contains 23 entries but you want to display no more than
15 the entries. The entire search result may contains 23 entries but you want to display no more than
16 10 entries at once. The first page contains entries 1-10, the second 11-20 and the third 21-23.
16 10 entries at once. The first page contains entries 1-10, the second 11-20 and the third 21-23.
17 Each "Page" instance represents the items of one of these three pages.
17 Each "Page" instance represents the items of one of these three pages.
18
18
19 See the documentation of the "Page" class for more information.
19 See the documentation of the "Page" class for more information.
20
20
21 How do I use it?
21 How do I use it?
22 ------------------
22 ------------------
23
23
24 A page of items is represented by the *Page* object. A *Page* gets initialized with these arguments:
24 A page of items is represented by the *Page* object. A *Page* gets initialized with these arguments:
25
25
26 - The collection of items to pick a range from. Usually just a list.
26 - The collection of items to pick a range from. Usually just a list.
27 - The page number you want to display. Default is 1: the first page.
27 - The page number you want to display. Default is 1: the first page.
28
28
29 Now we can make up a collection and create a Page instance of it::
29 Now we can make up a collection and create a Page instance of it::
30
30
31 # Create a sample collection of 1000 items
31 # Create a sample collection of 1000 items
32 >> my_collection = range(1000)
32 >> my_collection = range(1000)
33
33
34 # Create a Page object for the 3rd page (20 items per page is the default)
34 # Create a Page object for the 3rd page (20 items per page is the default)
35 >> my_page = Page(my_collection, page=3)
35 >> my_page = Page(my_collection, page=3)
36
36
37 # The page object can be printed as a string to get its details
37 # The page object can be printed as a string to get its details
38 >> str(my_page)
38 >> str(my_page)
39 Page:
39 Page:
40 Collection type: <type 'range'>
40 Collection type: <type 'range'>
41 Current page: 3
41 Current page: 3
42 First item: 41
42 First item: 41
43 Last item: 60
43 Last item: 60
44 First page: 1
44 First page: 1
45 Last page: 50
45 Last page: 50
46 Previous page: 2
46 Previous page: 2
47 Next page: 4
47 Next page: 4
48 Items per page: 20
48 Items per page: 20
49 Number of items: 1000
49 Number of items: 1000
50 Number of pages: 50
50 Number of pages: 50
51
51
52 # Print a list of items on the current page
52 # Print a list of items on the current page
53 >> my_page.items
53 >> my_page.items
54 [40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59]
54 [40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59]
55
55
56 # The *Page* object can be used as an iterator:
56 # The *Page* object can be used as an iterator:
57 >> for my_item in my_page: print(my_item)
57 >> for my_item in my_page: print(my_item)
58 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
58 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
59
59
60 # The .pager() method returns an HTML fragment with links to surrounding pages.
60 # The .pager() method returns an HTML fragment with links to surrounding pages.
61 >> my_page.pager(url="http://example.org/foo/page=$page")
61 >> my_page.pager(url="http://example.org/foo/page=$page")
62
62
63 <a href="http://example.org/foo/page=1">1</a>
63 <a href="http://example.org/foo/page=1">1</a>
64 <a href="http://example.org/foo/page=2">2</a>
64 <a href="http://example.org/foo/page=2">2</a>
65 3
65 3
66 <a href="http://example.org/foo/page=4">4</a>
66 <a href="http://example.org/foo/page=4">4</a>
67 <a href="http://example.org/foo/page=5">5</a>
67 <a href="http://example.org/foo/page=5">5</a>
68 ..
68 ..
69 <a href="http://example.org/foo/page=50">50</a>'
69 <a href="http://example.org/foo/page=50">50</a>'
70
70
71 # Without the HTML it would just look like:
71 # Without the HTML it would just look like:
72 # 1 2 [3] 4 5 .. 50
72 # 1 2 [3] 4 5 .. 50
73
73
74 # The pager can be customized:
74 # The pager can be customized:
75 >> my_page.pager('$link_previous ~3~ $link_next (Page $page of $page_count)',
75 >> my_page.pager('$link_previous ~3~ $link_next (Page $page of $page_count)',
76 url="http://example.org/foo/page=$page")
76 url="http://example.org/foo/page=$page")
77
77
78 <a href="http://example.org/foo/page=2">&lt;</a>
78 <a href="http://example.org/foo/page=2">&lt;</a>
79 <a href="http://example.org/foo/page=1">1</a>
79 <a href="http://example.org/foo/page=1">1</a>
80 <a href="http://example.org/foo/page=2">2</a>
80 <a href="http://example.org/foo/page=2">2</a>
81 3
81 3
82 <a href="http://example.org/foo/page=4">4</a>
82 <a href="http://example.org/foo/page=4">4</a>
83 <a href="http://example.org/foo/page=5">5</a>
83 <a href="http://example.org/foo/page=5">5</a>
84 <a href="http://example.org/foo/page=6">6</a>
84 <a href="http://example.org/foo/page=6">6</a>
85 ..
85 ..
86 <a href="http://example.org/foo/page=50">50</a>
86 <a href="http://example.org/foo/page=50">50</a>
87 <a href="http://example.org/foo/page=4">&gt;</a>
87 <a href="http://example.org/foo/page=4">&gt;</a>
88 (Page 3 of 50)
88 (Page 3 of 50)
89
89
90 # Without the HTML it would just look like:
90 # Without the HTML it would just look like:
91 # 1 2 [3] 4 5 6 .. 50 > (Page 3 of 50)
91 # 1 2 [3] 4 5 6 .. 50 > (Page 3 of 50)
92
92
93 # The url argument to the pager method can be omitted when an url_maker is
93 # The url argument to the pager method can be omitted when an url_maker is
94 # given during instantiation:
94 # given during instantiation:
95 >> my_page = Page(my_collection, page=3,
95 >> my_page = Page(my_collection, page=3,
96 url_maker=lambda p: "http://example.org/%s" % p)
96 url_maker=lambda p: "http://example.org/%s" % p)
97 >> page.pager()
97 >> page.pager()
98
98
99 There are some interesting parameters that customize the Page's behavior. See the documentation on
99 There are some interesting parameters that customize the Page's behavior. See the documentation on
100 ``Page`` and ``Page.pager()``.
100 ``Page`` and ``Page.pager()``.
101
101
102
102
103 Notes
103 Notes
104 -------
104 -------
105
105
106 Page numbers and item numbers start at 1. This concept has been used because users expect that the
106 Page numbers and item numbers start at 1. This concept has been used because users expect that the
107 first page has number 1 and the first item on a page also has number 1. So if you want to use the
107 first page has number 1 and the first item on a page also has number 1. So if you want to use the
108 page's items by their index number please note that you have to subtract 1.
108 page's items by their index number please note that you have to subtract 1.
109 """
109 """
110
110
111 import re
111 import re
112 import sys
113 from string import Template
112 from string import Template
114 from webhelpers2.html import literal
113 from webhelpers2.html import literal
115
114
116 # are we running at least python 3.x ?
117 PY3 = sys.version_info[0] >= 3
118
119 if PY3:
120 unicode = str
121
122
115
123 def make_html_tag(tag, text=None, **params):
116 def make_html_tag(tag, text=None, **params):
124 """Create an HTML tag string.
117 """Create an HTML tag string.
125
118
126 tag
119 tag
127 The HTML tag to use (e.g. 'a', 'span' or 'div')
120 The HTML tag to use (e.g. 'a', 'span' or 'div')
128
121
129 text
122 text
130 The text to enclose between opening and closing tag. If no text is specified then only
123 The text to enclose between opening and closing tag. If no text is specified then only
131 the opening tag is returned.
124 the opening tag is returned.
132
125
133 Example::
126 Example::
134 make_html_tag('a', text="Hello", href="/another/page")
127 make_html_tag('a', text="Hello", href="/another/page")
135 -> <a href="/another/page">Hello</a>
128 -> <a href="/another/page">Hello</a>
136
129
137 To use reserved Python keywords like "class" as a parameter prepend it with
130 To use reserved Python keywords like "class" as a parameter prepend it with
138 an underscore. Instead of "class='green'" use "_class='green'".
131 an underscore. Instead of "class='green'" use "_class='green'".
139
132
140 Warning: Quotes and apostrophes are not escaped."""
133 Warning: Quotes and apostrophes are not escaped."""
141 params_string = ""
134 params_string = ""
142
135
143 # Parameters are passed. Turn the dict into a string like "a=1 b=2 c=3" string.
136 # Parameters are passed. Turn the dict into a string like "a=1 b=2 c=3" string.
144 for key, value in sorted(params.items()):
137 for key, value in sorted(params.items()):
145 # Strip off a leading underscore from the attribute's key to allow attributes like '_class'
138 # Strip off a leading underscore from the attribute's key to allow attributes like '_class'
146 # to be used as a CSS class specification instead of the reserved Python keyword 'class'.
139 # to be used as a CSS class specification instead of the reserved Python keyword 'class'.
147 key = key.lstrip("_")
140 key = key.lstrip("_")
148
141
149 params_string += ' {0}="{1}"'.format(key, value)
142 params_string += ' {0}="{1}"'.format(key, value)
150
143
151 # Create the tag string
144 # Create the tag string
152 tag_string = "<{0}{1}>".format(tag, params_string)
145 tag_string = "<{0}{1}>".format(tag, params_string)
153
146
154 # Add text and closing tag if required.
147 # Add text and closing tag if required.
155 if text:
148 if text:
156 tag_string += "{0}</{1}>".format(text, tag)
149 tag_string += "{0}</{1}>".format(text, tag)
157
150
158 return tag_string
151 return tag_string
159
152
160
153
161 # Since the items on a page are mainly a list we subclass the "list" type
154 # Since the items on a page are mainly a list we subclass the "list" type
162 class _Page(list):
155 class _Page(list):
163 """A list/iterator representing the items on one page of a larger collection.
156 """A list/iterator representing the items on one page of a larger collection.
164
157
165 An instance of the "Page" class is created from a _collection_ which is any
158 An instance of the "Page" class is created from a _collection_ which is any
166 list-like object that allows random access to its elements.
159 list-like object that allows random access to its elements.
167
160
168 The instance works as an iterator running from the first item to the last item on the given
161 The instance works as an iterator running from the first item to the last item on the given
169 page. The Page.pager() method creates a link list allowing the user to go to other pages.
162 page. The Page.pager() method creates a link list allowing the user to go to other pages.
170
163
171 A "Page" does not only carry the items on a certain page. It gives you additional information
164 A "Page" does not only carry the items on a certain page. It gives you additional information
172 about the page in these "Page" object attributes:
165 about the page in these "Page" object attributes:
173
166
174 item_count
167 item_count
175 Number of items in the collection
168 Number of items in the collection
176
169
177 **WARNING:** Unless you pass in an item_count, a count will be
170 **WARNING:** Unless you pass in an item_count, a count will be
178 performed on the collection every time a Page instance is created.
171 performed on the collection every time a Page instance is created.
179
172
180 page
173 page
181 Number of the current page
174 Number of the current page
182
175
183 items_per_page
176 items_per_page
184 Maximal number of items displayed on a page
177 Maximal number of items displayed on a page
185
178
186 first_page
179 first_page
187 Number of the first page - usually 1 :)
180 Number of the first page - usually 1 :)
188
181
189 last_page
182 last_page
190 Number of the last page
183 Number of the last page
191
184
192 previous_page
185 previous_page
193 Number of the previous page. If this is the first page it returns None.
186 Number of the previous page. If this is the first page it returns None.
194
187
195 next_page
188 next_page
196 Number of the next page. If this is the last page it returns None.
189 Number of the next page. If this is the last page it returns None.
197
190
198 page_count
191 page_count
199 Number of pages
192 Number of pages
200
193
201 items
194 items
202 Sequence/iterator of items on the current page
195 Sequence/iterator of items on the current page
203
196
204 first_item
197 first_item
205 Index of first item on the current page - starts with 1
198 Index of first item on the current page - starts with 1
206
199
207 last_item
200 last_item
208 Index of last item on the current page
201 Index of last item on the current page
209 """
202 """
210
203
211 def __init__(
204 def __init__(
212 self,
205 self,
213 collection,
206 collection,
214 page=1,
207 page=1,
215 items_per_page=20,
208 items_per_page=20,
216 item_count=None,
209 item_count=None,
217 wrapper_class=None,
210 wrapper_class=None,
218 url_maker=None,
211 url_maker=None,
219 bar_size=10,
212 bar_size=10,
220 **kwargs
213 **kwargs
221 ):
214 ):
222 """Create a "Page" instance.
215 """Create a "Page" instance.
223
216
224 Parameters:
217 Parameters:
225
218
226 collection
219 collection
227 Sequence representing the collection of items to page through.
220 Sequence representing the collection of items to page through.
228
221
229 page
222 page
230 The requested page number - starts with 1. Default: 1.
223 The requested page number - starts with 1. Default: 1.
231
224
232 items_per_page
225 items_per_page
233 The maximal number of items to be displayed per page.
226 The maximal number of items to be displayed per page.
234 Default: 20.
227 Default: 20.
235
228
236 item_count (optional)
229 item_count (optional)
237 The total number of items in the collection - if known.
230 The total number of items in the collection - if known.
238 If this parameter is not given then the paginator will count
231 If this parameter is not given then the paginator will count
239 the number of elements in the collection every time a "Page"
232 the number of elements in the collection every time a "Page"
240 is created. Giving this parameter will speed up things. In a busy
233 is created. Giving this parameter will speed up things. In a busy
241 real-life application you may want to cache the number of items.
234 real-life application you may want to cache the number of items.
242
235
243 url_maker (optional)
236 url_maker (optional)
244 Callback to generate the URL of other pages, given its numbers.
237 Callback to generate the URL of other pages, given its numbers.
245 Must accept one int parameter and return a URI string.
238 Must accept one int parameter and return a URI string.
246
239
247 bar_size
240 bar_size
248 maximum size of rendered pages numbers within radius
241 maximum size of rendered pages numbers within radius
249
242
250 """
243 """
251 if collection is not None:
244 if collection is not None:
252 if wrapper_class is None:
245 if wrapper_class is None:
253 # Default case. The collection is already a list-type object.
246 # Default case. The collection is already a list-type object.
254 self.collection = collection
247 self.collection = collection
255 else:
248 else:
256 # Special case. A custom wrapper class is used to access elements of the collection.
249 # Special case. A custom wrapper class is used to access elements of the collection.
257 self.collection = wrapper_class(collection)
250 self.collection = wrapper_class(collection)
258 else:
251 else:
259 self.collection = []
252 self.collection = []
260
253
261 self.collection_type = type(collection)
254 self.collection_type = type(collection)
262
255
263 if url_maker is not None:
256 if url_maker is not None:
264 self.url_maker = url_maker
257 self.url_maker = url_maker
265 else:
258 else:
266 self.url_maker = self._default_url_maker
259 self.url_maker = self._default_url_maker
267 self.bar_size = bar_size
260 self.bar_size = bar_size
268 # Assign kwargs to self
261 # Assign kwargs to self
269 self.kwargs = kwargs
262 self.kwargs = kwargs
270
263
271 # The self.page is the number of the current page.
264 # The self.page is the number of the current page.
272 # The first page has the number 1!
265 # The first page has the number 1!
273 try:
266 try:
274 self.page = int(page) # make it int() if we get it as a string
267 self.page = int(page) # make it int() if we get it as a string
275 except (ValueError, TypeError):
268 except (ValueError, TypeError):
276 self.page = 1
269 self.page = 1
277 # normally page should be always at least 1 but the original maintainer
270 # normally page should be always at least 1 but the original maintainer
278 # decided that for empty collection and empty page it can be...0? (based on tests)
271 # decided that for empty collection and empty page it can be...0? (based on tests)
279 # preserving behavior for BW compat
272 # preserving behavior for BW compat
280 if self.page < 1:
273 if self.page < 1:
281 self.page = 1
274 self.page = 1
282
275
283 self.items_per_page = items_per_page
276 self.items_per_page = items_per_page
284
277
285 # We subclassed "list" so we need to call its init() method
278 # We subclassed "list" so we need to call its init() method
286 # and fill the new list with the items to be displayed on the page.
279 # and fill the new list with the items to be displayed on the page.
287 # We use list() so that the items on the current page are retrieved
280 # We use list() so that the items on the current page are retrieved
288 # only once. In an SQL context that could otherwise lead to running the
281 # only once. In an SQL context that could otherwise lead to running the
289 # same SQL query every time items would be accessed.
282 # same SQL query every time items would be accessed.
290 # We do this here, prior to calling len() on the collection so that a
283 # We do this here, prior to calling len() on the collection so that a
291 # wrapper class can execute a query with the knowledge of what the
284 # wrapper class can execute a query with the knowledge of what the
292 # slice will be (for efficiency) and, in the same query, ask for the
285 # slice will be (for efficiency) and, in the same query, ask for the
293 # total number of items and only execute one query.
286 # total number of items and only execute one query.
294
287
295 try:
288 try:
296 first = (self.page - 1) * items_per_page
289 first = (self.page - 1) * items_per_page
297 last = first + items_per_page
290 last = first + items_per_page
298 self.items = list(self.collection[first:last])
291 self.items = list(self.collection[first:last])
299 except TypeError as err:
292 except TypeError as err:
300 raise TypeError(
293 raise TypeError(
301 f"Your collection of type {type(self.collection)} cannot be handled "
294 f"Your collection of type {type(self.collection)} cannot be handled "
302 f"by paginate. ERROR:{err}"
295 f"by paginate. ERROR:{err}"
303 )
296 )
304
297
305 # Unless the user tells us how many items the collections has
298 # Unless the user tells us how many items the collections has
306 # we calculate that ourselves.
299 # we calculate that ourselves.
307 if item_count is not None:
300 if item_count is not None:
308 self.item_count = item_count
301 self.item_count = item_count
309 else:
302 else:
310 self.item_count = len(self.collection)
303 self.item_count = len(self.collection)
311
304
312 # Compute the number of the first and last available page
305 # Compute the number of the first and last available page
313 if self.item_count > 0:
306 if self.item_count > 0:
314 self.first_page = 1
307 self.first_page = 1
315 self.page_count = ((self.item_count - 1) // self.items_per_page) + 1
308 self.page_count = ((self.item_count - 1) // self.items_per_page) + 1
316 self.last_page = self.first_page + self.page_count - 1
309 self.last_page = self.first_page + self.page_count - 1
317
310
318 # Make sure that the requested page number is the range of valid pages
311 # Make sure that the requested page number is the range of valid pages
319 if self.page > self.last_page:
312 if self.page > self.last_page:
320 self.page = self.last_page
313 self.page = self.last_page
321 elif self.page < self.first_page:
314 elif self.page < self.first_page:
322 self.page = self.first_page
315 self.page = self.first_page
323
316
324 # Note: the number of items on this page can be less than
317 # Note: the number of items on this page can be less than
325 # items_per_page if the last page is not full
318 # items_per_page if the last page is not full
326 self.first_item = (self.page - 1) * items_per_page + 1
319 self.first_item = (self.page - 1) * items_per_page + 1
327 self.last_item = min(self.first_item + items_per_page - 1, self.item_count)
320 self.last_item = min(self.first_item + items_per_page - 1, self.item_count)
328
321
329 # Links to previous and next page
322 # Links to previous and next page
330 if self.page > self.first_page:
323 if self.page > self.first_page:
331 self.previous_page = self.page - 1
324 self.previous_page = self.page - 1
332 else:
325 else:
333 self.previous_page = None
326 self.previous_page = None
334
327
335 if self.page < self.last_page:
328 if self.page < self.last_page:
336 self.next_page = self.page + 1
329 self.next_page = self.page + 1
337 else:
330 else:
338 self.next_page = None
331 self.next_page = None
339
332
340 # No items available
333 # No items available
341 else:
334 else:
342 self.first_page = None
335 self.first_page = None
343 self.page_count = 0
336 self.page_count = 0
344 self.last_page = None
337 self.last_page = None
345 self.first_item = None
338 self.first_item = None
346 self.last_item = None
339 self.last_item = None
347 self.previous_page = None
340 self.previous_page = None
348 self.next_page = None
341 self.next_page = None
349 self.items = []
342 self.items = []
350
343
351 # This is a subclass of the 'list' type. Initialise the list now.
344 # This is a subclass of the 'list' type. Initialise the list now.
352 list.__init__(self, self.items)
345 list.__init__(self, self.items)
353
346
354 def __str__(self):
347 def __str__(self):
355 return (
348 return (
356 "Page:\n"
349 "Page:\n"
357 "Collection type: {0.collection_type}\n"
350 "Collection type: {0.collection_type}\n"
358 "Current page: {0.page}\n"
351 "Current page: {0.page}\n"
359 "First item: {0.first_item}\n"
352 "First item: {0.first_item}\n"
360 "Last item: {0.last_item}\n"
353 "Last item: {0.last_item}\n"
361 "First page: {0.first_page}\n"
354 "First page: {0.first_page}\n"
362 "Last page: {0.last_page}\n"
355 "Last page: {0.last_page}\n"
363 "Previous page: {0.previous_page}\n"
356 "Previous page: {0.previous_page}\n"
364 "Next page: {0.next_page}\n"
357 "Next page: {0.next_page}\n"
365 "Items per page: {0.items_per_page}\n"
358 "Items per page: {0.items_per_page}\n"
366 "Total number of items: {0.item_count}\n"
359 "Total number of items: {0.item_count}\n"
367 "Number of pages: {0.page_count}\n"
360 "Number of pages: {0.page_count}\n"
368 ).format(self)
361 ).format(self)
369
362
370 def __repr__(self):
363 def __repr__(self):
371 return "<paginate.Page: Page {0}/{1}>".format(self.page, self.page_count)
364 return "<paginate.Page: Page {0}/{1}>".format(self.page, self.page_count)
372
365
373 def pager(
366 def pager(
374 self,
367 self,
375 tmpl_format="~2~",
368 tmpl_format="~2~",
376 url=None,
369 url=None,
377 show_if_single_page=False,
370 show_if_single_page=False,
378 separator=" ",
371 separator=" ",
379 symbol_first="&lt;&lt;",
372 symbol_first="&lt;&lt;",
380 symbol_last="&gt;&gt;",
373 symbol_last="&gt;&gt;",
381 symbol_previous="&lt;",
374 symbol_previous="&lt;",
382 symbol_next="&gt;",
375 symbol_next="&gt;",
383 link_attr=None,
376 link_attr=None,
384 curpage_attr=None,
377 curpage_attr=None,
385 dotdot_attr=None,
378 dotdot_attr=None,
386 link_tag=None,
379 link_tag=None,
387 ):
380 ):
388 """
381 """
389 Return string with links to other pages (e.g. '1 .. 5 6 7 [8] 9 10 11 .. 50').
382 Return string with links to other pages (e.g. '1 .. 5 6 7 [8] 9 10 11 .. 50').
390
383
391 tmpl_format:
384 tmpl_format:
392 Format string that defines how the pager is rendered. The string
385 Format string that defines how the pager is rendered. The string
393 can contain the following $-tokens that are substituted by the
386 can contain the following $-tokens that are substituted by the
394 string.Template module:
387 string.Template module:
395
388
396 - $first_page: number of first reachable page
389 - $first_page: number of first reachable page
397 - $last_page: number of last reachable page
390 - $last_page: number of last reachable page
398 - $page: number of currently selected page
391 - $page: number of currently selected page
399 - $page_count: number of reachable pages
392 - $page_count: number of reachable pages
400 - $items_per_page: maximal number of items per page
393 - $items_per_page: maximal number of items per page
401 - $first_item: index of first item on the current page
394 - $first_item: index of first item on the current page
402 - $last_item: index of last item on the current page
395 - $last_item: index of last item on the current page
403 - $item_count: total number of items
396 - $item_count: total number of items
404 - $link_first: link to first page (unless this is first page)
397 - $link_first: link to first page (unless this is first page)
405 - $link_last: link to last page (unless this is last page)
398 - $link_last: link to last page (unless this is last page)
406 - $link_previous: link to previous page (unless this is first page)
399 - $link_previous: link to previous page (unless this is first page)
407 - $link_next: link to next page (unless this is last page)
400 - $link_next: link to next page (unless this is last page)
408
401
409 To render a range of pages the token '~3~' can be used. The
402 To render a range of pages the token '~3~' can be used. The
410 number sets the radius of pages around the current page.
403 number sets the radius of pages around the current page.
411 Example for a range with radius 3:
404 Example for a range with radius 3:
412
405
413 '1 .. 5 6 7 [8] 9 10 11 .. 50'
406 '1 .. 5 6 7 [8] 9 10 11 .. 50'
414
407
415 Default: '~2~'
408 Default: '~2~'
416
409
417 url
410 url
418 The URL that page links will point to. Make sure it contains the string
411 The URL that page links will point to. Make sure it contains the string
419 $page which will be replaced by the actual page number.
412 $page which will be replaced by the actual page number.
420 Must be given unless a url_maker is specified to __init__, in which
413 Must be given unless a url_maker is specified to __init__, in which
421 case this parameter is ignored.
414 case this parameter is ignored.
422
415
423 symbol_first
416 symbol_first
424 String to be displayed as the text for the $link_first link above.
417 String to be displayed as the text for the $link_first link above.
425
418
426 Default: '&lt;&lt;' (<<)
419 Default: '&lt;&lt;' (<<)
427
420
428 symbol_last
421 symbol_last
429 String to be displayed as the text for the $link_last link above.
422 String to be displayed as the text for the $link_last link above.
430
423
431 Default: '&gt;&gt;' (>>)
424 Default: '&gt;&gt;' (>>)
432
425
433 symbol_previous
426 symbol_previous
434 String to be displayed as the text for the $link_previous link above.
427 String to be displayed as the text for the $link_previous link above.
435
428
436 Default: '&lt;' (<)
429 Default: '&lt;' (<)
437
430
438 symbol_next
431 symbol_next
439 String to be displayed as the text for the $link_next link above.
432 String to be displayed as the text for the $link_next link above.
440
433
441 Default: '&gt;' (>)
434 Default: '&gt;' (>)
442
435
443 separator:
436 separator:
444 String that is used to separate page links/numbers in the above range of pages.
437 String that is used to separate page links/numbers in the above range of pages.
445
438
446 Default: ' '
439 Default: ' '
447
440
448 show_if_single_page:
441 show_if_single_page:
449 if True the navigator will be shown even if there is only one page.
442 if True the navigator will be shown even if there is only one page.
450
443
451 Default: False
444 Default: False
452
445
453 link_attr (optional)
446 link_attr (optional)
454 A dictionary of attributes that get added to A-HREF links pointing to other pages. Can
447 A dictionary of attributes that get added to A-HREF links pointing to other pages. Can
455 be used to define a CSS style or class to customize the look of links.
448 be used to define a CSS style or class to customize the look of links.
456
449
457 Example: { 'style':'border: 1px solid green' }
450 Example: { 'style':'border: 1px solid green' }
458 Example: { 'class':'pager_link' }
451 Example: { 'class':'pager_link' }
459
452
460 curpage_attr (optional)
453 curpage_attr (optional)
461 A dictionary of attributes that get added to the current page number in the pager (which
454 A dictionary of attributes that get added to the current page number in the pager (which
462 is obviously not a link). If this dictionary is not empty then the elements will be
455 is obviously not a link). If this dictionary is not empty then the elements will be
463 wrapped in a SPAN tag with the given attributes.
456 wrapped in a SPAN tag with the given attributes.
464
457
465 Example: { 'style':'border: 3px solid blue' }
458 Example: { 'style':'border: 3px solid blue' }
466 Example: { 'class':'pager_curpage' }
459 Example: { 'class':'pager_curpage' }
467
460
468 dotdot_attr (optional)
461 dotdot_attr (optional)
469 A dictionary of attributes that get added to the '..' string in the pager (which is
462 A dictionary of attributes that get added to the '..' string in the pager (which is
470 obviously not a link). If this dictionary is not empty then the elements will be wrapped
463 obviously not a link). If this dictionary is not empty then the elements will be wrapped
471 in a SPAN tag with the given attributes.
464 in a SPAN tag with the given attributes.
472
465
473 Example: { 'style':'color: #808080' }
466 Example: { 'style':'color: #808080' }
474 Example: { 'class':'pager_dotdot' }
467 Example: { 'class':'pager_dotdot' }
475
468
476 link_tag (optional)
469 link_tag (optional)
477 A callable that accepts single argument `page` (page link information)
470 A callable that accepts single argument `page` (page link information)
478 and generates string with html that represents the link for specific page.
471 and generates string with html that represents the link for specific page.
479 Page objects are supplied from `link_map()` so the keys are the same.
472 Page objects are supplied from `link_map()` so the keys are the same.
480
473
481
474
482 """
475 """
483 link_attr = link_attr or {}
476 link_attr = link_attr or {}
484 curpage_attr = curpage_attr or {}
477 curpage_attr = curpage_attr or {}
485 dotdot_attr = dotdot_attr or {}
478 dotdot_attr = dotdot_attr or {}
486 self.curpage_attr = curpage_attr
479 self.curpage_attr = curpage_attr
487 self.separator = separator
480 self.separator = separator
488 self.link_attr = link_attr
481 self.link_attr = link_attr
489 self.dotdot_attr = dotdot_attr
482 self.dotdot_attr = dotdot_attr
490 self.url = url
483 self.url = url
491 self.link_tag = link_tag or self.default_link_tag
484 self.link_tag = link_tag or self.default_link_tag
492
485
493 # Don't show navigator if there is no more than one page
486 # Don't show navigator if there is no more than one page
494 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
487 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
495 return ""
488 return ""
496
489
497 regex_res = re.search(r"~(\d+)~", tmpl_format)
490 regex_res = re.search(r"~(\d+)~", tmpl_format)
498 if regex_res:
491 if regex_res:
499 radius = regex_res.group(1)
492 radius = regex_res.group(1)
500 else:
493 else:
501 radius = 2
494 radius = 2
502
495
503 self.radius = int(radius)
496 self.radius = int(radius)
504 link_map = self.link_map(
497 link_map = self.link_map(
505 tmpl_format=tmpl_format,
498 tmpl_format=tmpl_format,
506 url=url,
499 url=url,
507 show_if_single_page=show_if_single_page,
500 show_if_single_page=show_if_single_page,
508 separator=separator,
501 separator=separator,
509 symbol_first=symbol_first,
502 symbol_first=symbol_first,
510 symbol_last=symbol_last,
503 symbol_last=symbol_last,
511 symbol_previous=symbol_previous,
504 symbol_previous=symbol_previous,
512 symbol_next=symbol_next,
505 symbol_next=symbol_next,
513 link_attr=link_attr,
506 link_attr=link_attr,
514 curpage_attr=curpage_attr,
507 curpage_attr=curpage_attr,
515 dotdot_attr=dotdot_attr,
508 dotdot_attr=dotdot_attr,
516 link_tag=link_tag,
509 link_tag=link_tag,
517 )
510 )
518 links_markup = self._range(link_map, self.radius)
511 links_markup = self._range(link_map, self.radius)
519
512
520 # Replace ~...~ in token tmpl_format by range of pages
513 # Replace ~...~ in token tmpl_format by range of pages
521 result = re.sub(r"~(\d+)~", links_markup, tmpl_format)
514 result = re.sub(r"~(\d+)~", links_markup, tmpl_format)
522
515
523 link_first = (
516 link_first = (
524 self.page > self.first_page and self.link_tag(link_map["first_page"]) or ""
517 self.page > self.first_page and self.link_tag(link_map["first_page"]) or ""
525 )
518 )
526 link_last = (
519 link_last = (
527 self.page < self.last_page and self.link_tag(link_map["last_page"]) or ""
520 self.page < self.last_page and self.link_tag(link_map["last_page"]) or ""
528 )
521 )
529 link_previous = (
522 link_previous = (
530 self.previous_page and self.link_tag(link_map["previous_page"]) or ""
523 self.previous_page and self.link_tag(link_map["previous_page"]) or ""
531 )
524 )
532 link_next = self.next_page and self.link_tag(link_map["next_page"]) or ""
525 link_next = self.next_page and self.link_tag(link_map["next_page"]) or ""
533 # Interpolate '$' variables
526 # Interpolate '$' variables
534 result = Template(result).safe_substitute(
527 result = Template(result).safe_substitute(
535 {
528 {
536 "first_page": self.first_page,
529 "first_page": self.first_page,
537 "last_page": self.last_page,
530 "last_page": self.last_page,
538 "page": self.page,
531 "page": self.page,
539 "page_count": self.page_count,
532 "page_count": self.page_count,
540 "items_per_page": self.items_per_page,
533 "items_per_page": self.items_per_page,
541 "first_item": self.first_item,
534 "first_item": self.first_item,
542 "last_item": self.last_item,
535 "last_item": self.last_item,
543 "item_count": self.item_count,
536 "item_count": self.item_count,
544 "link_first": link_first,
537 "link_first": link_first,
545 "link_last": link_last,
538 "link_last": link_last,
546 "link_previous": link_previous,
539 "link_previous": link_previous,
547 "link_next": link_next,
540 "link_next": link_next,
548 }
541 }
549 )
542 )
550
543
551 return result
544 return result
552
545
553 def _get_edges(self, cur_page, max_page, items):
546 def _get_edges(self, cur_page, max_page, items):
554 cur_page = int(cur_page)
547 cur_page = int(cur_page)
555 edge = (items // 2) + 1
548 edge = (items // 2) + 1
556 if cur_page <= edge:
549 if cur_page <= edge:
557 radius = max(items // 2, items - cur_page)
550 radius = max(items // 2, items - cur_page)
558 elif (max_page - cur_page) < edge:
551 elif (max_page - cur_page) < edge:
559 radius = (items - 1) - (max_page - cur_page)
552 radius = (items - 1) - (max_page - cur_page)
560 else:
553 else:
561 radius = (items // 2) - 1
554 radius = (items // 2) - 1
562
555
563 left = max(1, (cur_page - radius))
556 left = max(1, (cur_page - radius))
564 right = min(max_page, cur_page + radius)
557 right = min(max_page, cur_page + radius)
565 return left, right
558 return left, right
566
559
567 def link_map(
560 def link_map(
568 self,
561 self,
569 tmpl_format="~2~",
562 tmpl_format="~2~",
570 url=None,
563 url=None,
571 show_if_single_page=False,
564 show_if_single_page=False,
572 separator=" ",
565 separator=" ",
573 symbol_first="&lt;&lt;",
566 symbol_first="&lt;&lt;",
574 symbol_last="&gt;&gt;",
567 symbol_last="&gt;&gt;",
575 symbol_previous="&lt;",
568 symbol_previous="&lt;",
576 symbol_next="&gt;",
569 symbol_next="&gt;",
577 link_attr=None,
570 link_attr=None,
578 curpage_attr=None,
571 curpage_attr=None,
579 dotdot_attr=None,
572 dotdot_attr=None,
580 link_tag=None
573 link_tag=None
581 ):
574 ):
582 """ Return map with links to other pages if default pager() function is not suitable solution.
575 """ Return map with links to other pages if default pager() function is not suitable solution.
583 tmpl_format:
576 tmpl_format:
584 Format string that defines how the pager would be normally rendered rendered. Uses same arguments as pager()
577 Format string that defines how the pager would be normally rendered rendered. Uses same arguments as pager()
585 method, but returns a simple dictionary in form of:
578 method, but returns a simple dictionary in form of:
586 {'current_page': {'attrs': {},
579 {'current_page': {'attrs': {},
587 'href': 'http://example.org/foo/page=1',
580 'href': 'http://example.org/foo/page=1',
588 'value': 1},
581 'value': 1},
589 'first_page': {'attrs': {},
582 'first_page': {'attrs': {},
590 'href': 'http://example.org/foo/page=1',
583 'href': 'http://example.org/foo/page=1',
591 'type': 'first_page',
584 'type': 'first_page',
592 'value': 1},
585 'value': 1},
593 'last_page': {'attrs': {},
586 'last_page': {'attrs': {},
594 'href': 'http://example.org/foo/page=8',
587 'href': 'http://example.org/foo/page=8',
595 'type': 'last_page',
588 'type': 'last_page',
596 'value': 8},
589 'value': 8},
597 'next_page': {'attrs': {}, 'href': 'HREF', 'type': 'next_page', 'value': 2},
590 'next_page': {'attrs': {}, 'href': 'HREF', 'type': 'next_page', 'value': 2},
598 'previous_page': None,
591 'previous_page': None,
599 'range_pages': [{'attrs': {},
592 'range_pages': [{'attrs': {},
600 'href': 'http://example.org/foo/page=1',
593 'href': 'http://example.org/foo/page=1',
601 'type': 'current_page',
594 'type': 'current_page',
602 'value': 1},
595 'value': 1},
603 ....
596 ....
604 {'attrs': {}, 'href': '', 'type': 'span', 'value': '..'}]}
597 {'attrs': {}, 'href': '', 'type': 'span', 'value': '..'}]}
605
598
606
599
607 The string can contain the following $-tokens that are substituted by the
600 The string can contain the following $-tokens that are substituted by the
608 string.Template module:
601 string.Template module:
609
602
610 - $first_page: number of first reachable page
603 - $first_page: number of first reachable page
611 - $last_page: number of last reachable page
604 - $last_page: number of last reachable page
612 - $page: number of currently selected page
605 - $page: number of currently selected page
613 - $page_count: number of reachable pages
606 - $page_count: number of reachable pages
614 - $items_per_page: maximal number of items per page
607 - $items_per_page: maximal number of items per page
615 - $first_item: index of first item on the current page
608 - $first_item: index of first item on the current page
616 - $last_item: index of last item on the current page
609 - $last_item: index of last item on the current page
617 - $item_count: total number of items
610 - $item_count: total number of items
618 - $link_first: link to first page (unless this is first page)
611 - $link_first: link to first page (unless this is first page)
619 - $link_last: link to last page (unless this is last page)
612 - $link_last: link to last page (unless this is last page)
620 - $link_previous: link to previous page (unless this is first page)
613 - $link_previous: link to previous page (unless this is first page)
621 - $link_next: link to next page (unless this is last page)
614 - $link_next: link to next page (unless this is last page)
622
615
623 To render a range of pages the token '~3~' can be used. The
616 To render a range of pages the token '~3~' can be used. The
624 number sets the radius of pages around the current page.
617 number sets the radius of pages around the current page.
625 Example for a range with radius 3:
618 Example for a range with radius 3:
626
619
627 '1 .. 5 6 7 [8] 9 10 11 .. 50'
620 '1 .. 5 6 7 [8] 9 10 11 .. 50'
628
621
629 Default: '~2~'
622 Default: '~2~'
630
623
631 url
624 url
632 The URL that page links will point to. Make sure it contains the string
625 The URL that page links will point to. Make sure it contains the string
633 $page which will be replaced by the actual page number.
626 $page which will be replaced by the actual page number.
634 Must be given unless a url_maker is specified to __init__, in which
627 Must be given unless a url_maker is specified to __init__, in which
635 case this parameter is ignored.
628 case this parameter is ignored.
636
629
637 symbol_first
630 symbol_first
638 String to be displayed as the text for the $link_first link above.
631 String to be displayed as the text for the $link_first link above.
639
632
640 Default: '&lt;&lt;' (<<)
633 Default: '&lt;&lt;' (<<)
641
634
642 symbol_last
635 symbol_last
643 String to be displayed as the text for the $link_last link above.
636 String to be displayed as the text for the $link_last link above.
644
637
645 Default: '&gt;&gt;' (>>)
638 Default: '&gt;&gt;' (>>)
646
639
647 symbol_previous
640 symbol_previous
648 String to be displayed as the text for the $link_previous link above.
641 String to be displayed as the text for the $link_previous link above.
649
642
650 Default: '&lt;' (<)
643 Default: '&lt;' (<)
651
644
652 symbol_next
645 symbol_next
653 String to be displayed as the text for the $link_next link above.
646 String to be displayed as the text for the $link_next link above.
654
647
655 Default: '&gt;' (>)
648 Default: '&gt;' (>)
656
649
657 separator:
650 separator:
658 String that is used to separate page links/numbers in the above range of pages.
651 String that is used to separate page links/numbers in the above range of pages.
659
652
660 Default: ' '
653 Default: ' '
661
654
662 show_if_single_page:
655 show_if_single_page:
663 if True the navigator will be shown even if there is only one page.
656 if True the navigator will be shown even if there is only one page.
664
657
665 Default: False
658 Default: False
666
659
667 link_attr (optional)
660 link_attr (optional)
668 A dictionary of attributes that get added to A-HREF links pointing to other pages. Can
661 A dictionary of attributes that get added to A-HREF links pointing to other pages. Can
669 be used to define a CSS style or class to customize the look of links.
662 be used to define a CSS style or class to customize the look of links.
670
663
671 Example: { 'style':'border: 1px solid green' }
664 Example: { 'style':'border: 1px solid green' }
672 Example: { 'class':'pager_link' }
665 Example: { 'class':'pager_link' }
673
666
674 curpage_attr (optional)
667 curpage_attr (optional)
675 A dictionary of attributes that get added to the current page number in the pager (which
668 A dictionary of attributes that get added to the current page number in the pager (which
676 is obviously not a link). If this dictionary is not empty then the elements will be
669 is obviously not a link). If this dictionary is not empty then the elements will be
677 wrapped in a SPAN tag with the given attributes.
670 wrapped in a SPAN tag with the given attributes.
678
671
679 Example: { 'style':'border: 3px solid blue' }
672 Example: { 'style':'border: 3px solid blue' }
680 Example: { 'class':'pager_curpage' }
673 Example: { 'class':'pager_curpage' }
681
674
682 dotdot_attr (optional)
675 dotdot_attr (optional)
683 A dictionary of attributes that get added to the '..' string in the pager (which is
676 A dictionary of attributes that get added to the '..' string in the pager (which is
684 obviously not a link). If this dictionary is not empty then the elements will be wrapped
677 obviously not a link). If this dictionary is not empty then the elements will be wrapped
685 in a SPAN tag with the given attributes.
678 in a SPAN tag with the given attributes.
686
679
687 Example: { 'style':'color: #808080' }
680 Example: { 'style':'color: #808080' }
688 Example: { 'class':'pager_dotdot' }
681 Example: { 'class':'pager_dotdot' }
689 """
682 """
690 link_attr = link_attr or {}
683 link_attr = link_attr or {}
691 curpage_attr = curpage_attr or {}
684 curpage_attr = curpage_attr or {}
692 dotdot_attr = dotdot_attr or {}
685 dotdot_attr = dotdot_attr or {}
693 self.curpage_attr = curpage_attr
686 self.curpage_attr = curpage_attr
694 self.separator = separator
687 self.separator = separator
695 self.link_attr = link_attr
688 self.link_attr = link_attr
696 self.dotdot_attr = dotdot_attr
689 self.dotdot_attr = dotdot_attr
697 self.url = url
690 self.url = url
698
691
699 regex_res = re.search(r"~(\d+)~", tmpl_format)
692 regex_res = re.search(r"~(\d+)~", tmpl_format)
700 if regex_res:
693 if regex_res:
701 radius = regex_res.group(1)
694 radius = regex_res.group(1)
702 else:
695 else:
703 radius = 2
696 radius = 2
704
697
705 self.radius = int(radius)
698 self.radius = int(radius)
706
699
707 # Compute the first and last page number within the radius
700 # Compute the first and last page number within the radius
708 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
701 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
709 # -> leftmost_page = 5
702 # -> leftmost_page = 5
710 # -> rightmost_page = 9
703 # -> rightmost_page = 9
711 leftmost_page, rightmost_page = self._get_edges(
704 leftmost_page, rightmost_page = self._get_edges(
712 self.page, self.last_page, (self.radius * 2) + 1)
705 self.page, self.last_page, (self.radius * 2) + 1)
713
706
714 nav_items = {
707 nav_items = {
715 "first_page": None,
708 "first_page": None,
716 "last_page": None,
709 "last_page": None,
717 "previous_page": None,
710 "previous_page": None,
718 "next_page": None,
711 "next_page": None,
719 "current_page": None,
712 "current_page": None,
720 "radius": self.radius,
713 "radius": self.radius,
721 "range_pages": [],
714 "range_pages": [],
722 }
715 }
723
716
724 if leftmost_page is None or rightmost_page is None:
717 if leftmost_page is None or rightmost_page is None:
725 return nav_items
718 return nav_items
726
719
727 nav_items["first_page"] = {
720 nav_items["first_page"] = {
728 "type": "first_page",
721 "type": "first_page",
729 "value": str(symbol_first),
722 "value": str(symbol_first),
730 "attrs": self.link_attr,
723 "attrs": self.link_attr,
731 "number": self.first_page,
724 "number": self.first_page,
732 "href": self.url_maker(self.first_page),
725 "href": self.url_maker(self.first_page),
733 }
726 }
734
727
735 # Insert dots if there are pages between the first page
728 # Insert dots if there are pages between the first page
736 # and the currently displayed page range
729 # and the currently displayed page range
737 if leftmost_page - self.first_page > 1:
730 if leftmost_page - self.first_page > 1:
738 # Wrap in a SPAN tag if dotdot_attr is set
731 # Wrap in a SPAN tag if dotdot_attr is set
739 nav_items["range_pages"].append(
732 nav_items["range_pages"].append(
740 {
733 {
741 "type": "span",
734 "type": "span",
742 "value": "..",
735 "value": "..",
743 "attrs": self.dotdot_attr,
736 "attrs": self.dotdot_attr,
744 "href": "",
737 "href": "",
745 "number": None,
738 "number": None,
746 }
739 }
747 )
740 )
748
741
749 for this_page in range(leftmost_page, rightmost_page + 1):
742 for this_page in range(leftmost_page, rightmost_page + 1):
750 # Highlight the current page number and do not use a link
743 # Highlight the current page number and do not use a link
751 if this_page == self.page:
744 if this_page == self.page:
752 # Wrap in a SPAN tag if curpage_attr is set
745 # Wrap in a SPAN tag if curpage_attr is set
753 nav_items["range_pages"].append(
746 nav_items["range_pages"].append(
754 {
747 {
755 "type": "current_page",
748 "type": "current_page",
756 "value": str(this_page),
749 "value": str(this_page),
757 "number": this_page,
750 "number": this_page,
758 "attrs": self.curpage_attr,
751 "attrs": self.curpage_attr,
759 "href": self.url_maker(this_page),
752 "href": self.url_maker(this_page),
760 }
753 }
761 )
754 )
762 nav_items["current_page"] = {
755 nav_items["current_page"] = {
763 "value": this_page,
756 "value": this_page,
764 "attrs": self.curpage_attr,
757 "attrs": self.curpage_attr,
765 "type": "current_page",
758 "type": "current_page",
766 "href": self.url_maker(this_page),
759 "href": self.url_maker(this_page),
767 }
760 }
768 # Otherwise create just a link to that page
761 # Otherwise create just a link to that page
769 else:
762 else:
770 nav_items["range_pages"].append(
763 nav_items["range_pages"].append(
771 {
764 {
772 "type": "page",
765 "type": "page",
773 "value": str(this_page),
766 "value": str(this_page),
774 "number": this_page,
767 "number": this_page,
775 "attrs": self.link_attr,
768 "attrs": self.link_attr,
776 "href": self.url_maker(this_page),
769 "href": self.url_maker(this_page),
777 }
770 }
778 )
771 )
779
772
780 # Insert dots if there are pages between the displayed
773 # Insert dots if there are pages between the displayed
781 # page numbers and the end of the page range
774 # page numbers and the end of the page range
782 if self.last_page - rightmost_page > 1:
775 if self.last_page - rightmost_page > 1:
783 # Wrap in a SPAN tag if dotdot_attr is set
776 # Wrap in a SPAN tag if dotdot_attr is set
784 nav_items["range_pages"].append(
777 nav_items["range_pages"].append(
785 {
778 {
786 "type": "span",
779 "type": "span",
787 "value": "..",
780 "value": "..",
788 "attrs": self.dotdot_attr,
781 "attrs": self.dotdot_attr,
789 "href": "",
782 "href": "",
790 "number": None,
783 "number": None,
791 }
784 }
792 )
785 )
793
786
794 # Create a link to the very last page (unless we are on the last
787 # Create a link to the very last page (unless we are on the last
795 # page or there would be no need to insert '..' spacers)
788 # page or there would be no need to insert '..' spacers)
796 nav_items["last_page"] = {
789 nav_items["last_page"] = {
797 "type": "last_page",
790 "type": "last_page",
798 "value": str(symbol_last),
791 "value": str(symbol_last),
799 "attrs": self.link_attr,
792 "attrs": self.link_attr,
800 "href": self.url_maker(self.last_page),
793 "href": self.url_maker(self.last_page),
801 "number": self.last_page,
794 "number": self.last_page,
802 }
795 }
803
796
804 nav_items["previous_page"] = {
797 nav_items["previous_page"] = {
805 "type": "previous_page",
798 "type": "previous_page",
806 "value": str(symbol_previous),
799 "value": str(symbol_previous),
807 "attrs": self.link_attr,
800 "attrs": self.link_attr,
808 "number": self.previous_page or self.first_page,
801 "number": self.previous_page or self.first_page,
809 "href": self.url_maker(self.previous_page or self.first_page),
802 "href": self.url_maker(self.previous_page or self.first_page),
810 }
803 }
811
804
812 nav_items["next_page"] = {
805 nav_items["next_page"] = {
813 "type": "next_page",
806 "type": "next_page",
814 "value": str(symbol_next),
807 "value": str(symbol_next),
815 "attrs": self.link_attr,
808 "attrs": self.link_attr,
816 "number": self.next_page or self.last_page,
809 "number": self.next_page or self.last_page,
817 "href": self.url_maker(self.next_page or self.last_page),
810 "href": self.url_maker(self.next_page or self.last_page),
818 }
811 }
819
812
820 return nav_items
813 return nav_items
821
814
822 def _range(self, link_map, radius):
815 def _range(self, link_map, radius):
823 """
816 """
824 Return range of linked pages to substitute placeholder in pattern
817 Return range of linked pages to substitute placeholder in pattern
825 """
818 """
826 # Compute the first and last page number within the radius
819 # Compute the first and last page number within the radius
827 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
820 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
828 # -> leftmost_page = 5
821 # -> leftmost_page = 5
829 # -> rightmost_page = 9
822 # -> rightmost_page = 9
830 leftmost_page, rightmost_page = self._get_edges(
823 leftmost_page, rightmost_page = self._get_edges(
831 self.page, self.last_page, (radius * 2) + 1)
824 self.page, self.last_page, (radius * 2) + 1)
832
825
833 nav_items = []
826 nav_items = []
834 # Create a link to the first page (unless we are on the first page
827 # Create a link to the first page (unless we are on the first page
835 # or there would be no need to insert '..' spacers)
828 # or there would be no need to insert '..' spacers)
836 if self.first_page and self.page != self.first_page and self.first_page < leftmost_page:
829 if self.first_page and self.page != self.first_page and self.first_page < leftmost_page:
837 page = link_map["first_page"].copy()
830 page = link_map["first_page"].copy()
838 page["value"] = str(page["number"])
831 page["value"] = str(page["number"])
839 nav_items.append(self.link_tag(page))
832 nav_items.append(self.link_tag(page))
840
833
841 for item in link_map["range_pages"]:
834 for item in link_map["range_pages"]:
842 nav_items.append(self.link_tag(item))
835 nav_items.append(self.link_tag(item))
843
836
844 # Create a link to the very last page (unless we are on the last
837 # Create a link to the very last page (unless we are on the last
845 # page or there would be no need to insert '..' spacers)
838 # page or there would be no need to insert '..' spacers)
846 if self.last_page and self.page != self.last_page and rightmost_page < self.last_page:
839 if self.last_page and self.page != self.last_page and rightmost_page < self.last_page:
847 page = link_map["last_page"].copy()
840 page = link_map["last_page"].copy()
848 page["value"] = str(page["number"])
841 page["value"] = str(page["number"])
849 nav_items.append(self.link_tag(page))
842 nav_items.append(self.link_tag(page))
850
843
851 return self.separator.join(nav_items)
844 return self.separator.join(nav_items)
852
845
853 def _default_url_maker(self, page_number):
846 def _default_url_maker(self, page_number):
854 if self.url is None:
847 if self.url is None:
855 raise Exception(
848 raise Exception(
856 "You need to specify a 'url' parameter containing a '$page' placeholder."
849 "You need to specify a 'url' parameter containing a '$page' placeholder."
857 )
850 )
858
851
859 if "$page" not in self.url:
852 if "$page" not in self.url:
860 raise Exception("The 'url' parameter must contain a '$page' placeholder.")
853 raise Exception("The 'url' parameter must contain a '$page' placeholder.")
861
854
862 return self.url.replace("$page", str(page_number))
855 return self.url.replace("$page", str(page_number))
863
856
864 @staticmethod
857 @staticmethod
865 def default_link_tag(item):
858 def default_link_tag(item):
866 """
859 """
867 Create an A-HREF tag that points to another page.
860 Create an A-HREF tag that points to another page.
868 """
861 """
869 text = item["value"]
862 text = item["value"]
870 target_url = item["href"]
863 target_url = item["href"]
871
864
872 if not item["href"] or item["type"] in ("span", "current_page"):
865 if not item["href"] or item["type"] in ("span", "current_page"):
873 if item["attrs"]:
866 if item["attrs"]:
874 text = make_html_tag("span", **item["attrs"]) + text + "</span>"
867 text = make_html_tag("span", **item["attrs"]) + text + "</span>"
875 return text
868 return text
876
869
877 return make_html_tag("a", text=text, href=target_url, **item["attrs"])
870 return make_html_tag("a", text=text, href=target_url, **item["attrs"])
878
871
879 # Below is RhodeCode custom code
872 # Below is RhodeCode custom code
880
873
881 # Copyright (C) 2010-2020 RhodeCode GmbH
874 # Copyright (C) 2010-2020 RhodeCode GmbH
882 #
875 #
883 # This program is free software: you can redistribute it and/or modify
876 # This program is free software: you can redistribute it and/or modify
884 # it under the terms of the GNU Affero General Public License, version 3
877 # it under the terms of the GNU Affero General Public License, version 3
885 # (only), as published by the Free Software Foundation.
878 # (only), as published by the Free Software Foundation.
886 #
879 #
887 # This program is distributed in the hope that it will be useful,
880 # This program is distributed in the hope that it will be useful,
888 # but WITHOUT ANY WARRANTY; without even the implied warranty of
881 # but WITHOUT ANY WARRANTY; without even the implied warranty of
889 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
882 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
890 # GNU General Public License for more details.
883 # GNU General Public License for more details.
891 #
884 #
892 # You should have received a copy of the GNU Affero General Public License
885 # You should have received a copy of the GNU Affero General Public License
893 # along with this program. If not, see <http://www.gnu.org/licenses/>.
886 # along with this program. If not, see <http://www.gnu.org/licenses/>.
894 #
887 #
895 # This program is dual-licensed. If you wish to learn more about the
888 # This program is dual-licensed. If you wish to learn more about the
896 # RhodeCode Enterprise Edition, including its added features, Support services,
889 # RhodeCode Enterprise Edition, including its added features, Support services,
897 # and proprietary license terms, please see https://rhodecode.com/licenses/
890 # and proprietary license terms, please see https://rhodecode.com/licenses/
898
891
899
892
900 PAGE_FORMAT = '$link_previous ~3~ $link_next'
893 PAGE_FORMAT = '$link_previous ~3~ $link_next'
901
894
902
895
903 class SqlalchemyOrmWrapper(object):
896 class SqlalchemyOrmWrapper(object):
904 """Wrapper class to access elements of a collection."""
897 """Wrapper class to access elements of a collection."""
905
898
906 def __init__(self, pager, collection):
899 def __init__(self, pager, collection):
907 self.pager = pager
900 self.pager = pager
908 self.collection = collection
901 self.collection = collection
909
902
910 def __getitem__(self, range):
903 def __getitem__(self, key):
904 if not isinstance(key, slice):
905 raise ValueError('Pagination without a slice not supported')
906
911 # Return a range of objects of an sqlalchemy.orm.query.Query object
907 # Return a range of objects of an sqlalchemy.orm.query.Query object
912 return self.collection[range]
908 return self.collection[key]
913
909
914 def __len__(self):
910 def __len__(self):
915 # support empty types, without actually making a query.
911 # support empty types, without actually making a query.
916 if self.collection is None or self.collection == []:
912 if self.collection is None or self.collection == []:
917 return 0
913 return 0
918
914
919 # Count the number of objects in an sqlalchemy.orm.query.Query object
915 # Count the number of objects in an sqlalchemy.orm.query.Query object
920 return self.collection.count()
916 return self.collection.count()
921
917
922
918
923 class CustomPager(_Page):
919 class CustomPager(_Page):
924
920
925 @staticmethod
921 @staticmethod
926 def disabled_link_tag(item):
922 def disabled_link_tag(item):
927 """
923 """
928 Create an A-HREF tag that is disabled
924 Create an A-HREF tag that is disabled
929 """
925 """
930 text = item['value']
926 text = item['value']
931 attrs = item['attrs'].copy()
927 attrs = item['attrs'].copy()
932 attrs['class'] = 'disabled ' + attrs['class']
928 attrs['class'] = 'disabled ' + attrs['class']
933
929
934 return make_html_tag('a', text=text, **attrs)
930 return make_html_tag('a', text=text, **attrs)
935
931
936 def render(self):
932 def render(self):
937 # Don't show navigator if there is no more than one page
933 # Don't show navigator if there is no more than one page
938 if self.page_count == 0:
934 if self.page_count == 0:
939 return ""
935 return ""
940
936
941 self.link_tag = self.default_link_tag
937 self.link_tag = self.default_link_tag
942
938
943 link_map = self.link_map(
939 link_map = self.link_map(
944 tmpl_format=PAGE_FORMAT, url=None,
940 tmpl_format=PAGE_FORMAT, url=None,
945 show_if_single_page=False, separator=' ',
941 show_if_single_page=False, separator=' ',
946 symbol_first='<<', symbol_last='>>',
942 symbol_first='<<', symbol_last='>>',
947 symbol_previous='<', symbol_next='>',
943 symbol_previous='<', symbol_next='>',
948 link_attr={'class': 'pager_link'},
944 link_attr={'class': 'pager_link'},
949 curpage_attr={'class': 'pager_curpage'},
945 curpage_attr={'class': 'pager_curpage'},
950 dotdot_attr={'class': 'pager_dotdot'})
946 dotdot_attr={'class': 'pager_dotdot'})
951
947
952 links_markup = self._range(link_map, self.radius)
948 links_markup = self._range(link_map, self.radius)
953
949
954 link_first = (
950 link_first = (
955 self.page > self.first_page and self.link_tag(link_map['first_page']) or ''
951 self.page > self.first_page and self.link_tag(link_map['first_page']) or ''
956 )
952 )
957 link_last = (
953 link_last = (
958 self.page < self.last_page and self.link_tag(link_map['last_page']) or ''
954 self.page < self.last_page and self.link_tag(link_map['last_page']) or ''
959 )
955 )
960
956
961 link_previous = (
957 link_previous = (
962 self.previous_page and self.link_tag(link_map['previous_page'])
958 self.previous_page and self.link_tag(link_map['previous_page'])
963 or self.disabled_link_tag(link_map['previous_page'])
959 or self.disabled_link_tag(link_map['previous_page'])
964 )
960 )
965 link_next = (
961 link_next = (
966 self.next_page and self.link_tag(link_map['next_page'])
962 self.next_page and self.link_tag(link_map['next_page'])
967 or self.disabled_link_tag(link_map['next_page'])
963 or self.disabled_link_tag(link_map['next_page'])
968 )
964 )
969
965
970 # Interpolate '$' variables
966 # Interpolate '$' variables
971 # Replace ~...~ in token tmpl_format by range of pages
967 # Replace ~...~ in token tmpl_format by range of pages
972 result = re.sub(r"~(\d+)~", links_markup, PAGE_FORMAT)
968 result = re.sub(r"~(\d+)~", links_markup, PAGE_FORMAT)
973 result = Template(result).safe_substitute(
969 result = Template(result).safe_substitute(
974 {
970 {
975 "links": links_markup,
971 "links": links_markup,
976 "first_page": self.first_page,
972 "first_page": self.first_page,
977 "last_page": self.last_page,
973 "last_page": self.last_page,
978 "page": self.page,
974 "page": self.page,
979 "page_count": self.page_count,
975 "page_count": self.page_count,
980 "items_per_page": self.items_per_page,
976 "items_per_page": self.items_per_page,
981 "first_item": self.first_item,
977 "first_item": self.first_item,
982 "last_item": self.last_item,
978 "last_item": self.last_item,
983 "item_count": self.item_count,
979 "item_count": self.item_count,
984 "link_first": link_first,
980 "link_first": link_first,
985 "link_last": link_last,
981 "link_last": link_last,
986 "link_previous": link_previous,
982 "link_previous": link_previous,
987 "link_next": link_next,
983 "link_next": link_next,
988 }
984 }
989 )
985 )
990
986
991 return literal(result)
987 return literal(result)
992
988
993
989
994 class Page(CustomPager):
990 class Page(CustomPager):
995 """
991 """
996 Custom pager to match rendering style with paginator
992 Custom pager to match rendering style with paginator
997 """
993 """
998
994
999 def __init__(self, collection, page=1, items_per_page=20, item_count=None,
995 def __init__(self, collection, page=1, items_per_page=20, item_count=None,
1000 url_maker=None, **kwargs):
996 url_maker=None, **kwargs):
1001 """
997 """
1002 Special type of pager. We intercept collection to wrap it in our custom
998 Special type of pager. We intercept collection to wrap it in our custom
1003 logic instead of using wrapper_class
999 logic instead of using wrapper_class
1004 """
1000 """
1005
1001
1006 super(Page, self).__init__(collection=collection, page=page,
1002 super(Page, self).__init__(collection=collection, page=page,
1007 items_per_page=items_per_page, item_count=item_count,
1003 items_per_page=items_per_page, item_count=item_count,
1008 wrapper_class=None, url_maker=url_maker, **kwargs)
1004 wrapper_class=None, url_maker=url_maker, **kwargs)
1009
1005
1010
1006
1011 class SqlPage(CustomPager):
1007 class SqlPage(CustomPager):
1012 """
1008 """
1013 Custom pager to match rendering style with paginator
1009 Custom pager to match rendering style with paginator
1014 """
1010 """
1015
1011
1016 def __init__(self, collection, page=1, items_per_page=20, item_count=None,
1012 def __init__(self, collection, page=1, items_per_page=20, item_count=None,
1017 url_maker=None, **kwargs):
1013 url_maker=None, **kwargs):
1018 """
1014 """
1019 Special type of pager. We intercept collection to wrap it in our custom
1015 Special type of pager. We intercept collection to wrap it in our custom
1020 logic instead of using wrapper_class
1016 logic instead of using wrapper_class
1021 """
1017 """
1022 collection = SqlalchemyOrmWrapper(self, collection)
1018 collection = SqlalchemyOrmWrapper(self, collection)
1023
1019
1024 super(SqlPage, self).__init__(collection=collection, page=page,
1020 super(SqlPage, self).__init__(collection=collection, page=page,
1025 items_per_page=items_per_page, item_count=item_count,
1021 items_per_page=items_per_page, item_count=item_count,
1026 wrapper_class=None, url_maker=url_maker, **kwargs)
1022 wrapper_class=None, url_maker=url_maker, **kwargs)
1027
1023
1028
1024
1029 class RepoCommitsWrapper(object):
1025 class RepoCommitsWrapper(object):
1030 """Wrapper class to access elements of a collection."""
1026 """Wrapper class to access elements of a collection."""
1031
1027
1032 def __init__(self, pager, collection):
1028 def __init__(self, pager, collection):
1033 self.pager = pager
1029 self.pager = pager
1034 self.collection = collection
1030 self.collection = collection
1035
1031
1036 def __getitem__(self, range):
1032 def __getitem__(self, key):
1037 cur_page = self.pager.page
1033 cur_page = self.pager.page
1038 items_per_page = self.pager.items_per_page
1034 items_per_page = self.pager.items_per_page
1039 first_item = max(0, (len(self.collection) - (cur_page * items_per_page)))
1035 first_item = max(0, (len(self.collection) - (cur_page * items_per_page)))
1040 last_item = ((len(self.collection) - 1) - items_per_page * (cur_page - 1))
1036 last_item = ((len(self.collection) - 1) - items_per_page * (cur_page - 1))
1041 return reversed(list(self.collection[first_item:last_item + 1]))
1037 return reversed(list(self.collection[first_item:last_item + 1]))
1042
1038
1043 def __len__(self):
1039 def __len__(self):
1044 return len(self.collection)
1040 return len(self.collection)
1045
1041
1046
1042
1047 class RepoPage(CustomPager):
1043 class RepoPage(CustomPager):
1048 """
1044 """
1049 Create a "RepoPage" instance. special pager for paging repository
1045 Create a "RepoPage" instance. special pager for paging repository
1050 """
1046 """
1051
1047
1052 def __init__(self, collection, page=1, items_per_page=20, item_count=None,
1048 def __init__(self, collection, page=1, items_per_page=20, item_count=None,
1053 url_maker=None, **kwargs):
1049 url_maker=None, **kwargs):
1054 """
1050 """
1055 Special type of pager. We intercept collection to wrap it in our custom
1051 Special type of pager. We intercept collection to wrap it in our custom
1056 logic instead of using wrapper_class
1052 logic instead of using wrapper_class
1057 """
1053 """
1058 collection = RepoCommitsWrapper(self, collection)
1054 collection = RepoCommitsWrapper(self, collection)
1059 super(RepoPage, self).__init__(collection=collection, page=page,
1055 super(RepoPage, self).__init__(collection=collection, page=page,
1060 items_per_page=items_per_page, item_count=item_count,
1056 items_per_page=items_per_page, item_count=item_count,
1061 wrapper_class=None, url_maker=url_maker, **kwargs)
1057 wrapper_class=None, url_maker=url_maker, **kwargs)
General Comments 0
You need to be logged in to leave comments. Login now