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.py103
1 files changed, 74 insertions, 29 deletions
diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py
index 80985873..5e60448d 100644
--- a/rest_framework/pagination.py
+++ b/rest_framework/pagination.py
@@ -10,7 +10,7 @@ from django.core.paginator import InvalidPage, Paginator as DjangoPaginator
from django.template import Context, loader
from django.utils import six
from django.utils.six.moves.urllib import parse as urlparse
-from django.utils.translation import ugettext as _
+from django.utils.translation import ugettext_lazy as _
from rest_framework.compat import OrderedDict
from rest_framework.exceptions import NotFound
from rest_framework.response import Response
@@ -18,6 +18,7 @@ from rest_framework.settings import api_settings
from rest_framework.utils.urls import (
replace_query_param, remove_query_param
)
+import warnings
def _positive_int(integer_string, strict=False, cutoff=None):
@@ -130,12 +131,19 @@ def _decode_cursor(encoded):
"""
Given a string representing an encoded cursor, return a `Cursor` instance.
"""
+
+ # The offset in the cursor is used in situations where we have a
+ # nearly-unique index. (Eg millisecond precision creation timestamps)
+ # We guard against malicious users attempting to cause expensive database
+ # queries, by having a hard cap on the maximum possible size of the offset.
+ OFFSET_CUTOFF = 1000
+
try:
querystring = b64decode(encoded.encode('ascii')).decode('ascii')
tokens = urlparse.parse_qs(querystring, keep_blank_values=True)
offset = tokens.get('o', ['0'])[0]
- offset = _positive_int(offset)
+ offset = _positive_int(offset, cutoff=OFFSET_CUTOFF)
reverse = tokens.get('r', ['0'])[0]
reverse = bool(int(reverse))
@@ -203,18 +211,18 @@ class PageNumberPagination(BasePagination):
"""
# The default page size.
# Defaults to `None`, meaning pagination is disabled.
- paginate_by = api_settings.PAGINATE_BY
+ page_size = api_settings.PAGE_SIZE
# 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
+ page_size_query_param = None
# 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
+ # Only relevant if 'page_size_query_param' has also been set.
+ max_page_size = None
last_page_strings = ('last',)
@@ -228,12 +236,48 @@ class PageNumberPagination(BasePagination):
attributes were set there. The attributes should now be set on
the pagination class, but the old style is still pending deprecation.
"""
- for attr in (
- 'paginate_by', 'page_query_param',
- 'paginate_by_param', 'max_paginate_by'
+ assert not (
+ getattr(view, 'pagination_serializer_class', None) or
+ getattr(api_settings, 'DEFAULT_PAGINATION_SERIALIZER_CLASS', None)
+ ), (
+ "The pagination_serializer_class attribute and "
+ "DEFAULT_PAGINATION_SERIALIZER_CLASS setting have been removed as "
+ "part of the 3.1 pagination API improvement. See the pagination "
+ "documentation for details on the new API."
+ )
+
+ for (settings_key, attr_name) in (
+ ('PAGINATE_BY', 'page_size'),
+ ('PAGINATE_BY_PARAM', 'page_size_query_param'),
+ ('MAX_PAGINATE_BY', 'max_page_size')
):
- if hasattr(view, attr):
- setattr(self, attr, getattr(view, attr))
+ value = getattr(api_settings, settings_key, None)
+ if value is not None:
+ setattr(self, attr_name, value)
+ warnings.warn(
+ "The `%s` settings key is pending deprecation. "
+ "Use the `%s` attribute on the pagination class instead." % (
+ settings_key, attr_name
+ ),
+ PendingDeprecationWarning,
+ )
+
+ for (view_attr, attr_name) in (
+ ('paginate_by', 'page_size'),
+ ('page_query_param', 'page_query_param'),
+ ('paginate_by_param', 'page_size_query_param'),
+ ('max_paginate_by', 'max_page_size')
+ ):
+ value = getattr(view, view_attr, None)
+ if value is not None:
+ setattr(self, attr_name, value)
+ warnings.warn(
+ "The `%s` view attribute is pending deprecation. "
+ "Use the `%s` attribute on the pagination class instead." % (
+ view_attr, attr_name
+ ),
+ PendingDeprecationWarning,
+ )
def paginate_queryset(self, queryset, request, view=None):
"""
@@ -264,7 +308,7 @@ class PageNumberPagination(BasePagination):
self.display_page_controls = True
self.request = request
- return self.page
+ return list(self.page)
def get_paginated_response(self, data):
return Response(OrderedDict([
@@ -275,17 +319,17 @@ class PageNumberPagination(BasePagination):
]))
def get_page_size(self, request):
- if self.paginate_by_param:
+ if self.page_size_query_param:
try:
return _positive_int(
- request.query_params[self.paginate_by_param],
+ request.query_params[self.page_size_query_param],
strict=True,
- cutoff=self.max_paginate_by
+ cutoff=self.max_page_size
)
except (KeyError, ValueError):
pass
- return self.paginate_by
+ return self.page_size
def get_next_link(self):
if not self.page.has_next():
@@ -336,7 +380,7 @@ class LimitOffsetPagination(BasePagination):
http://api.example.org/accounts/?limit=100
http://api.example.org/accounts/?offset=400&limit=100
"""
- default_limit = api_settings.PAGINATE_BY
+ default_limit = api_settings.PAGE_SIZE
limit_query_param = 'limit'
offset_query_param = 'offset'
max_limit = None
@@ -349,7 +393,7 @@ class LimitOffsetPagination(BasePagination):
self.request = request
if self.count > self.limit and self.template is not None:
self.display_page_controls = True
- return queryset[self.offset:self.offset + self.limit]
+ return list(queryset[self.offset:self.offset + self.limit])
def get_paginated_response(self, data):
return Response(OrderedDict([
@@ -435,14 +479,15 @@ class LimitOffsetPagination(BasePagination):
class CursorPagination(BasePagination):
- # Determine how/if True, False and None positions work - do the string
- # encodings work with Django queryset filters?
- # Consider a max offset cap.
- # Tidy up the `get_ordering` API (eg remove queryset from it)
+ """
+ The cursor pagination implementation is neccessarily complex.
+ For an overview of the position/offset style we use, see this post:
+ http://cramer.io/2011/03/08/building-cursors-for-the-disqus-api/
+ """
cursor_query_param = 'cursor'
- page_size = api_settings.PAGINATE_BY
+ page_size = api_settings.PAGE_SIZE
invalid_cursor_message = _('Invalid cursor')
- ordering = None
+ ordering = '-created'
template = 'rest_framework/pagination/previous_and_next.html'
def paginate_queryset(self, queryset, request, view=None):
@@ -484,7 +529,7 @@ class CursorPagination(BasePagination):
# We also always fetch an extra item in order to determine if there is a
# page following on from this one.
results = list(queryset[offset:offset + self.page_size + 1])
- self.page = results[:self.page_size]
+ self.page = list(results[:self.page_size])
# Determine the position of the final item following the page.
if len(results) > len(self.page):
@@ -643,12 +688,12 @@ class CursorPagination(BasePagination):
)
)
else:
- # The default case is to check for an `ordering` attribute,
- # first on the view instance, and then on this pagination instance.
- ordering = getattr(view, 'ordering', getattr(self, 'ordering', None))
+ # The default case is to check for an `ordering` attribute
+ # on this pagination instance.
+ ordering = self.ordering
assert ordering is not None, (
'Using cursor pagination, but no ordering attribute was declared '
- 'on the view or on the pagination class.'
+ 'on the pagination class.'
)
assert isinstance(ordering, (six.string_types, list, tuple)), (