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 | |
| parent | 11efde890556a46f32d486250b1a0b03ce83c17d (diff) | |
| download | django-rest-framework-73feaf6299827607eab94ce96b77b73671880626.tar.bz2 | |
First pass at 3.1 pagination API
Diffstat (limited to 'rest_framework')
| -rw-r--r-- | rest_framework/generics.py | 220 | ||||
| -rw-r--r-- | rest_framework/mixins.py | 13 | ||||
| -rw-r--r-- | rest_framework/pagination.py | 219 | ||||
| -rw-r--r-- | rest_framework/settings.py | 4 |
4 files changed, 241 insertions, 215 deletions
diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 0d709c37..12fb6413 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -2,29 +2,13 @@ Generic views that provide commonly needed behaviour. """ from __future__ import unicode_literals - -from django.core.paginator import Paginator, InvalidPage from django.db.models.query import QuerySet from django.http import Http404 from django.shortcuts import get_object_or_404 as _get_object_or_404 -from django.utils import six -from django.utils.translation import ugettext as _ from rest_framework import views, mixins from rest_framework.settings import api_settings -def strict_positive_int(integer_string, cutoff=None): - """ - Cast a string to a strictly positive integer. - """ - ret = int(integer_string) - if ret <= 0: - raise ValueError() - if cutoff: - ret = min(ret, cutoff) - return ret - - def get_object_or_404(queryset, *filter_args, **filter_kwargs): """ Same as Django's standard shortcut, but make sure to also raise 404 @@ -40,7 +24,6 @@ class GenericAPIView(views.APIView): """ Base class for all other generic views. """ - # You'll need to either set these attributes, # or override `get_queryset()`/`get_serializer_class()`. # If you are overriding a view method, it is important that you call @@ -50,146 +33,16 @@ class GenericAPIView(views.APIView): queryset = None serializer_class = None - # If you want to use object lookups other than pk, set this attribute. + # If you want to use object lookups other than pk, set 'lookup_field'. # For more complex lookup requirements override `get_object()`. lookup_field = 'pk' lookup_url_kwarg = None - # Pagination settings - paginate_by = api_settings.PAGINATE_BY - paginate_by_param = api_settings.PAGINATE_BY_PARAM - max_paginate_by = api_settings.MAX_PAGINATE_BY - pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS - page_kwarg = 'page' - # The filter backend classes to use for queryset filtering filter_backends = api_settings.DEFAULT_FILTER_BACKENDS - # The following attribute may be subject to change, - # and should be considered private API. - paginator_class = Paginator - - def get_serializer_context(self): - """ - Extra context provided to the serializer class. - """ - return { - 'request': self.request, - 'format': self.format_kwarg, - 'view': self - } - - def get_serializer(self, *args, **kwargs): - """ - Return the serializer instance that should be used for validating and - deserializing input, and for serializing output. - """ - serializer_class = self.get_serializer_class() - kwargs['context'] = self.get_serializer_context() - return serializer_class(*args, **kwargs) - - def get_pagination_serializer(self, page): - """ - Return a serializer instance to use with paginated data. - """ - class SerializerClass(self.pagination_serializer_class): - class Meta: - object_serializer_class = self.get_serializer_class() - - pagination_serializer_class = SerializerClass - context = self.get_serializer_context() - return pagination_serializer_class(instance=page, context=context) - - def paginate_queryset(self, queryset): - """ - Paginate a queryset if required, either returning a page object, - or `None` if pagination is not configured for this view. - """ - page_size = self.get_paginate_by() - if not page_size: - return None - - paginator = self.paginator_class(queryset, page_size) - page_kwarg = self.kwargs.get(self.page_kwarg) - page_query_param = self.request.query_params.get(self.page_kwarg) - page = page_kwarg or page_query_param or 1 - try: - page_number = paginator.validate_number(page) - except InvalidPage: - if page == 'last': - page_number = paginator.num_pages - else: - raise Http404(_('Choose a valid page number. Page numbers must be a whole number, or must be the string "last".')) - - try: - page = paginator.page(page_number) - except InvalidPage as exc: - error_format = _('Invalid page "{page_number}": {message}.') - raise Http404(error_format.format( - page_number=page_number, message=six.text_type(exc) - )) - - return page - - def filter_queryset(self, queryset): - """ - Given a queryset, filter it with whichever filter backend is in use. - - You are unlikely to want to override this method, although you may need - to call it either from a list view, or from a custom `get_object` - method if you want to apply the configured filtering backend to the - default queryset. - """ - for backend in self.get_filter_backends(): - queryset = backend().filter_queryset(self.request, queryset, self) - return queryset - - def get_filter_backends(self): - """ - Returns the list of filter backends that this view requires. - """ - return list(self.filter_backends) - - # The following methods provide default implementations - # that you may want to override for more complex cases. - - def get_paginate_by(self): - """ - Return the size of pages to use with pagination. - - If `PAGINATE_BY_PARAM` is set it will attempt to get the page size - from a named query parameter in the url, eg. ?page_size=100 - - Otherwise defaults to using `self.paginate_by`. - """ - if self.paginate_by_param: - try: - return strict_positive_int( - self.request.query_params[self.paginate_by_param], - cutoff=self.max_paginate_by - ) - except (KeyError, ValueError): - pass - - return self.paginate_by - - def get_serializer_class(self): - """ - Return the class to use for the serializer. - Defaults to using `self.serializer_class`. - - You may want to override this if you need to provide different - serializations depending on the incoming request. - - (Eg. admins get full serialization, others get basic serialization) - """ - assert self.serializer_class is not None, ( - "'%s' should either include a `serializer_class` attribute, " - "or override the `get_serializer_class()` method." - % self.__class__.__name__ - ) - - return self.serializer_class + # The style to use for queryset pagination. + pagination_class = api_settings.DEFAULT_PAGINATION_CLASS def get_queryset(self): """ @@ -246,6 +99,73 @@ class GenericAPIView(views.APIView): return obj + def get_serializer(self, *args, **kwargs): + """ + Return the serializer instance that should be used for validating and + deserializing input, and for serializing output. + """ + serializer_class = self.get_serializer_class() + kwargs['context'] = self.get_serializer_context() + return serializer_class(*args, **kwargs) + + def get_serializer_class(self): + """ + Return the class to use for the serializer. + Defaults to using `self.serializer_class`. + + You may want to override this if you need to provide different + serializations depending on the incoming request. + + (Eg. admins get full serialization, others get basic serialization) + """ + assert self.serializer_class is not None, ( + "'%s' should either include a `serializer_class` attribute, " + "or override the `get_serializer_class()` method." + % self.__class__.__name__ + ) + + return self.serializer_class + + def get_serializer_context(self): + """ + Extra context provided to the serializer class. + """ + return { + 'request': self.request, + 'format': self.format_kwarg, + 'view': self + } + + def filter_queryset(self, queryset): + """ + Given a queryset, filter it with whichever filter backend is in use. + + You are unlikely to want to override this method, although you may need + to call it either from a list view, or from a custom `get_object` + method if you want to apply the configured filtering backend to the + default queryset. + """ + for backend in list(self.filter_backends): + queryset = backend().filter_queryset(self.request, queryset, self) + return queryset + + @property + def pager(self): + if not hasattr(self, '_pager'): + if self.pagination_class is None: + self._pager = None + else: + self._pager = self.pagination_class() + return self._pager + + def paginate_queryset(self, queryset): + if self.pager is None: + return None + return self.pager.paginate_queryset(queryset, self.request, view=self) + + def get_paginated_response(self, objects): + return self.pager.get_paginated_response(objects) + # Concrete view classes that provide method handlers # by composing the mixin classes with the base view. diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 2074a107..c34cfcee 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -5,7 +5,6 @@ We don't bind behaviour to http method handlers yet, which allows mixin classes to be composed in interesting ways. """ from __future__ import unicode_literals - from rest_framework import status from rest_framework.response import Response from rest_framework.settings import api_settings @@ -37,12 +36,14 @@ class ListModelMixin(object): List a queryset. """ def list(self, request, *args, **kwargs): - instance = self.filter_queryset(self.get_queryset()) - page = self.paginate_queryset(instance) + queryset = self.filter_queryset(self.get_queryset()) + + page = self.paginate_queryset(queryset) if page is not None: - serializer = self.get_pagination_serializer(page) - else: - serializer = self.get_serializer(instance, many=True) + serializer = self.get_serializer(page, many=True) + return self.get_paginated_response(serializer.data) + + serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) 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) diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 877d461b..3cce26b1 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -49,7 +49,7 @@ DEFAULTS = { 'DEFAULT_VERSIONING_CLASS': None, # Generic view behavior - 'DEFAULT_PAGINATION_SERIALIZER_CLASS': 'rest_framework.pagination.PaginationSerializer', + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'DEFAULT_FILTER_BACKENDS': (), # Throttling @@ -130,7 +130,7 @@ IMPORT_STRINGS = ( 'DEFAULT_CONTENT_NEGOTIATION_CLASS', 'DEFAULT_METADATA_CLASS', 'DEFAULT_VERSIONING_CLASS', - 'DEFAULT_PAGINATION_SERIALIZER_CLASS', + 'DEFAULT_PAGINATION_CLASS', 'DEFAULT_FILTER_BACKENDS', 'EXCEPTION_HANDLER', 'TEST_REQUEST_RENDERER_CLASSES', |
