Find file
Fetching contributors…
Cannot retrieve contributors at this time
171 lines (134 sloc) 5.75 KB
from django.conf import settings
from tastypie.exceptions import BadRequest
from urllib import urlencode
class Paginator(object):
"""
Limits result sets down to sane amounts for passing to the client.
This is used in place of Django's ``Paginator`` due to the way pagination
works. ``limit`` & ``offset`` (tastypie) are used in place of ``page``
(Django) so none of the page-related calculations are necessary.
This implementation also provides additional details like the
``total_count`` of resources seen and convenience links to the
``previous``/``next`` pages of data as available.
"""
def __init__(self, request_data, objects, resource_uri=None, limit=None, offset=0):
"""
Instantiates the ``Paginator`` and allows for some configuration.
The ``request_data`` argument ought to be a dictionary-like object.
May provide ``limit`` and/or ``offset`` to override the defaults.
Commonly provided ``request.GET``. Required.
The ``objects`` should be a list-like object of ``Resources``.
This is typically a ``QuerySet`` but can be anything that
implements slicing. Required.
Optionally accepts a ``limit`` argument, which specifies how many
items to show at a time. Defaults to ``None``, which is no limit.
Optionally accepts an ``offset`` argument, which specifies where in
the ``objects`` to start displaying results from. Defaults to 0.
"""
self.request_data = request_data
self.objects = objects
self.limit = limit
self.offset = offset
self.resource_uri = resource_uri
def get_limit(self):
"""
Determines the proper maximum number of results to return.
In order of importance, it will use:
* The user-requested ``limit`` from the GET parameters, if specified.
* The object-level ``limit`` if specified.
* ``settings.API_LIMIT_PER_PAGE`` if specified.
Default is 20 per page.
"""
limit = getattr(settings, 'API_LIMIT_PER_PAGE', 20)
if 'limit' in self.request_data:
limit = self.request_data['limit']
elif self.limit is not None:
limit = self.limit
try:
limit = int(limit)
except ValueError:
raise BadRequest("Invalid limit '%s' provided. Please provide a positive integer.")
if limit < 0:
raise BadRequest("Invalid limit '%s' provided. Please provide an integer >= 0.")
return limit
def get_offset(self):
"""
Determines the proper starting offset of results to return.
It attempst to use the user-provided ``offset`` from the GET parameters,
if specified. Otherwise, it falls back to the object-level ``offset``.
Default is 0.
"""
offset = self.offset
if 'offset' in self.request_data:
offset = self.request_data['offset']
try:
offset = int(offset)
except ValueError:
raise BadRequest("Invalid offset '%s' provided. Please provide an integer.")
if offset < 0:
raise BadRequest("Invalid offset '%s' provided. Please provide an integer >= 0.")
return offset
def get_slice(self, limit, offset):
"""
Slices the result set to the specified ``limit`` & ``offset``.
"""
# If it's zero, return everything.
if limit == 0:
return self.objects[offset:]
return self.objects[offset:offset + limit]
def get_count(self):
"""
Returns a count of the total number of objects seen.
"""
try:
return self.objects.count()
except (AttributeError, TypeError):
# If it's not a QuerySet (or it's ilk), fallback to ``len``.
return len(self.objects)
def get_previous(self, limit, offset):
"""
If a previous page is available, will generate a URL to request that
page. If not available, this returns ``None``.
"""
if offset - limit < 0:
return None
return self._generate_uri(limit, offset-limit)
def get_next(self, limit, offset, count):
"""
If a next page is available, will generate a URL to request that
page. If not available, this returns ``None``.
"""
if offset + limit >= count:
return None
return self._generate_uri(limit, offset+limit)
def _generate_uri(self, limit, offset):
if self.resource_uri is None:
return None
request_params = dict([k, v.encode('utf-8')] for k, v in self.request_data.items())
request_params.update({'limit': limit, 'offset': offset})
return '%s?%s' % (
self.resource_uri,
urlencode(request_params)
)
def page(self):
"""
Generates all pertinent data about the requested page.
Handles getting the correct ``limit`` & ``offset``, then slices off
the correct set of results and returns all pertinent metadata.
"""
limit = self.get_limit()
offset = self.get_offset()
count = self.get_count()
objects = self.get_slice(limit, offset)
meta = {
'offset': offset,
'limit': limit,
'total_count': count,
}
if limit:
meta['previous'] = self.get_previous(limit, offset)
meta['next'] = self.get_next(limit, offset, count)
return {
'objects': objects,
'meta': meta,
}