0001"""Pagination for Collections and ORMs
0002
0003The Pagination module aids in the process of paging large collections of
0004objects. It can be used macro-style for automatic fetching of large collections
0005using one of the ORM wrappers, or handle a large collection responding to
0006standard Python list slicing operations. These methods can also be used
0007individually and customized to do as much or little as desired.
0008
0009The Paginator itself maintains pagination logic associated with each page,
0010where begins, what the first/last item on the page is, etc.
0011
0012Helper functions hook-up the Paginator in more conveinent methods for the more
0013macro-style approach to return the Paginator and the slice of the collection
0014desired.
0015
0016"""
0017import re
0018
0019from routes import request_config
0020from orm import get_wrapper
0021
0022find_page = re.compile('page=(\d+)', re.I)
0023
0024def paginate(collection, page=None, per_page=10, item_count=None,
0025 query_args=None, **options):
0026 """Paginate a collection of data
0027
0028 If the collection is a list, it will return the slice of the list along
0029 with the Paginator object. If the collection is given using an ORM, the
0030 collection argument must be a partial representing the function to be
0031 used that will generate the proper query and extend properly for the
0032 limit/offset.
0033
0034 query_args will be passed to the partial and is for use in generating
0035 limiting conditions that your collection object may take, the remaining
0036 unused keyword arguments will also be passed into the collection object.
0037
0038 Example::
0039
0040 # In this case, Person is a SQLObject class, or it could be a
0041 # list/tuple
0042 person_paginator, person_set = paginate(Person, page=1)
0043
0044 set_count = int(person_paginator.current)
0045 total_pages = len(person_paginator)
0046
0047 Current ORM support is limited to SQLObject and SQLAlchemy. You can use any
0048 ORM you'd like with the Paginator as it will give you the offset/limit
0049 data necessary to make your own query.
0050
0051 If you fail to pass in a page value, paginate will attempt to find a page
0052 value in the QUERY_STRING from environ, or the Routes match dict. This
0053 feature only works if routes was used to resolve the URL.
0054
0055 Example::
0056
0057 # Using an SQLAlchemy object with assign_mapper under Pylons
0058 # with an order_by passed in
0059 c.paginator, c.people = paginate(model.Person,
0060 order_by=[model.Person.c.date])
0061
0062 **WARNING:** Unless you pass in an item_count, a count will be performed
0063 on the collection every time paginate is called. If using an ORM, it's
0064 suggested that you count the items yourself and/or cache them.
0065 """
0066 # If our page wasn't passed in, attempt to pull out either a page arg from
0067 # the routes route path, or try the environ GET.
0068 if page is None:
0069 config = request_config()
0070 if hasattr(config, 'mapper_dict'):
0071 page = config.mapper_dict.get('page')
0072 if page is not None:
0073 if re.match(r'\d+', page):
0074 page = int(page)
0075 else:
0076 page = 0
0077 elif page is None and hasattr(config, 'environ'):
0078 page_match = re.match(find_page,
0079 config.environ.get('QUERY_STRING', ''))
0080 if page_match:
0081 page = int(page_match.groups()[0])
0082
0083 # If environ is set, and no page has been we will assume they wanted to
0084 # find a page value but didn't so we default to 0 now.
0085 if page is None:
0086 page = 0
0087
0088 if query_args is None:
0089 query_args = []
0090
0091 collection = get_wrapper(collection, *query_args, **options)
0092 if not item_count:
0093 item_count = len(collection)
0094 paginator = Paginator(item_count, per_page, page)
0095 if page < 0 or page >= len(paginator):
0096 subset = []
0097 else:
0098 subset = collection[paginator.current.first_item:paginator.current.last_item]
0099
0100 return paginator, subset
0101
0102
0103class Paginator(object):
0104 """Tracks paginated sets of data, and supplies common pagination operations
0105
0106 The Paginator tracks data associated with pagination of groups of data, as well
0107 as supplying objects and methods that make dealing with paginated results easier.
0108
0109 A Paginator supports list operations, including item fetching, length, iteration,
0110 and the 'in' operation. Each item in the Paginator is a Page object representing
0111 data about that specific page in the set of paginated data. As with the standard
0112 Python list, the Paginator list index starts at 0.
0113
0114 """
0115 def __init__(self, item_count, items_per_page=10, current_page=0):
0116 """Initialize a Paginator with the item count specified."""
0117 self.item_count = item_count
0118 self.items_per_page = items_per_page
0119 self.pages = {}
0120 self.current_page = current_page
0121
0122 def current():
0123 doc = """\
0124Page object currently being displayed
0125
0126When assigning to the current page, it will set the page number for this page
0127and create it if needed. If the page is a Page object and does not belong to
0128this paginator, an AttributeError will be raised.
0129
0130"""
0131 def fget(self):
0132 return self[int(self.current_page)]
0133 def fset(self, page):
0134 if isinstance(page, Page) and page.paginator != self:
0135 raise AttributeError("Page/Paginator mismatch")
0136 page = int(page)
0137 self.current_page = page in self and page or 0
0138 return locals()
0139 current = property(**current())
0140
0141 def __len__(self):
0142 if self.item_count == 0:
0143 return 0
0144 else:
0145 return ((self.item_count - 1)//self.items_per_page) + 1
0146
0147 def __iter__(self):
0148 for i in range(0, len(self)):
0149 yield self[i]
0150
0151 def __getitem__(self, index):
0152 # Handle negative indexing like a normal list
0153 if index < 0:
0154 index = len(self) + index
0155
0156 if index < 0:
0157 index = 0
0158
0159 if index not in self and index != 0:
0160 raise IndexError, "list index out of range"
0161
0162 return self.pages.setdefault(index, Page(self, index))
0163
0164 def __contains__(self, value):
0165 if value >= 0 and value <= (len(self) - 1):
0166 return True
0167 return False
0168
0169class Page(object):
0170 """Represents a single page from a paginated set."""
0171 def __init__(self, paginator, number):
0172 """Creates a new Page for the given ``paginator`` with the index ``number``."""
0173 self.paginator = paginator
0174 self.number = int(number)
0175
0176 def __int__(self):
0177 return self.number
0178
0179 def __eq__(self, page):
0180 return self.paginator == page.paginator and self.number == page.number
0181
0182 def __cmp__(self, page):
0183 return cmp(self.number, page.number)
0184
0185 def offset():
0186 doc = """Offset of the page, useful for database queries."""
0187 def fget(self):
0188 return self.paginator.items_per_page * self.number
0189 return locals()
0190 offset = property(**offset())
0191
0192 def first_item():
0193 doc = """The number of the first item in the page."""
0194 def fget(self):
0195 return self.offset
0196 return locals()
0197 first_item = property(**first_item())
0198
0199 def last_item():
0200 doc = """The number of the last item in the page."""
0201 def fget(self):
0202 return min(self.paginator.items_per_page * (self.number + 1),
0203 self.paginator.item_count)
0204 return locals()
0205 last_item = property(**last_item())
0206
0207 def first():
0208 doc = """Boolean indiciating if this page is the first."""
0209 def fget(self):
0210 return self == self.paginator[0]
0211 return locals()
0212 first = property(**first())
0213
0214 def last():
0215 doc = """Boolean indicating if this page is the last."""
0216 def fget(self):
0217 return self == self.paginator[-1]
0218 return locals()
0219 last = property(**last())
0220
0221 def previous():
0222 doc = """Previous page if it exists, None otherwise."""
0223 def fget(self):
0224 if self.first:
0225 return None
0226 return self.paginator[self.number - 1]
0227 return locals()
0228 previous = property(**previous())
0229
0230 def next():
0231 doc = """Next page if it exists, None otherwise."""
0232 def fget(self):
0233 if self.last:
0234 return None
0235 return self.paginator[self.number + 1]
0236 return locals()
0237 next = property(**next())
0238
0239 def window(self, padding = 2):
0240 return Window(self, padding)
0241
0242 def __repr__(self):
0243 return str(self.number)
0244
0245class Window(object):
0246 """Represents ranges around a given page."""
0247 def __init__(self, page, padding = 2):
0248 """Creates a new Window object for the given ``page`` with the specified ``padding``."""
0249 self.paginator = page.paginator
0250 self.page = page
0251 self.padding = padding
0252
0253 def padding():
0254 doc = """Sets the window's padding (the number of pages on either side of the window page)."""
0255 def fset(self, padding):
0256 self._padding = padding
0257 if padding < 0: self._padding = 0
0258 first_page_in_window = self.page.number - self._padding
0259 self.first = first_page_in_window in self.paginator and (
0260 self.paginator[first_page_in_window]) or self.paginator[0]
0261 last_page_in_window = self.page.number + self._padding
0262 self.last = last_page_in_window in self.paginator and (
0263 self.paginator[last_page_in_window]) or self.paginator[-1]
0264 def fget(self):
0265 return self._padding
0266 return locals()
0267 padding = property(**padding())
0268
0269 def pages():
0270 doc = """Returns a list of Page objects in the current window."""
0271 def fget(self):
0272 return [self.paginator[page_number] for page_number in
0273 range(self.first.number, self.last.number+1)]
0274 return locals()
0275 pages = property(**pages())
0276
0277 def __add__(self, window):
0278 if window.paginator != self.paginator:
0279 raise AttributeError("Window/paginator mismatch")
0280 assert self.last >= window.first
0281 return Window(self.page.next, padding=self.padding+1)