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