aboutsummaryrefslogtreecommitdiffstats
path: root/rest_framework/pagination.py
diff options
context:
space:
mode:
Diffstat (limited to 'rest_framework/pagination.py')
-rw-r--r--rest_framework/pagination.py219
1 files changed, 162 insertions, 57 deletions
diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py
index f31e5fa4..da2d60a4 100644
--- a/rest_framework/pagination.py
+++ b/rest_framework/pagination.py
@@ -3,87 +3,192 @@ Pagination serializers determine the structure of the output that should
be used for paginated responses.
"""
from __future__ import unicode_literals
-from rest_framework import serializers
+from django.core.paginator import InvalidPage, Paginator as DjangoPaginator
+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
from rest_framework.templatetags.rest_framework import replace_query_param
-class NextPageField(serializers.Field):
+def _strict_positive_int(integer_string, cutoff=None):
"""
- Field that returns a link to the next page in paginated results.
+ Cast a string to a strictly positive integer.
"""
- page_field = 'page'
-
- def to_representation(self, value):
- if not value.has_next():
- return None
- page = value.next_page_number()
- request = self.context.get('request')
- url = request and request.build_absolute_uri() or ''
- return replace_query_param(url, self.page_field, page)
+ ret = int(integer_string)
+ if ret <= 0:
+ raise ValueError()
+ if cutoff:
+ ret = min(ret, cutoff)
+ return ret
-class PreviousPageField(serializers.Field):
- """
- Field that returns a link to the previous page in paginated results.
- """
- page_field = 'page'
+class BasePagination(object):
+ def paginate_queryset(self, queryset, request):
+ raise NotImplemented('paginate_queryset() must be implemented.')
- def to_representation(self, value):
- if not value.has_previous():
- return None
- page = value.previous_page_number()
- request = self.context.get('request')
- url = request and request.build_absolute_uri() or ''
- return replace_query_param(url, self.page_field, page)
+ def get_paginated_response(self, data, page, request):
+ raise NotImplemented('get_paginated_response() must be implemented.')
-class DefaultObjectSerializer(serializers.ReadOnlyField):
+class PageNumberPagination(BasePagination):
"""
- If no object serializer is specified, then this serializer will be applied
- as the default.
+ 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
"""
+ # The default page size.
+ # Defaults to `None`, meaning pagination is disabled.
+ paginate_by = api_settings.PAGINATE_BY
- def __init__(self, source=None, many=None, context=None):
- # Note: Swallow context and many kwargs - only required for
- # eg. ModelSerializer.
- super(DefaultObjectSerializer, self).__init__(source=source)
+ # Client can control the page using this query parameter.
+ page_query_param = 'page'
+ # 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
-class BasePaginationSerializer(serializers.Serializer):
- """
- A base class for pagination serializers to inherit from,
- to make implementing custom serializers more easy.
- """
- results_field = 'results'
+ # 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
- def __init__(self, *args, **kwargs):
+ def paginate_queryset(self, queryset, request, view):
"""
- Override init to add in the object serializer field on-the-fly.
+ Paginate a queryset if required, either returning a page object,
+ or `None` if pagination is not configured for this view.
"""
- super(BasePaginationSerializer, self).__init__(*args, **kwargs)
- results_field = self.results_field
+ for attr in (
+ 'paginate_by', 'page_query_param',
+ 'paginate_by_param', 'max_paginate_by'
+ ):
+ if hasattr(view, attr):
+ setattr(self, attr, getattr(view, attr))
+
+ page_size = self.get_page_size(request)
+ if not page_size:
+ return None
+ paginator = DjangoPaginator(queryset, page_size)
+ page_string = request.query_params.get(self.page_query_param, 1)
try:
- object_serializer = self.Meta.object_serializer_class
- except AttributeError:
- object_serializer = DefaultObjectSerializer
+ page_number = paginator.validate_number(page_string)
+ except InvalidPage:
+ if page_string == 'last':
+ page_number = paginator.num_pages
+ else:
+ msg = _(
+ 'Choose a valid page number. Page numbers must be a '
+ 'whole number, or must be the string "last".'
+ )
+ raise NotFound(msg)
try:
- list_serializer_class = object_serializer.Meta.list_serializer_class
- except AttributeError:
- list_serializer_class = serializers.ListSerializer
+ 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)
+
+ self.request = request
+ return self.page
+
+ def get_paginated_response(self, objects):
+ return Response(OrderedDict([
+ ('count', self.page.paginator.count),
+ ('next', self.get_next_link()),
+ ('previous', self.get_previous_link()),
+ ('results', objects)
+ ]))
+
+ 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)
- self.fields[results_field] = list_serializer_class(
- child=object_serializer(),
- source='object_list'
- )
- self.fields[results_field].bind(field_name=results_field, parent=self)
+ 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()
+ return replace_query_param(url, self.page_query_param, page_number)
-class PaginationSerializer(BasePaginationSerializer):
+class LimitOffsetPagination(BasePagination):
"""
- A default implementation of a pagination serializer.
+ A limit/offset based style. For example:
+
+ http://api.example.org/accounts/?limit=100
+ http://api.example.org/accounts/?offset=400&limit=100
"""
- count = serializers.ReadOnlyField(source='paginator.count')
- next = NextPageField(source='*')
- previous = PreviousPageField(source='*')
+ default_limit = api_settings.PAGINATE_BY
+ limit_query_param = 'limit'
+ offset_query_param = 'offset'
+ max_limit = None
+
+ def paginate_queryset(self, queryset, request, view):
+ self.limit = self.get_limit(request)
+ self.offset = self.get_offset(request)
+ self.count = queryset.count()
+ self.request = request
+ return queryset[self.offset:self.offset + self.limit]
+
+ def get_paginated_response(self, objects):
+ return Response(OrderedDict([
+ ('count', self.count),
+ ('next', self.get_next_link()),
+ ('previous', self.get_previous_link()),
+ ('results', objects)
+ ]))
+
+ 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
+
+ def get_next_link(self, page):
+ if self.offset + self.limit >= self.count:
+ return None
+ url = self.request.build_absolute_uri()
+ offset = self.offset + self.limit
+ return replace_query_param(url, self.offset_query_param, offset)
+
+ def get_previous_link(self, page):
+ if self.offset - self.limit < 0:
+ return None
+ url = self.request.build_absolute_uri()
+ offset = self.offset - self.limit
+ return replace_query_param(url, self.offset_query_param, offset)