Skip to content

Commit b51141c

Browse files
Move page settings to property
1 parent 91695fb commit b51141c

File tree

2 files changed

+135
-17
lines changed

2 files changed

+135
-17
lines changed

rest_framework/pagination.py

+63-16
Original file line numberDiff line numberDiff line change
@@ -169,9 +169,6 @@ class PageNumberPagination(BasePagination):
169169
http://api.example.org/accounts/?page=4
170170
http://api.example.org/accounts/?page=4&page_size=100
171171
"""
172-
# The default page size.
173-
# Defaults to `None`, meaning pagination is disabled.
174-
page_size = api_settings.PAGE_SIZE
175172

176173
django_paginator_class = DjangoPaginator
177174

@@ -184,18 +181,33 @@ class PageNumberPagination(BasePagination):
184181
page_size_query_param = None
185182
page_size_query_description = _('Number of results to return per page.')
186183

187-
# Set to an integer to limit the maximum page size the client may request.
188-
# Only relevant if 'page_size_query_param' has also been set.
189-
# Defaults to `None`, meaning page size is unlimited.
190-
# It's recommended that you would set a limit to avoid api abuse.
191-
max_page_size = api_settings.MAX_PAGE_SIZE
192-
193184
last_page_strings = ('last',)
194185

195186
template = 'rest_framework/pagination/numbers.html'
196187

197188
invalid_page_message = _('Invalid page.')
198189

190+
@property
191+
def page_size(self) -> int:
192+
"""Get default page size.
193+
194+
Defaults to `None`, meaning pagination is disabled.
195+
196+
"""
197+
return api_settings.PAGE_SIZE
198+
199+
@property
200+
def max_page_size(self) -> int:
201+
"""Limit page size.
202+
203+
Set to an integer to limit the maximum page size the client may request.
204+
Only relevant if 'page_size_query_param' has also been set.
205+
Defaults to `None`, meaning page size is unlimited.
206+
It's recommended that you would set a limit to avoid api abuse.
207+
208+
"""
209+
return api_settings.MAX_PAGE_SIZE
210+
199211
def paginate_queryset(self, queryset, request, view=None):
200212
"""
201213
Paginate a queryset if required, either returning a
@@ -379,14 +391,33 @@ class LimitOffsetPagination(BasePagination):
379391
http://api.example.org/accounts/?limit=100
380392
http://api.example.org/accounts/?offset=400&limit=100
381393
"""
382-
default_limit = api_settings.PAGE_SIZE
383394
limit_query_param = 'limit'
384395
limit_query_description = _('Number of results to return per page.')
385396
offset_query_param = 'offset'
386397
offset_query_description = _('The initial index from which to return the results.')
387-
max_limit = api_settings.MAX_PAGE_SIZE
388398
template = 'rest_framework/pagination/numbers.html'
389399

400+
@property
401+
def max_limit(self) -> int:
402+
"""Limit maximum page size.
403+
404+
Set to an integer to limit the maximum page size the client may request.
405+
Only relevant if 'page_size_query_param' has also been set.
406+
Defaults to `None`, meaning page size is unlimited.
407+
It's recommended that you would set a limit to avoid api abuse.
408+
409+
"""
410+
return api_settings.MAX_PAGE_SIZE
411+
412+
@property
413+
def default_limit(self) -> int:
414+
"""Get default page size.
415+
416+
Defaults to `None`, meaning pagination is disabled.
417+
418+
"""
419+
return api_settings.PAGE_SIZE
420+
390421
def paginate_queryset(self, queryset, request, view=None):
391422
self.request = request
392423
self.limit = self.get_limit(request)
@@ -590,7 +621,6 @@ class CursorPagination(BasePagination):
590621
"""
591622
cursor_query_param = 'cursor'
592623
cursor_query_description = _('The pagination cursor value.')
593-
page_size = api_settings.PAGE_SIZE
594624
invalid_cursor_message = _('Invalid cursor')
595625
ordering = '-created'
596626
template = 'rest_framework/pagination/previous_and_next.html'
@@ -600,16 +630,33 @@ class CursorPagination(BasePagination):
600630
page_size_query_param = None
601631
page_size_query_description = _('Number of results to return per page.')
602632

603-
# Set to an integer to limit the maximum page size the client may request.
604-
# Only relevant if 'page_size_query_param' has also been set.
605-
max_page_size = api_settings.MAX_PAGE_SIZE
606-
607633
# The offset in the cursor is used in situations where we have a
608634
# nearly-unique index. (Eg millisecond precision creation timestamps)
609635
# We guard against malicious users attempting to cause expensive database
610636
# queries, by having a hard cap on the maximum possible size of the offset.
611637
offset_cutoff = 1000
612638

639+
@property
640+
def page_size(self) -> int:
641+
"""Get default page size.
642+
643+
Defaults to `None`, meaning pagination is disabled.
644+
645+
"""
646+
return api_settings.PAGE_SIZE
647+
648+
@property
649+
def max_page_size(self) -> int:
650+
"""Limit page size.
651+
652+
Set to an integer to limit the maximum page size the client may request.
653+
Only relevant if 'page_size_query_param' has also been set.
654+
Defaults to `None`, meaning page size is unlimited.
655+
It's recommended that you would set a limit to avoid api abuse.
656+
657+
"""
658+
return api_settings.MAX_PAGE_SIZE
659+
613660
def paginate_queryset(self, queryset, request, view=None):
614661
self.request = request
615662
self.page_size = self.get_page_size(request)

tests/test_pagination.py

+72-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import pytest
22
from django.core.paginator import Paginator as DjangoPaginator
33
from django.db import models
4-
from django.test import TestCase
4+
from django.test import TestCase, override_settings
55

66
from rest_framework import (
77
exceptions, filters, generics, pagination, serializers, status
@@ -135,6 +135,77 @@ def test_404_not_found_for_invalid_page(self):
135135
}
136136

137137

138+
class TestPaginationSettingsIntegration:
139+
"""
140+
Integration tests for pagination settings.
141+
"""
142+
143+
def setup_method(self):
144+
class PassThroughSerializer(serializers.BaseSerializer):
145+
def to_representation(self, item):
146+
return item
147+
148+
class EvenItemsOnly(filters.BaseFilterBackend):
149+
def filter_queryset(self, request, queryset, view):
150+
return [item for item in queryset if item % 2 == 0]
151+
152+
class BasicPagination(pagination.PageNumberPagination):
153+
page_size_query_param = 'page_size'
154+
155+
self.view = generics.ListAPIView.as_view(
156+
serializer_class=PassThroughSerializer,
157+
queryset=range(1, 101),
158+
filter_backends=[EvenItemsOnly],
159+
pagination_class=BasicPagination
160+
)
161+
162+
@override_settings(
163+
REST_FRAMEWORK={
164+
"MAX_PAGE_SIZE": 20,
165+
"PAGE_SIZE": 5,
166+
}
167+
)
168+
def test_setting_page_size_over_maximum(self):
169+
"""
170+
When page_size parameter exceeds maximum allowable,
171+
then it should be capped to the maximum.
172+
"""
173+
request = factory.get('/', {'page_size': 1000})
174+
response = self.view(request)
175+
assert response.status_code == status.HTTP_200_OK
176+
assert len(response.data["results"]) == 20, response.data
177+
assert response.data == {
178+
'results': [
179+
2, 4, 6, 8, 10, 12, 14, 16, 18, 20,
180+
22, 24, 26, 28, 30, 32, 34, 36, 38, 40
181+
],
182+
'previous': None,
183+
'next': 'http://testserver/?page=2&page_size=1000',
184+
'count': 50
185+
}
186+
187+
@override_settings(
188+
REST_FRAMEWORK={
189+
"MAX_PAGE_SIZE": 20,
190+
"PAGE_SIZE": 5,
191+
}
192+
)
193+
def test_setting_page_size_to_zero(self):
194+
"""
195+
When page_size parameter is invalid it should return to the default.
196+
"""
197+
request = factory.get('/', {'page_size': 0})
198+
response = self.view(request)
199+
assert response.status_code == status.HTTP_200_OK
200+
assert len(response.data["results"]) == 5, response.data
201+
assert response.data == {
202+
'results': [2, 4, 6, 8, 10],
203+
'previous': None,
204+
'next': 'http://testserver/?page=2&page_size=0',
205+
'count': 50
206+
}
207+
208+
138209
class TestPaginationDisabledIntegration:
139210
"""
140211
Integration tests for disabled pagination.

0 commit comments

Comments
 (0)