diff options
| author | Tom Christie | 2015-03-06 10:22:32 +0000 | 
|---|---|---|
| committer | Tom Christie | 2015-03-06 10:22:32 +0000 | 
| commit | 58dfde7fcd9c29530d0613161dda0cf30c56a0a4 (patch) | |
| tree | 30ad934aee9221db06dfa8d277ced9876f3ce8c6 | |
| parent | f7917928c080aa0b055cbfc588f61ec01f16771c (diff) | |
| download | django-rest-framework-58dfde7fcd9c29530d0613161dda0cf30c56a0a4.tar.bz2 | |
Tweaks for cursor pagination and docs
| -rw-r--r-- | docs/api-guide/pagination.md | 12 | ||||
| -rw-r--r-- | docs/topics/3.1-announcement.md | 1 | ||||
| -rw-r--r-- | docs/topics/release-notes.md | 5 | ||||
| -rw-r--r-- | rest_framework/pagination.py | 28 | 
4 files changed, 33 insertions, 13 deletions
| diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md index 13bd57ae..14c0b7f2 100644 --- a/docs/api-guide/pagination.md +++ b/docs/api-guide/pagination.md @@ -51,7 +51,8 @@ You can then apply your new style to a view using the `.pagination_class` attrib  Or apply the style globally, using the `DEFAULT_PAGINATION_CLASS` settings key. For example:      REST_FRAMEWORK = { -        'DEFAULT_PAGINATION_CLASS': 'apps.core.pagination.StandardResultsSetPagination'
    } +        'DEFAULT_PAGINATION_CLASS': 'apps.core.pagination.StandardResultsSetPagination' +        }  --- @@ -163,6 +164,10 @@ Cursor based pagination is more complex than other schemes. It also requires tha  #### Details and limitations +Cursor based pagination requires a specified ordering to be applied to the queryset. This will default to `'-created'`, which requires the model being paged against to have a `'created'` field. + + +  This implementation of cursor pagination uses a smart "position plus offset" style that allows it to properly support not-strictly-unique values as the ordering.  It should be noted that using non-unique values the ordering does introduce the possibility of paging artifacts, where pagination consistency is no longer 100% guaranteed. @@ -192,7 +197,7 @@ To set these attributes you should override the `CursorPagination` class, and th  * `page_size` = A numeric value indicating the page size. If set, this overrides the `DEFAULT_PAGE_SIZE` setting. Defaults to the same value as the `DEFAULT_PAGE_SIZE` settings key.  * `cursor_query_param` = A string value indicating the name of the "cursor" query parameter. Defaults to `'cursor'`. -* `ordering` = This should be a string, or list of strings, indicating the field against which the cursor based pagination will be applied. For example: `ordering = 'created'`. Any filters on the view which define a `get_ordering` will override this attribute. Defaults to `None`. +* `ordering` = This should be a string, or list of strings, indicating the field against which the cursor based pagination will be applied. For example: `ordering = '-created'`. Any filters on the view which define a `get_ordering` will override this attribute. Defaults to `None`.  * `template` = The name of a template to use when rendering pagination controls in the browsable API. May be overridden to modify the rendering style, or set to `None` to disable HTML pagination controls completely. Defaults to `"rest_framework/pagination/previous_and_next.html"`.  --- @@ -236,7 +241,8 @@ Let's modify the built-in `PageNumberPagination` style, so that instead of inclu      class LinkHeaderPagination(pagination.PageNumberPagination):          def get_paginated_response(self, data): -            next_url = self.get_next_link()
            previous_url = self.get_previous_link() +            next_url = self.get_next_link() +            previous_url = self.get_previous_link()              if next_url is not None and previous_url is not None:                  link = '<{next_url}; rel="next">, <{previous_url}; rel="prev">' diff --git a/docs/topics/3.1-announcement.md b/docs/topics/3.1-announcement.md index ecbc9a38..6606d843 100644 --- a/docs/topics/3.1-announcement.md +++ b/docs/topics/3.1-announcement.md @@ -203,3 +203,4 @@ The next focus will be on HTML renderings of API output and will include:  This will either be made as a single 3.2 release, or split across two separate releases, with the HTML forms and filter controls coming in 3.2, and the admin-style interface coming in a 3.3 release.  [custom-exception-handler]: ../api-guide/exceptions.md#custom-exception-handling +[pagination]: ../api-guide/pagination.md diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 51eb45c3..35592feb 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,6 +40,11 @@ You can determine your currently installed version using `pip freeze`:  ## 3.0.x series +### 3.1.0 + +**Date**: [5th March 2015][3.1.0-milestone]. + +For full details see the [3.1 release announcement](3.1-announcement.md).  ### 3.0.5 diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 6a2f5b27..f41a9ae1 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -131,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)) @@ -472,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.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): @@ -680,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)), ( | 
