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)