diff options
| author | Tom Christie | 2015-01-09 15:30:36 +0000 |
|---|---|---|
| committer | Tom Christie | 2015-01-09 15:30:36 +0000 |
| commit | 73feaf6299827607eab94ce96b77b73671880626 (patch) | |
| tree | 15a7b068afda489adc232d4018d645ca0f7f7d7a /rest_framework/pagination.py | |
| parent | 11efde890556a46f32d486250b1a0b03ce83c17d (diff) | |
| download | django-rest-framework-73feaf6299827607eab94ce96b77b73671880626.tar.bz2 | |
First pass at 3.1 pagination API
Diffstat (limited to 'rest_framework/pagination.py')
| -rw-r--r-- | rest_framework/pagination.py | 219 |
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) |
