[3] | 1 | """Pagination for Collections and ORMs |
---|
| 2 | |
---|
| 3 | The Pagination module aids in the process of paging large collections of |
---|
| 4 | objects. It can be used macro-style for automatic fetching of large collections |
---|
| 5 | using one of the ORM wrappers, or handle a large collection responding to |
---|
| 6 | standard Python list slicing operations. These methods can also be used |
---|
| 7 | individually and customized to do as much or little as desired. |
---|
| 8 | |
---|
| 9 | The Paginator itself maintains pagination logic associated with each page, where |
---|
| 10 | it begins, what the first/last item on the page is, etc. |
---|
| 11 | |
---|
| 12 | Helper functions hook-up the Paginator in more conveinent methods for the more |
---|
| 13 | macro-style approach to return the Paginator and the slice of the collection |
---|
| 14 | desired. |
---|
| 15 | |
---|
| 16 | """ |
---|
| 17 | from routes import request_config |
---|
| 18 | from orm import get_wrapper |
---|
| 19 | |
---|
| 20 | def paginate(collection, page=None, per_page=10, item_count=None, *args, **options): |
---|
| 21 | """Paginate a collection of data |
---|
| 22 | |
---|
| 23 | If the collection is a list, it will return the slice of the list along |
---|
| 24 | with the Paginator object. If the collection is given using an ORM, the |
---|
| 25 | collection argument must be a partial representing the function to be |
---|
| 26 | used that will generate the proper query and extend properly for the |
---|
| 27 | limit/offset. |
---|
| 28 | |
---|
| 29 | Example:: |
---|
| 30 | |
---|
| 31 | # In this case, Person is a SQLObject class, or it could be a list/tuple |
---|
| 32 | person_paginator, person_set = paginate(Person, page=1) |
---|
| 33 | |
---|
| 34 | set_count = int(person_paginator.current) |
---|
| 35 | total_pages = len(person_paginator) |
---|
| 36 | |
---|
| 37 | Current ORM support is limited to SQLObject and SQLAlchemy. You can use any ORM |
---|
| 38 | you'd like with the Paginator as it will give you the offset/limit data necessary |
---|
| 39 | to make your own query. |
---|
| 40 | |
---|
| 41 | **WARNING:** Unless you pass in an item_count, a count will be performed on the |
---|
| 42 | collection every time paginate is called. If using an ORM, it's suggested that |
---|
| 43 | you count the items yourself and/or cache them. |
---|
| 44 | |
---|
| 45 | """ |
---|
| 46 | collection = get_wrapper(collection, *args, **options) |
---|
| 47 | if not item_count: |
---|
| 48 | item_count = len(collection) |
---|
| 49 | paginator = Paginator(item_count, per_page, page) |
---|
| 50 | subset = collection[paginator.current.first_item:paginator.current.last_item] |
---|
| 51 | |
---|
| 52 | return paginator, subset |
---|
| 53 | |
---|
| 54 | |
---|
| 55 | class Paginator(object): |
---|
| 56 | """Tracks paginated sets of data, and supplies common pagination operations |
---|
| 57 | |
---|
| 58 | The Paginator tracks data associated with pagination of groups of data, as well |
---|
| 59 | as supplying objects and methods that make dealing with paginated results easier. |
---|
| 60 | |
---|
| 61 | A Paginator supports list operations, including item fetching, length, iteration, |
---|
| 62 | and the 'in' operation. Each item in the Paginator is a Page object representing |
---|
| 63 | data about that specific page in the set of paginated data. As with the standard |
---|
| 64 | Python list, the Paginator list index starts at 0. |
---|
| 65 | |
---|
| 66 | """ |
---|
| 67 | def __init__(self, item_count, items_per_page=10, current_page=0): |
---|
| 68 | """Initialize a Paginator with the item count specified.""" |
---|
| 69 | self.item_count = item_count |
---|
| 70 | self.items_per_page = items_per_page |
---|
| 71 | self.pages = {} |
---|
| 72 | self.current_page = current_page |
---|
| 73 | |
---|
| 74 | def current(): |
---|
| 75 | doc = """\ |
---|
| 76 | Page object currently being displayed |
---|
| 77 | |
---|
| 78 | When assigning to the current page, it will set the page number for this page |
---|
| 79 | and create it if needed. If the page is a Page object and does not belong to |
---|
| 80 | this paginator, an AttributeError will be raised. |
---|
| 81 | |
---|
| 82 | """ |
---|
| 83 | def fget(self): |
---|
| 84 | return self[int(self.current_page)] |
---|
| 85 | def fset(self, page): |
---|
| 86 | if isinstance(page, Page) and page.paginator != self: |
---|
| 87 | raise AttributeError("Page/Paginator mismatch") |
---|
| 88 | page = int(page) |
---|
| 89 | self.current_page = page in self and page or 0 |
---|
| 90 | return locals() |
---|
| 91 | current = property(**current()) |
---|
| 92 | |
---|
| 93 | def __len__(self): |
---|
| 94 | return (self.item_count == 0) and 0 or (((self.item_count - 1)//self.items_per_page) + 1) |
---|
| 95 | |
---|
| 96 | def __iter__(self): |
---|
| 97 | for i in range(0, len(self)): |
---|
| 98 | yield self[i] |
---|
| 99 | |
---|
| 100 | def __getitem__(self, index): |
---|
| 101 | # Handle negative indexing like a normal list |
---|
| 102 | if index < 0: |
---|
| 103 | index = len(self) + index |
---|
| 104 | |
---|
| 105 | if index < 0: |
---|
| 106 | index = 0 |
---|
| 107 | |
---|
| 108 | if index not in self and index != 0: |
---|
| 109 | raise IndexError, "list index out of range" |
---|
| 110 | |
---|
| 111 | return self.pages.setdefault(index, Page(self, index)) |
---|
| 112 | |
---|
| 113 | def __contains__(self, value): |
---|
| 114 | if value >= 0 and value <= (len(self) - 1): |
---|
| 115 | return True |
---|
| 116 | return False |
---|
| 117 | |
---|
| 118 | class Page(object): |
---|
| 119 | """Represents a single page from a paginated set.""" |
---|
| 120 | def __init__(self, paginator, number): |
---|
| 121 | """Creates a new Page for the given ``paginator`` with the index ``number``.""" |
---|
| 122 | self.paginator = paginator |
---|
| 123 | self.number = int(number) |
---|
| 124 | |
---|
| 125 | def __int__(self): |
---|
| 126 | return self.number |
---|
| 127 | |
---|
| 128 | def __eq__(self, page): |
---|
| 129 | return self.paginator == page.paginator and self.number == page.number |
---|
| 130 | |
---|
| 131 | def __cmp__(self, page): |
---|
| 132 | return cmp(self.number, page.number) |
---|
| 133 | |
---|
| 134 | def offset(): |
---|
| 135 | doc = """Offset of the page, useful for database queries.""" |
---|
| 136 | def fget(self): |
---|
| 137 | return self.paginator.items_per_page * self.number |
---|
| 138 | return locals() |
---|
| 139 | offset = property(**offset()) |
---|
| 140 | |
---|
| 141 | def first_item(): |
---|
| 142 | doc = """The number of the first item in the page.""" |
---|
| 143 | def fget(self): |
---|
| 144 | return self.offset |
---|
| 145 | return locals() |
---|
| 146 | first_item = property(**first_item()) |
---|
| 147 | |
---|
| 148 | def last_item(): |
---|
| 149 | doc = """The number of the last item in the page.""" |
---|
| 150 | def fget(self): |
---|
| 151 | return min(self.paginator.items_per_page * (self.number + 1), |
---|
| 152 | self.paginator.item_count) |
---|
| 153 | return locals() |
---|
| 154 | last_item = property(**last_item()) |
---|
| 155 | |
---|
| 156 | def first(): |
---|
| 157 | doc = """Boolean indiciating if this page is the first.""" |
---|
| 158 | def fget(self): |
---|
| 159 | return self == self.paginator[0] |
---|
| 160 | return locals() |
---|
| 161 | first = property(**first()) |
---|
| 162 | |
---|
| 163 | def last(): |
---|
| 164 | doc = """Boolean indicating if this page is the last.""" |
---|
| 165 | def fget(self): |
---|
| 166 | return self == self.paginator[-1] |
---|
| 167 | return locals() |
---|
| 168 | last = property(**last()) |
---|
| 169 | |
---|
| 170 | def previous(): |
---|
| 171 | doc = """Previous page if it exists, None otherwise.""" |
---|
| 172 | def fget(self): |
---|
| 173 | if self.first: |
---|
| 174 | return None |
---|
| 175 | return self.paginator[self.number - 1] |
---|
| 176 | return locals() |
---|
| 177 | previous = property(**previous()) |
---|
| 178 | |
---|
| 179 | def next(): |
---|
| 180 | doc = """Next page if it exists, None otherwise.""" |
---|
| 181 | def fget(self): |
---|
| 182 | if self.last: |
---|
| 183 | return None |
---|
| 184 | return self.paginator[self.number + 1] |
---|
| 185 | return locals() |
---|
| 186 | next = property(**next()) |
---|
| 187 | |
---|
| 188 | def window(self, padding = 2): |
---|
| 189 | return Window(self, padding) |
---|
| 190 | |
---|
| 191 | def __repr__(self): |
---|
| 192 | return str(self.number) |
---|
| 193 | |
---|
| 194 | class Window(object): |
---|
| 195 | """Represents ranges around a given page.""" |
---|
| 196 | def __init__(self, page, padding = 2): |
---|
| 197 | """Creates a new Window object for the given ``page`` with the specified ``padding``.""" |
---|
| 198 | self.paginator = page.paginator |
---|
| 199 | self.page = page |
---|
| 200 | self.padding = padding |
---|
| 201 | |
---|
| 202 | def padding(): |
---|
| 203 | doc = """Sets the window's padding (the number of pages on either side of the window page).""" |
---|
| 204 | def fset(self, padding): |
---|
| 205 | self._padding = padding |
---|
| 206 | if padding < 0: self._padding = 0 |
---|
| 207 | first_page_in_window = self.page.number - self._padding |
---|
| 208 | self.first = first_page_in_window in self.paginator and ( |
---|
| 209 | self.paginator[first_page_in_window]) or self.paginator[0] |
---|
| 210 | last_page_in_window = self.page.number + self._padding |
---|
| 211 | self.last = last_page_in_window in self.paginator and ( |
---|
| 212 | self.paginator[last_page_in_window]) or self.paginator[-1] |
---|
| 213 | def fget(self): |
---|
| 214 | return self._padding |
---|
| 215 | return locals() |
---|
| 216 | padding = property(**padding()) |
---|
| 217 | |
---|
| 218 | def pages(): |
---|
| 219 | doc = """Returns a list of Page objects in the current window.""" |
---|
| 220 | def fget(self): |
---|
| 221 | return [self.paginator[page_number] for page_number in |
---|
| 222 | range(self.first.number, self.last.number+1)] |
---|
| 223 | return locals() |
---|
| 224 | pages = property(**pages()) |
---|
| 225 | |
---|
| 226 | def __add__(self, window): |
---|
| 227 | if window.paginator != self.paginator: |
---|
| 228 | raise AttributeError("Window/paginator mismatch") |
---|
| 229 | assert self.last >= window.first |
---|
| 230 | return Window(self.page.next, padding=self.padding+1) |
---|