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