2013-04-25 15:47:34 +04:00
|
|
|
"""
|
|
|
|
Pagination serializers determine the structure of the output that should
|
|
|
|
be used for paginated responses.
|
|
|
|
"""
|
2013-02-05 00:55:35 +04:00
|
|
|
from __future__ import unicode_literals
|
2015-01-14 19:51:26 +03:00
|
|
|
from collections import namedtuple
|
2015-01-09 18:30:36 +03:00
|
|
|
from django.core.paginator import InvalidPage, Paginator as DjangoPaginator
|
2015-01-14 19:51:26 +03:00
|
|
|
from django.template import Context, loader
|
2015-01-09 18:30:36 +03:00
|
|
|
from django.utils import six
|
|
|
|
from django.utils.translation import ugettext as _
|
|
|
|
from rest_framework.compat import OrderedDict
|
|
|
|
from rest_framework.exceptions import NotFound
|
|
|
|
from rest_framework.response import Response
|
|
|
|
from rest_framework.settings import api_settings
|
2015-01-14 19:51:26 +03:00
|
|
|
from rest_framework.templatetags.rest_framework import (
|
|
|
|
replace_query_param, remove_query_param
|
|
|
|
)
|
2012-09-30 20:31:28 +04:00
|
|
|
|
|
|
|
|
2015-01-09 18:30:36 +03:00
|
|
|
def _strict_positive_int(integer_string, cutoff=None):
|
2012-10-01 18:49:19 +04:00
|
|
|
"""
|
2015-01-09 18:30:36 +03:00
|
|
|
Cast a string to a strictly positive integer.
|
2012-10-01 18:49:19 +04:00
|
|
|
"""
|
2015-01-09 18:30:36 +03:00
|
|
|
ret = int(integer_string)
|
|
|
|
if ret <= 0:
|
|
|
|
raise ValueError()
|
|
|
|
if cutoff:
|
|
|
|
ret = min(ret, cutoff)
|
|
|
|
return ret
|
2012-09-30 20:31:28 +04:00
|
|
|
|
|
|
|
|
2015-01-15 19:52:07 +03:00
|
|
|
def _divide_with_ceil(a, b):
|
|
|
|
"""
|
|
|
|
Returns 'a' divded by 'b', with any remainder rounded up.
|
|
|
|
"""
|
|
|
|
if a % b:
|
|
|
|
return (a / b) + 1
|
|
|
|
return a / b
|
|
|
|
|
|
|
|
|
2015-01-13 20:14:13 +03:00
|
|
|
def _get_count(queryset):
|
|
|
|
"""
|
|
|
|
Determine an object count, supporting either querysets or regular lists.
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
return queryset.count()
|
2015-01-16 00:07:05 +03:00
|
|
|
except (AttributeError, TypeError):
|
2015-01-13 20:14:13 +03:00
|
|
|
return len(queryset)
|
|
|
|
|
|
|
|
|
2015-01-14 19:51:26 +03:00
|
|
|
def _get_displayed_page_numbers(current, final):
|
|
|
|
"""
|
|
|
|
This utility function determines a list of page numbers to display.
|
|
|
|
This gives us a nice contextually relevant set of page numbers.
|
|
|
|
|
|
|
|
For example:
|
|
|
|
current=14, final=16 -> [1, None, 13, 14, 15, 16]
|
2015-01-14 20:46:41 +03:00
|
|
|
|
|
|
|
This implementation gives one page to each side of the cursor,
|
2015-01-15 19:52:07 +03:00
|
|
|
or two pages to the side when the cursor is at the edge, then
|
|
|
|
ensures that any breaks between non-continous page numbers never
|
|
|
|
remove only a single page.
|
|
|
|
|
|
|
|
For an alernativative implementation which gives two pages to each side of
|
|
|
|
the cursor, eg. as in GitHub issue list pagination, see:
|
2015-01-14 20:46:41 +03:00
|
|
|
|
|
|
|
https://gist.github.com/tomchristie/321140cebb1c4a558b15
|
2015-01-14 19:51:26 +03:00
|
|
|
"""
|
|
|
|
assert current >= 1
|
|
|
|
assert final >= current
|
|
|
|
|
2015-01-15 19:52:07 +03:00
|
|
|
if final <= 5:
|
|
|
|
return range(1, final + 1)
|
|
|
|
|
2015-01-14 19:51:26 +03:00
|
|
|
# We always include the first two pages, last two pages, and
|
|
|
|
# two pages either side of the current page.
|
|
|
|
included = set((
|
|
|
|
1,
|
|
|
|
current - 1, current, current + 1,
|
|
|
|
final
|
|
|
|
))
|
|
|
|
|
|
|
|
# If the break would only exclude a single page number then we
|
|
|
|
# may as well include the page number instead of the break.
|
2015-01-14 20:46:41 +03:00
|
|
|
if current <= 4:
|
2015-01-14 19:51:26 +03:00
|
|
|
included.add(2)
|
2015-01-14 20:46:41 +03:00
|
|
|
included.add(3)
|
|
|
|
if current >= final - 3:
|
2015-01-14 19:51:26 +03:00
|
|
|
included.add(final - 1)
|
2015-01-14 20:46:41 +03:00
|
|
|
included.add(final - 2)
|
2015-01-14 19:51:26 +03:00
|
|
|
|
|
|
|
# Now sort the page numbers and drop anything outside the limits.
|
|
|
|
included = [
|
|
|
|
idx for idx in sorted(list(included))
|
|
|
|
if idx > 0 and idx <= final
|
|
|
|
]
|
|
|
|
|
|
|
|
# Finally insert any `...` breaks
|
|
|
|
if current > 4:
|
|
|
|
included.insert(1, None)
|
|
|
|
if current < final - 3:
|
|
|
|
included.insert(len(included) - 1, None)
|
|
|
|
return included
|
|
|
|
|
|
|
|
|
2015-01-15 19:52:07 +03:00
|
|
|
def _get_page_links(page_numbers, current, url_func):
|
|
|
|
"""
|
|
|
|
Given a list of page numbers and `None` page breaks,
|
|
|
|
return a list of `PageLink` objects.
|
|
|
|
"""
|
|
|
|
page_links = []
|
|
|
|
for page_number in page_numbers:
|
|
|
|
if page_number is None:
|
2015-01-16 00:07:05 +03:00
|
|
|
page_link = PAGE_BREAK
|
2015-01-15 19:52:07 +03:00
|
|
|
else:
|
|
|
|
page_link = PageLink(
|
|
|
|
url=url_func(page_number),
|
|
|
|
number=page_number,
|
|
|
|
is_active=(page_number == current),
|
|
|
|
is_break=False
|
|
|
|
)
|
|
|
|
page_links.append(page_link)
|
|
|
|
return page_links
|
|
|
|
|
|
|
|
|
2015-01-14 19:51:26 +03:00
|
|
|
PageLink = namedtuple('PageLink', ['url', 'number', 'is_active', 'is_break'])
|
|
|
|
|
2015-01-16 00:07:05 +03:00
|
|
|
PAGE_BREAK = PageLink(url=None, number=None, is_active=False, is_break=True)
|
|
|
|
|
2015-01-14 19:51:26 +03:00
|
|
|
|
2015-01-09 18:30:36 +03:00
|
|
|
class BasePagination(object):
|
2015-01-15 19:52:07 +03:00
|
|
|
display_page_controls = False
|
|
|
|
|
2015-01-16 19:55:46 +03:00
|
|
|
def paginate_queryset(self, queryset, request, view=None): # pragma: no cover
|
2015-01-09 18:30:36 +03:00
|
|
|
raise NotImplemented('paginate_queryset() must be implemented.')
|
2012-11-08 01:09:26 +04:00
|
|
|
|
2015-01-16 19:55:46 +03:00
|
|
|
def get_paginated_response(self, data): # pragma: no cover
|
2015-01-09 18:30:36 +03:00
|
|
|
raise NotImplemented('get_paginated_response() must be implemented.')
|
2012-09-30 20:31:28 +04:00
|
|
|
|
2015-01-16 19:55:46 +03:00
|
|
|
def to_html(self): # pragma: no cover
|
2015-01-15 19:52:07 +03:00
|
|
|
raise NotImplemented('to_html() must be implemented to display page controls.')
|
|
|
|
|
2012-09-30 20:31:28 +04:00
|
|
|
|
2015-01-09 18:30:36 +03:00
|
|
|
class PageNumberPagination(BasePagination):
|
2013-01-27 00:54:41 +04:00
|
|
|
"""
|
2015-01-09 18:30:36 +03:00
|
|
|
A simple page number based style that supports page numbers as
|
|
|
|
query parameters. For example:
|
|
|
|
|
|
|
|
http://api.example.org/accounts/?page=4
|
|
|
|
http://api.example.org/accounts/?page=4&page_size=100
|
2013-01-27 00:54:41 +04:00
|
|
|
"""
|
2015-01-09 18:30:36 +03:00
|
|
|
# The default page size.
|
|
|
|
# Defaults to `None`, meaning pagination is disabled.
|
|
|
|
paginate_by = api_settings.PAGINATE_BY
|
2013-01-27 00:54:41 +04:00
|
|
|
|
2015-01-09 18:30:36 +03:00
|
|
|
# Client can control the page using this query parameter.
|
|
|
|
page_query_param = 'page'
|
2013-01-27 00:54:41 +04:00
|
|
|
|
2015-01-09 18:30:36 +03:00
|
|
|
# Client can control the page size using this query parameter.
|
|
|
|
# Default is 'None'. Set to eg 'page_size' to enable usage.
|
|
|
|
paginate_by_param = api_settings.PAGINATE_BY_PARAM
|
2013-01-27 00:54:41 +04:00
|
|
|
|
2015-01-09 18:30:36 +03:00
|
|
|
# Set to an integer to limit the maximum page size the client may request.
|
|
|
|
# Only relevant if 'paginate_by_param' has also been set.
|
|
|
|
max_paginate_by = api_settings.MAX_PAGINATE_BY
|
2012-10-01 18:49:19 +04:00
|
|
|
|
2015-01-16 00:07:05 +03:00
|
|
|
last_page_strings = ('last',)
|
|
|
|
|
2015-01-14 19:51:26 +03:00
|
|
|
template = 'rest_framework/pagination/numbers.html'
|
|
|
|
|
2015-01-16 19:55:46 +03:00
|
|
|
def _handle_backwards_compat(self, view):
|
2012-10-01 18:49:19 +04:00
|
|
|
"""
|
2015-01-16 19:55:46 +03:00
|
|
|
Prior to version 3.1, pagination was handled in the view, and the
|
|
|
|
attributes were set there. The attributes should now be set on
|
|
|
|
the pagination class, but the old style is still pending deprecation.
|
2012-10-01 18:49:19 +04:00
|
|
|
"""
|
2015-01-09 18:30:36 +03:00
|
|
|
for attr in (
|
|
|
|
'paginate_by', 'page_query_param',
|
|
|
|
'paginate_by_param', 'max_paginate_by'
|
|
|
|
):
|
|
|
|
if hasattr(view, attr):
|
|
|
|
setattr(self, attr, getattr(view, attr))
|
|
|
|
|
2015-01-16 19:55:46 +03:00
|
|
|
def paginate_queryset(self, queryset, request, view=None):
|
|
|
|
"""
|
|
|
|
Paginate a queryset if required, either returning a
|
|
|
|
page object, or `None` if pagination is not configured for this view.
|
|
|
|
"""
|
|
|
|
self._handle_backwards_compat(view)
|
|
|
|
|
2015-01-09 18:30:36 +03:00
|
|
|
page_size = self.get_page_size(request)
|
|
|
|
if not page_size:
|
|
|
|
return None
|
2013-01-23 11:38:13 +04:00
|
|
|
|
2015-01-09 18:30:36 +03:00
|
|
|
paginator = DjangoPaginator(queryset, page_size)
|
2015-01-16 00:07:05 +03:00
|
|
|
page_number = request.query_params.get(self.page_query_param, 1)
|
|
|
|
if page_number in self.last_page_strings:
|
|
|
|
page_number = paginator.num_pages
|
2014-09-02 20:41:23 +04:00
|
|
|
|
2014-12-20 19:32:07 +03:00
|
|
|
try:
|
2015-01-09 18:30:36 +03:00
|
|
|
self.page = paginator.page(page_number)
|
|
|
|
except InvalidPage as exc:
|
|
|
|
msg = _('Invalid page "{page_number}": {message}.').format(
|
|
|
|
page_number=page_number, message=six.text_type(exc)
|
|
|
|
)
|
|
|
|
raise NotFound(msg)
|
|
|
|
|
2015-01-15 19:52:07 +03:00
|
|
|
if paginator.count > 1:
|
|
|
|
# The browsable API should display pagination controls.
|
|
|
|
self.display_page_controls = True
|
2015-01-16 00:07:05 +03:00
|
|
|
|
2015-01-09 18:30:36 +03:00
|
|
|
self.request = request
|
|
|
|
return self.page
|
|
|
|
|
2015-01-13 20:14:13 +03:00
|
|
|
def get_paginated_response(self, data):
|
2015-01-09 18:30:36 +03:00
|
|
|
return Response(OrderedDict([
|
|
|
|
('count', self.page.paginator.count),
|
|
|
|
('next', self.get_next_link()),
|
|
|
|
('previous', self.get_previous_link()),
|
2015-01-13 20:14:13 +03:00
|
|
|
('results', data)
|
2015-01-09 18:30:36 +03:00
|
|
|
]))
|
|
|
|
|
|
|
|
def get_page_size(self, request):
|
|
|
|
if self.paginate_by_param:
|
|
|
|
try:
|
|
|
|
return _strict_positive_int(
|
|
|
|
request.query_params[self.paginate_by_param],
|
|
|
|
cutoff=self.max_paginate_by
|
|
|
|
)
|
|
|
|
except (KeyError, ValueError):
|
|
|
|
pass
|
|
|
|
|
|
|
|
return self.paginate_by
|
|
|
|
|
|
|
|
def get_next_link(self):
|
|
|
|
if not self.page.has_next():
|
|
|
|
return None
|
|
|
|
url = self.request.build_absolute_uri()
|
|
|
|
page_number = self.page.next_page_number()
|
|
|
|
return replace_query_param(url, self.page_query_param, page_number)
|
2014-12-20 19:32:07 +03:00
|
|
|
|
2015-01-09 18:30:36 +03:00
|
|
|
def get_previous_link(self):
|
|
|
|
if not self.page.has_previous():
|
|
|
|
return None
|
|
|
|
url = self.request.build_absolute_uri()
|
|
|
|
page_number = self.page.previous_page_number()
|
2015-01-14 19:51:26 +03:00
|
|
|
if page_number == 1:
|
|
|
|
return remove_query_param(url, self.page_query_param)
|
2015-01-09 18:30:36 +03:00
|
|
|
return replace_query_param(url, self.page_query_param, page_number)
|
2012-09-30 20:31:28 +04:00
|
|
|
|
2015-01-16 00:07:05 +03:00
|
|
|
def get_html_context(self):
|
2015-01-14 19:51:26 +03:00
|
|
|
base_url = self.request.build_absolute_uri()
|
2015-01-15 19:55:04 +03:00
|
|
|
|
2015-01-15 19:52:07 +03:00
|
|
|
def page_number_to_url(page_number):
|
|
|
|
if page_number == 1:
|
|
|
|
return remove_query_param(base_url, self.page_query_param)
|
2015-01-14 19:51:26 +03:00
|
|
|
else:
|
2015-01-15 19:52:07 +03:00
|
|
|
return replace_query_param(base_url, self.page_query_param, page_number)
|
|
|
|
|
|
|
|
current = self.page.number
|
|
|
|
final = self.page.paginator.num_pages
|
|
|
|
page_numbers = _get_displayed_page_numbers(current, final)
|
|
|
|
page_links = _get_page_links(page_numbers, current, page_number_to_url)
|
2015-01-14 19:51:26 +03:00
|
|
|
|
2015-01-16 00:07:05 +03:00
|
|
|
return {
|
2015-01-14 19:51:26 +03:00
|
|
|
'previous_url': self.get_previous_link(),
|
|
|
|
'next_url': self.get_next_link(),
|
|
|
|
'page_links': page_links
|
2015-01-16 00:07:05 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
def to_html(self):
|
|
|
|
template = loader.get_template(self.template)
|
|
|
|
context = Context(self.get_html_context())
|
2015-01-14 19:51:26 +03:00
|
|
|
return template.render(context)
|
|
|
|
|
2012-10-01 18:49:19 +04:00
|
|
|
|
2015-01-09 18:30:36 +03:00
|
|
|
class LimitOffsetPagination(BasePagination):
|
2012-10-01 18:49:19 +04:00
|
|
|
"""
|
2015-01-09 18:30:36 +03:00
|
|
|
A limit/offset based style. For example:
|
|
|
|
|
|
|
|
http://api.example.org/accounts/?limit=100
|
|
|
|
http://api.example.org/accounts/?offset=400&limit=100
|
2012-10-01 18:49:19 +04:00
|
|
|
"""
|
2015-01-09 18:30:36 +03:00
|
|
|
default_limit = api_settings.PAGINATE_BY
|
|
|
|
limit_query_param = 'limit'
|
|
|
|
offset_query_param = 'offset'
|
|
|
|
max_limit = None
|
2015-01-15 19:52:07 +03:00
|
|
|
template = 'rest_framework/pagination/numbers.html'
|
|
|
|
|
2015-01-16 00:07:05 +03:00
|
|
|
def paginate_queryset(self, queryset, request, view=None):
|
2015-01-09 18:30:36 +03:00
|
|
|
self.limit = self.get_limit(request)
|
|
|
|
self.offset = self.get_offset(request)
|
2015-01-13 20:14:13 +03:00
|
|
|
self.count = _get_count(queryset)
|
2015-01-09 18:30:36 +03:00
|
|
|
self.request = request
|
2015-01-15 19:52:07 +03:00
|
|
|
if self.count > self.limit:
|
|
|
|
self.display_page_controls = True
|
2015-01-09 18:30:36 +03:00
|
|
|
return queryset[self.offset:self.offset + self.limit]
|
|
|
|
|
2015-01-13 20:14:13 +03:00
|
|
|
def get_paginated_response(self, data):
|
2015-01-09 18:30:36 +03:00
|
|
|
return Response(OrderedDict([
|
|
|
|
('count', self.count),
|
|
|
|
('next', self.get_next_link()),
|
|
|
|
('previous', self.get_previous_link()),
|
2015-01-13 20:14:13 +03:00
|
|
|
('results', data)
|
2015-01-09 18:30:36 +03:00
|
|
|
]))
|
|
|
|
|
|
|
|
def get_limit(self, request):
|
|
|
|
if self.limit_query_param:
|
|
|
|
try:
|
|
|
|
return _strict_positive_int(
|
|
|
|
request.query_params[self.limit_query_param],
|
|
|
|
cutoff=self.max_limit
|
|
|
|
)
|
|
|
|
except (KeyError, ValueError):
|
|
|
|
pass
|
|
|
|
|
|
|
|
return self.default_limit
|
|
|
|
|
|
|
|
def get_offset(self, request):
|
|
|
|
try:
|
|
|
|
return _strict_positive_int(
|
|
|
|
request.query_params[self.offset_query_param],
|
|
|
|
)
|
|
|
|
except (KeyError, ValueError):
|
|
|
|
return 0
|
|
|
|
|
2015-01-15 19:52:07 +03:00
|
|
|
def get_next_link(self):
|
2015-01-09 18:30:36 +03:00
|
|
|
if self.offset + self.limit >= self.count:
|
|
|
|
return None
|
2015-01-15 19:52:07 +03:00
|
|
|
|
2015-01-09 18:30:36 +03:00
|
|
|
url = self.request.build_absolute_uri()
|
|
|
|
offset = self.offset + self.limit
|
|
|
|
return replace_query_param(url, self.offset_query_param, offset)
|
|
|
|
|
2015-01-15 19:52:07 +03:00
|
|
|
def get_previous_link(self):
|
|
|
|
if self.offset <= 0:
|
2015-01-09 18:30:36 +03:00
|
|
|
return None
|
2015-01-15 19:52:07 +03:00
|
|
|
|
2015-01-09 18:30:36 +03:00
|
|
|
url = self.request.build_absolute_uri()
|
2015-01-15 19:52:07 +03:00
|
|
|
|
|
|
|
if self.offset - self.limit <= 0:
|
|
|
|
return remove_query_param(url, self.offset_query_param)
|
|
|
|
|
2015-01-09 18:30:36 +03:00
|
|
|
offset = self.offset - self.limit
|
|
|
|
return replace_query_param(url, self.offset_query_param, offset)
|
2015-01-15 19:52:07 +03:00
|
|
|
|
2015-01-16 00:07:05 +03:00
|
|
|
def get_html_context(self):
|
2015-01-15 19:52:07 +03:00
|
|
|
base_url = self.request.build_absolute_uri()
|
|
|
|
current = _divide_with_ceil(self.offset, self.limit) + 1
|
2015-01-16 19:55:46 +03:00
|
|
|
# The number of pages is a little bit fiddly.
|
|
|
|
# We need to sum both the number of pages from current offset to end
|
|
|
|
# plus the number of pages up to the current offset.
|
|
|
|
# When offset is not strictly divisible by the limit then we may
|
|
|
|
# end up introducing an extra page as an artifact.
|
|
|
|
final = (
|
|
|
|
_divide_with_ceil(self.count - self.offset, self.limit) +
|
|
|
|
_divide_with_ceil(self.offset, self.limit)
|
|
|
|
)
|
2015-01-15 19:52:07 +03:00
|
|
|
|
|
|
|
def page_number_to_url(page_number):
|
|
|
|
if page_number == 1:
|
|
|
|
return remove_query_param(base_url, self.offset_query_param)
|
|
|
|
else:
|
|
|
|
offset = self.offset + ((page_number - current) * self.limit)
|
|
|
|
return replace_query_param(base_url, self.offset_query_param, offset)
|
|
|
|
|
|
|
|
page_numbers = _get_displayed_page_numbers(current, final)
|
|
|
|
page_links = _get_page_links(page_numbers, current, page_number_to_url)
|
|
|
|
|
2015-01-16 00:07:05 +03:00
|
|
|
return {
|
2015-01-15 19:52:07 +03:00
|
|
|
'previous_url': self.get_previous_link(),
|
|
|
|
'next_url': self.get_next_link(),
|
|
|
|
'page_links': page_links
|
2015-01-16 00:07:05 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
def to_html(self):
|
|
|
|
template = loader.get_template(self.template)
|
|
|
|
context = Context(self.get_html_context())
|
2015-01-15 19:55:04 +03:00
|
|
|
return template.render(context)
|