Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for page_size parameter in CursorPaginator class #5250

Merged
merged 1 commit into from
Sep 25, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion rest_framework/pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,15 @@ class CursorPagination(BasePagination):
ordering = '-created'
template = 'rest_framework/pagination/previous_and_next.html'

# Client can control the page size using this query parameter.
# Default is 'None'. Set to eg 'page_size' to enable usage.
page_size_query_param = None
page_size_query_description = _('Number of results to return per page.')

# Set to an integer to limit the maximum page size the client may request.
# Only relevant if 'page_size_query_param' has also been set.
max_page_size = None

# 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
Expand Down Expand Up @@ -566,6 +575,16 @@ def paginate_queryset(self, queryset, request, view=None):
return self.page

def get_page_size(self, request):
if self.page_size_query_param:
try:
return _positive_int(
request.query_params[self.page_size_query_param],
strict=True,
cutoff=self.max_page_size
)
except (KeyError, ValueError):
pass

return self.page_size

def get_next_link(self):
Expand Down Expand Up @@ -779,7 +798,7 @@ def to_html(self):
def get_schema_fields(self, view):
assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`'
assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`'
return [
fields = [
coreapi.Field(
name=self.cursor_query_param,
required=False,
Expand All @@ -790,3 +809,16 @@ def get_schema_fields(self, view):
)
)
]
if self.page_size_query_param is not None:
fields.append(
coreapi.Field(
name=self.page_size_query_param,
required=False,
location='query',
schema=coreschema.Integer(
title='Page size',
description=force_text(self.page_size_query_description)
)
)
)
return fields
162 changes: 162 additions & 0 deletions tests/test_pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,164 @@ def test_cursor_pagination(self):

assert isinstance(self.pagination.to_html(), type(''))

def test_cursor_pagination_with_page_size(self):
(previous, current, next, previous_url, next_url) = self.get_pages('/?page_size=20')

assert previous is None
assert current == [1, 1, 1, 1, 1, 1, 2, 3, 4, 4, 4, 4, 5, 6, 7, 7, 7, 7, 7, 7]
assert next == [7, 7, 7, 8, 9, 9, 9, 9, 9, 9]

(previous, current, next, previous_url, next_url) = self.get_pages(next_url)
assert previous == [1, 1, 1, 1, 1, 1, 2, 3, 4, 4, 4, 4, 5, 6, 7, 7, 7, 7, 7, 7]
assert current == [7, 7, 7, 8, 9, 9, 9, 9, 9, 9]
assert next is None

def test_cursor_pagination_with_page_size_over_limit(self):
(previous, current, next, previous_url, next_url) = self.get_pages('/?page_size=30')

assert previous is None
assert current == [1, 1, 1, 1, 1, 1, 2, 3, 4, 4, 4, 4, 5, 6, 7, 7, 7, 7, 7, 7]
assert next == [7, 7, 7, 8, 9, 9, 9, 9, 9, 9]

(previous, current, next, previous_url, next_url) = self.get_pages(next_url)
assert previous == [1, 1, 1, 1, 1, 1, 2, 3, 4, 4, 4, 4, 5, 6, 7, 7, 7, 7, 7, 7]
assert current == [7, 7, 7, 8, 9, 9, 9, 9, 9, 9]
assert next is None

def test_cursor_pagination_with_page_size_zero(self):
(previous, current, next, previous_url, next_url) = self.get_pages('/?page_size=0')

assert previous is None
assert current == [1, 1, 1, 1, 1]
assert next == [1, 2, 3, 4, 4]

(previous, current, next, previous_url, next_url) = self.get_pages(next_url)

assert previous == [1, 1, 1, 1, 1]
assert current == [1, 2, 3, 4, 4]
assert next == [4, 4, 5, 6, 7]

(previous, current, next, previous_url, next_url) = self.get_pages(next_url)

assert previous == [1, 2, 3, 4, 4]
assert current == [4, 4, 5, 6, 7]
assert next == [7, 7, 7, 7, 7]

(previous, current, next, previous_url, next_url) = self.get_pages(next_url)

assert previous == [4, 4, 4, 5, 6] # Paging artifact
assert current == [7, 7, 7, 7, 7]
assert next == [7, 7, 7, 8, 9]

(previous, current, next, previous_url, next_url) = self.get_pages(next_url)

assert previous == [7, 7, 7, 7, 7]
assert current == [7, 7, 7, 8, 9]
assert next == [9, 9, 9, 9, 9]

(previous, current, next, previous_url, next_url) = self.get_pages(next_url)

assert previous == [7, 7, 7, 8, 9]
assert current == [9, 9, 9, 9, 9]
assert next is None

(previous, current, next, previous_url, next_url) = self.get_pages(previous_url)

assert previous == [7, 7, 7, 7, 7]
assert current == [7, 7, 7, 8, 9]
assert next == [9, 9, 9, 9, 9]

(previous, current, next, previous_url, next_url) = self.get_pages(previous_url)

assert previous == [4, 4, 5, 6, 7]
assert current == [7, 7, 7, 7, 7]
assert next == [8, 9, 9, 9, 9] # Paging artifact

(previous, current, next, previous_url, next_url) = self.get_pages(previous_url)

assert previous == [1, 2, 3, 4, 4]
assert current == [4, 4, 5, 6, 7]
assert next == [7, 7, 7, 7, 7]

(previous, current, next, previous_url, next_url) = self.get_pages(previous_url)

assert previous == [1, 1, 1, 1, 1]
assert current == [1, 2, 3, 4, 4]
assert next == [4, 4, 5, 6, 7]

(previous, current, next, previous_url, next_url) = self.get_pages(previous_url)

assert previous is None
assert current == [1, 1, 1, 1, 1]
assert next == [1, 2, 3, 4, 4]

def test_cursor_pagination_with_page_size_negative(self):
(previous, current, next, previous_url, next_url) = self.get_pages('/?page_size=-5')

assert previous is None
assert current == [1, 1, 1, 1, 1]
assert next == [1, 2, 3, 4, 4]

(previous, current, next, previous_url, next_url) = self.get_pages(next_url)

assert previous == [1, 1, 1, 1, 1]
assert current == [1, 2, 3, 4, 4]
assert next == [4, 4, 5, 6, 7]

(previous, current, next, previous_url, next_url) = self.get_pages(next_url)

assert previous == [1, 2, 3, 4, 4]
assert current == [4, 4, 5, 6, 7]
assert next == [7, 7, 7, 7, 7]

(previous, current, next, previous_url, next_url) = self.get_pages(next_url)

assert previous == [4, 4, 4, 5, 6] # Paging artifact
assert current == [7, 7, 7, 7, 7]
assert next == [7, 7, 7, 8, 9]

(previous, current, next, previous_url, next_url) = self.get_pages(next_url)

assert previous == [7, 7, 7, 7, 7]
assert current == [7, 7, 7, 8, 9]
assert next == [9, 9, 9, 9, 9]

(previous, current, next, previous_url, next_url) = self.get_pages(next_url)

assert previous == [7, 7, 7, 8, 9]
assert current == [9, 9, 9, 9, 9]
assert next is None

(previous, current, next, previous_url, next_url) = self.get_pages(previous_url)

assert previous == [7, 7, 7, 7, 7]
assert current == [7, 7, 7, 8, 9]
assert next == [9, 9, 9, 9, 9]

(previous, current, next, previous_url, next_url) = self.get_pages(previous_url)

assert previous == [4, 4, 5, 6, 7]
assert current == [7, 7, 7, 7, 7]
assert next == [8, 9, 9, 9, 9] # Paging artifact

(previous, current, next, previous_url, next_url) = self.get_pages(previous_url)

assert previous == [1, 2, 3, 4, 4]
assert current == [4, 4, 5, 6, 7]
assert next == [7, 7, 7, 7, 7]

(previous, current, next, previous_url, next_url) = self.get_pages(previous_url)

assert previous == [1, 1, 1, 1, 1]
assert current == [1, 2, 3, 4, 4]
assert next == [4, 4, 5, 6, 7]

(previous, current, next, previous_url, next_url) = self.get_pages(previous_url)

assert previous is None
assert current == [1, 1, 1, 1, 1]
assert next == [1, 2, 3, 4, 4]


class TestCursorPagination(CursorPaginationTestsMixin):
"""
Expand Down Expand Up @@ -671,6 +829,8 @@ def __getitem__(self, sliced):

class ExamplePagination(pagination.CursorPagination):
page_size = 5
page_size_query_param = 'page_size'
max_page_size = 20
ordering = 'created'

self.pagination = ExamplePagination()
Expand Down Expand Up @@ -727,6 +887,8 @@ class TestCursorPaginationWithValueQueryset(CursorPaginationTestsMixin, TestCase
def setUp(self):
class ExamplePagination(pagination.CursorPagination):
page_size = 5
page_size_query_param = 'page_size'
max_page_size = 20
ordering = 'created'

self.pagination = ExamplePagination()
Expand Down