Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Added pagination to Resource #9

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 168 additions & 0 deletions restkiss/paginator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import collections
import warnings
from math import ceil


class UnorderedObjectListWarning(RuntimeWarning):
pass


class InvalidPage(Exception):
pass


class PageNotAnInteger(InvalidPage):
pass


class EmptyPage(InvalidPage):
pass


class Paginator(object):

def __init__(self, object_list, per_page, orphans=0,
allow_empty_first_page=True):
self.object_list = object_list
self._check_object_list_is_ordered()
self.per_page = int(per_page)
self.orphans = int(orphans)
self.allow_empty_first_page = allow_empty_first_page

def validate_number(self, number):
"""
Validates the given 1-based page number.
"""
try:
number = int(number)
except (TypeError, ValueError):
raise PageNotAnInteger('That page number is not an integer')
if number < 1:
raise EmptyPage('That page number is less than 1')
if number > self.num_pages:
if number == 1 and self.allow_empty_first_page:
pass
else:
raise EmptyPage('That page contains no results')
return number

def page(self, number):
"""
Returns a Page object for the given 1-based page number.
"""
number = self.validate_number(number)
bottom = (number - 1) * self.per_page
top = bottom + self.per_page
if top + self.orphans >= self.count:
top = self.count
return self._get_page(self.object_list[bottom:top], number, self)

def _get_page(self, *args, **kwargs):
"""
Returns an instance of a single page.

This hook can be used by subclasses to use an alternative to the
standard :cls:`Page` object.
"""
return Page(*args, **kwargs)

def count(self):
"""
Returns the total number of objects, across all pages.
"""
try:
return self.object_list.count()
except (AttributeError, TypeError):
# AttributeError if object_list has no count() method.
# TypeError if object_list.count() requires arguments
# (i.e. is of type list).
return len(self.object_list)

def num_pages(self):
"""
Returns the total number of pages.
"""
if self.count == 0 and not self.allow_empty_first_page:
return 0
hits = max(1, self.count - self.orphans)
return int(ceil(hits / float(self.per_page)))

@property
def page_range(self):
"""
Returns a 1-based range of pages for iterating through within
a template for loop.
"""
return range(1, self.num_pages + 1)

def _check_object_list_is_ordered(self):
"""
Warn if self.object_list is unordered (typically a QuerySet).
"""
if hasattr(self.object_list, 'ordered') and not self.object_list.ordered:
warnings.warn(
'Pagination may yield inconsistent results with an unordered '
'object_list: {!r}'.format(self.object_list),
UnorderedObjectListWarning
)


QuerySetPaginator = Paginator # For backwards-compatibility.


class Page(collections.Sequence):

def __init__(self, object_list, number, paginator):
self.object_list = object_list
self.number = number
self.paginator = paginator

def __repr__(self):
return '<Page %s of %s>' % (self.number, self.paginator.num_pages)

def __len__(self):
return len(self.object_list)

def __getitem__(self, index):
if not isinstance(index, (slice, int)):
raise TypeError
# The object_list is converted to a list so that if it was a QuerySet
# it won't be a database hit per __getitem__.
if not isinstance(self.object_list, list):
self.object_list = list(self.object_list)
return self.object_list[index]

def has_next(self):
return self.number < self.paginator.num_pages

def has_previous(self):
return self.number > 1

def has_other_pages(self):
return self.has_previous() or self.has_next()

def next_page_number(self):
return self.paginator.validate_number(self.number + 1)

def previous_page_number(self):
return self.paginator.validate_number(self.number - 1)

def start_index(self):
"""
Returns the 1-based index of the first object on this page,
relative to total objects in the paginator.
"""
# Special case, return zero if no items.
if self.paginator.count == 0:
return 0
return (self.paginator.per_page * (self.number - 1)) + 1

def end_index(self):
"""
Returns the 1-based index of the last object on this page,
relative to total objects found (hits).
"""
# Special case for the last page because there can be orphans.
if self.number == self.paginator.num_pages:
return self.paginator.count
return self.number * self.paginator.per_page
25 changes: 22 additions & 3 deletions restkiss/resources.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import six
import sys

from .constants import OK, CREATED, ACCEPTED, NO_CONTENT
from .data import Data
from .exceptions import MethodNotImplemented, Unauthorized
from .exceptions import MethodNotImplemented, Unauthorized, BadRequest
from .paginator import Paginator
from .preparers import Preparer
from .serializers import JSONSerializer
from .utils import format_traceback
Expand Down Expand Up @@ -268,7 +268,7 @@ def handle(self, endpoint, *args, **kwargs):
try:
# Use ``.get()`` so we can also dodge potentially incorrect
# ``endpoint`` errors as well.
if not method in self.http_methods.get(endpoint, {}):
if method not in self.http_methods.get(endpoint, {}):
raise MethodNotImplemented(
"Unsupported method '{0}' for {1} endpoint.".format(
method,
Expand Down Expand Up @@ -385,7 +385,23 @@ def serialize(self, method, endpoint, data):
return self.serialize_list(data)
return self.serialize_detail(data)

def paginate_data(self, data):
page_size = getattr(self, 'page_size', 20)
paginator = Paginator(data, page_size)

try:
page_number = int(self.request.GET.get('p', 1))
except ValueError:
page_number = None

if page_number not in paginator.page_range:
raise BadRequest('Invalid page number')

self.page = paginator.page(page_number)
return self.page.object_list

def serialize_list(self, data):

"""
Given a collection of data (``objects`` or ``dicts``), serializes them.

Expand All @@ -398,6 +414,9 @@ def serialize_list(self, data):
if data is None:
return ''

if getattr(self, 'paginate', False):
data = self.paginate_data(data)

# Check for a ``Data``-like object. We should assume ``True`` (all
# data gets prepared) unless it's explicitly marked as not.
if not getattr(data, 'should_prepare', True):
Expand Down