datatracker/tastypie/paginator.py
Henrik Levkowetz c39925fcd0 Added the tastypie lib to the repository
- Legacy-Id: 8742
2014-12-14 20:28:33 +00:00

210 lines
7 KiB
Python

from __future__ import unicode_literals
from django.conf import settings
from django.utils import six
from tastypie.exceptions import BadRequest
try:
from urllib.parse import urlencode
except ImportError:
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, max_limit=1000, collection_name='objects'):
"""
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.
Optionally accepts a ``max_limit`` argument, which the upper bound
limit. Defaults to ``1000``. If you set it to 0 or ``None``, no upper
bound will be enforced.
"""
self.request_data = request_data
self.objects = objects
self.limit = limit
self.max_limit = max_limit
self.offset = offset
self.resource_uri = resource_uri
self.collection_name = collection_name
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 = self.request_data.get('limit', self.limit)
if limit is None:
limit = getattr(settings, 'API_LIMIT_PER_PAGE', 20)
try:
limit = int(limit)
except ValueError:
raise BadRequest("Invalid limit '%s' provided. Please provide a positive integer." % limit)
if limit < 0:
raise BadRequest("Invalid limit '%s' provided. Please provide a positive integer >= 0." % limit)
if self.max_limit and (not limit or limit > self.max_limit):
# If it's more than the max, we're only going to return the max.
# This is to prevent excessive DB (or other) load.
return self.max_limit
return limit
def get_offset(self):
"""
Determines the proper starting offset of results to return.
It attempts 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." % offset)
if offset < 0:
raise BadRequest("Invalid offset '%s' provided. Please provide a positive integer >= 0." % offset)
return offset
def get_slice(self, limit, offset):
"""
Slices the result set to the specified ``limit`` & ``offset``.
"""
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
try:
# QueryDict has a urlencode method that can handle multiple values for the same key
request_params = self.request_data.copy()
if 'limit' in request_params:
del request_params['limit']
if 'offset' in request_params:
del request_params['offset']
request_params.update({'limit': limit, 'offset': offset})
encoded_params = request_params.urlencode()
except AttributeError:
request_params = {}
for k, v in self.request_data.items():
if isinstance(v, six.text_type):
request_params[k] = v.encode('utf-8')
else:
request_params[k] = v
if 'limit' in request_params:
del request_params['limit']
if 'offset' in request_params:
del request_params['offset']
request_params.update({'limit': limit, 'offset': offset})
encoded_params = urlencode(request_params)
return '%s?%s' % (
self.resource_uri,
encoded_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 {
self.collection_name: objects,
'meta': meta,
}