diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py index bb26ad963..56f173935 100644 --- a/djangorestframework/mixins.py +++ b/djangorestframework/mixins.py @@ -4,6 +4,7 @@ classes that can be added to a `View`. """ from django.contrib.auth.models import AnonymousUser +from django.core.paginator import Paginator from django.db.models.query import QuerySet from django.db.models.fields.related import ForeignKey from django.http import HttpResponse @@ -637,4 +638,92 @@ class ListModelMixin(object): queryset = queryset.order_by(*args) return queryset.filter(**kwargs) +########## Pagination Mixins ########## + +class PaginatorMixin(object): + """ + Adds pagination support to GET requests + Obviously should only be used on lists :) + + A default limit can be set by setting `limit` on the object. This will also + be used as the maximum if the client sets the `limit` GET param + """ + limit = 20 + + def get_limit(self): + """ Helper method to determine what the `limit` should be """ + try: + limit = int(self.request.GET.get('limit', self.limit)) + return min(limit, self.limit) + except ValueError: + return self.limit + + def url_with_page_number(self, page_number): + """ Constructs a url used for getting the next/previous urls """ + url = "%s?page=%d" % (self.request.path, page_number) + + limit = self.get_limit() + if limit != self.limit: + url = "%s&limit=%d" % (url, limit) + + return url + + def next(self, page): + """ Returns a url to the next page of results (if any) """ + if not page.has_next(): + return None + + return self.url_with_page_number(page.next_page_number()) + + def previous(self, page): + """ Returns a url to the previous page of results (if any) """ + if not page.has_previous(): + return None + + return self.url_with_page_number(page.previous_page_number()) + + def serialize_page_info(self, page): + """ This is some useful information that is added to the response """ + return { + 'next': self.next(page), + 'page': page.number, + 'pages': page.paginator.num_pages, + 'per_page': self.get_limit(), + 'previous': self.previous(page), + 'total': page.paginator.count, + } + + def filter_response(self, obj): + """ + Given the response content, paginate and then serialize. + + The response is modified to include to useful data relating to the number + of objects, number of pages, next/previous urls etc. etc. + + The serialised objects are put into `results` on this new, modified + response + """ + + # We don't want to paginate responses for anything other than GET requests + if self.method.upper() != 'GET': + return self._resource.filter_response(obj) + + paginator = Paginator(obj, self.get_limit()) + + try: + page_num = int(self.request.GET.get('page', '1')) + except ValueError: + page_num = 1 + + if page_num not in paginator.page_range: + raise ErrorResponse(status.HTTP_404_NOT_FOUND, {'detail': 'That page contains no results'}) + + page = paginator.page(page_num) + + serialized_object_list = self._resource.filter_response(page.object_list) + serialized_page_info = self.serialize_page_info(page) + + serialized_page_info['results'] = serialized_object_list + + return serialized_page_info