Skip to content
Closed
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
15 changes: 15 additions & 0 deletions rest_framework/pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ class PageNumberPagination(BasePagination):
last_page_strings = ('last',)

template = 'rest_framework/pagination/numbers.html'
allow_negative_page_numbers = False

invalid_page_message = _('Invalid page.')

Expand Down Expand Up @@ -225,6 +226,14 @@ def get_page_number(self, request, paginator):
page_number = request.query_params.get(self.page_query_param) or 1
if page_number in self.last_page_strings:
page_number = paginator.num_pages
if self.allow_negative_page_numbers:
try:
page_number = int(page_number)
if page_number < 0:
page_number = paginator.num_pages + page_number
return max(page_number, 0)
except ValueError:
return page_number
return page_number

def get_paginated_response(self, data):
Expand Down Expand Up @@ -384,6 +393,7 @@ class LimitOffsetPagination(BasePagination):
offset_query_description = _('The initial index from which to return the results.')
max_limit = None
template = 'rest_framework/pagination/numbers.html'
allow_negative_offsets = False

def paginate_queryset(self, queryset, request, view=None):
self.request = request
Expand Down Expand Up @@ -447,6 +457,11 @@ def get_limit(self, request):

def get_offset(self, request):
try:
if self.allow_negative_offsets:
offset = int(request.query_params[self.offset_query_param])
if offset < 0:
offset = self.count + offset
return max(offset, 0)
return _positive_int(
request.query_params[self.offset_query_param],
)
Expand Down
70 changes: 70 additions & 0 deletions tests/test_pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,40 @@ def test_invalid_page(self):
with pytest.raises(exceptions.NotFound):
self.paginate_queryset(request)

def test_negative_page(self):
request = Request(factory.get('/', {'page': -1}))
print(request)
with pytest.raises(exceptions.NotFound):
self.paginate_queryset(request)

def test_allowed_negative_page(self):
self.pagination.allow_negative_page_numbers = True
request = Request(factory.get('/', {'page': -2}))
queryset = self.paginate_queryset(request)
content = self.get_paginated_content(queryset)
context = self.get_html_context()
assert queryset == [86, 87, 88, 89, 90]
assert content == {
'results': [86, 87, 88, 89, 90],
'previous': 'http://testserver/?page=17',
'next': 'http://testserver/?page=19',
'count': 100
}
assert context == {
'previous_url': 'http://testserver/?page=17',
'next_url': 'http://testserver/?page=19',
'page_links': [
PageLink('http://testserver/', 1, False, False),
PAGE_BREAK,
PageLink('http://testserver/?page=17', 17, False, False),
PageLink('http://testserver/?page=18', 18, True, False),
PageLink('http://testserver/?page=19', 19, False, False),
PageLink('http://testserver/?page=20', 20, False, False),
]
}
assert self.pagination.display_page_controls
assert isinstance(self.pagination.to_html(), str)

def test_get_paginated_response_schema(self):
unpaginated_schema = {
'type': 'object',
Expand Down Expand Up @@ -527,6 +561,42 @@ def test_invalid_offset(self):
queryset = self.paginate_queryset(request)
assert queryset == [1, 2, 3, 4, 5]

def test_negative_offset(self):
"""
A negative offset query param should be treated as 0.
"""
request = Request(factory.get('/', {'limit': 5, 'offset': -5}))
queryset = self.paginate_queryset(request)
assert queryset == [1, 2, 3, 4, 5]

def test_allowed_negative_offset(self):
"""
A negative offset query param should be treated as `count - offset`.
"""
self.pagination.allow_negative_offsets = True
request = Request(factory.get('/', {'limit': 5, 'offset': -10}))
queryset = self.paginate_queryset(request)
content = self.get_paginated_content(queryset)
context = self.get_html_context()
assert queryset == [91, 92, 93, 94, 95]
assert content == {
'results': [91, 92, 93, 94, 95],
'previous': 'http://testserver/?limit=5&offset=85',
'next': 'http://testserver/?limit=5&offset=95',
'count': 100
}
assert context == {
'previous_url': 'http://testserver/?limit=5&offset=85',
'next_url': 'http://testserver/?limit=5&offset=95',
'page_links': [
PageLink('http://testserver/?limit=5', 1, False, False),
PAGE_BREAK,
PageLink('http://testserver/?limit=5&offset=85', 18, False, False),
PageLink('http://testserver/?limit=5&offset=90', 19, True, False),
PageLink('http://testserver/?limit=5&offset=95', 20, False, False),
]
}

def test_invalid_limit(self):
"""
An invalid limit query param should be ignored in favor of the default.
Expand Down