This commit is contained in:
Stanislav Khlud 2025-04-10 19:00:29 +07:00 committed by GitHub
commit 48f7ced497
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 155 additions and 21 deletions

View File

@ -24,14 +24,16 @@ Pagination can be turned off by setting the pagination class to `None`.
## Setting the pagination style
The pagination style may be set globally, using the `DEFAULT_PAGINATION_CLASS` and `PAGE_SIZE` setting keys. For example, to use the built-in limit/offset pagination, you would do something like this:
The pagination style may be set globally, using the `DEFAULT_PAGINATION_CLASS`, `PAGE_SIZE` and `MAX_PAGE_SIZE` setting keys. For example, to use the built-in limit/offset pagination, you would do something like this:
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'PAGE_SIZE': 100
'PAGE_SIZE': 100,
'MAX_PAGE_SIZE': 250,
}
Note that you need to set both the pagination class, and the page size that should be used. Both `DEFAULT_PAGINATION_CLASS` and `PAGE_SIZE` are `None` by default.
Note that you need to set both the pagination class, and the page size and limit that should be used.
`DEFAULT_PAGINATION_CLASS`, `PAGE_SIZE`, `MAX_PAGE_SIZE` are `None` by default.
You can also set the pagination class on an individual view by using the `pagination_class` attribute. Typically you'll want to use the same pagination style throughout your API, although you might want to vary individual aspects of the pagination, such as default or maximum page size, on a per-view basis.

View File

@ -6,14 +6,16 @@ def pagination_system_check(app_configs, **kwargs):
errors = []
# Use of default page size setting requires a default Paginator class
from rest_framework.settings import api_settings
if api_settings.PAGE_SIZE and not api_settings.DEFAULT_PAGINATION_CLASS:
if (
api_settings.PAGE_SIZE or api_settings.MAX_PAGE_SIZE
) and not api_settings.DEFAULT_PAGINATION_CLASS:
errors.append(
Warning(
"You have specified a default PAGE_SIZE pagination rest_framework setting, "
"You have specified a default PAGE_SIZE pagination or MAX_PAGE_SIZE limit rest_framework setting, "
"without specifying also a DEFAULT_PAGINATION_CLASS.",
hint="The default for DEFAULT_PAGINATION_CLASS is None. "
"In previous versions this was PageNumberPagination. "
"If you wish to define PAGE_SIZE globally whilst defining "
"If you wish to define PAGE_SIZE or MAX_PAGE_SIZE globally whilst defining "
"pagination_class on a per-view basis you may silence this check.",
id="rest_framework.W001"
)

View File

@ -169,9 +169,6 @@ class PageNumberPagination(BasePagination):
http://api.example.org/accounts/?page=4
http://api.example.org/accounts/?page=4&page_size=100
"""
# The default page size.
# Defaults to `None`, meaning pagination is disabled.
page_size = api_settings.PAGE_SIZE
django_paginator_class = DjangoPaginator
@ -184,16 +181,33 @@ class PageNumberPagination(BasePagination):
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
last_page_strings = ('last',)
template = 'rest_framework/pagination/numbers.html'
invalid_page_message = _('Invalid page.')
@property
def page_size(self) -> int:
"""Get default page size.
Defaults to `None`, meaning pagination is disabled.
"""
return api_settings.PAGE_SIZE
@property
def max_page_size(self) -> int:
"""Limit page size.
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.
Defaults to `None`, meaning page size is unlimited.
It's recommended that you would set a limit to avoid api abuse.
"""
return api_settings.MAX_PAGE_SIZE
def paginate_queryset(self, queryset, request, view=None):
"""
Paginate a queryset if required, either returning a
@ -377,14 +391,33 @@ class LimitOffsetPagination(BasePagination):
http://api.example.org/accounts/?limit=100
http://api.example.org/accounts/?offset=400&limit=100
"""
default_limit = api_settings.PAGE_SIZE
limit_query_param = 'limit'
limit_query_description = _('Number of results to return per page.')
offset_query_param = 'offset'
offset_query_description = _('The initial index from which to return the results.')
max_limit = None
template = 'rest_framework/pagination/numbers.html'
@property
def max_limit(self) -> int:
"""Limit maximum page size.
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.
Defaults to `None`, meaning page size is unlimited.
It's recommended that you would set a limit to avoid api abuse.
"""
return api_settings.MAX_PAGE_SIZE
@property
def default_limit(self) -> int:
"""Get default page size.
Defaults to `None`, meaning pagination is disabled.
"""
return api_settings.PAGE_SIZE
def paginate_queryset(self, queryset, request, view=None):
self.request = request
self.limit = self.get_limit(request)
@ -588,7 +621,6 @@ class CursorPagination(BasePagination):
"""
cursor_query_param = 'cursor'
cursor_query_description = _('The pagination cursor value.')
page_size = api_settings.PAGE_SIZE
invalid_cursor_message = _('Invalid cursor')
ordering = '-created'
template = 'rest_framework/pagination/previous_and_next.html'
@ -598,16 +630,33 @@ class CursorPagination(BasePagination):
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
# queries, by having a hard cap on the maximum possible size of the offset.
offset_cutoff = 1000
@property
def page_size(self) -> int:
"""Get default page size.
Defaults to `None`, meaning pagination is disabled.
"""
return api_settings.PAGE_SIZE
@property
def max_page_size(self) -> int:
"""Limit page size.
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.
Defaults to `None`, meaning page size is unlimited.
It's recommended that you would set a limit to avoid api abuse.
"""
return api_settings.MAX_PAGE_SIZE
def paginate_queryset(self, queryset, request, view=None):
self.request = request
self.page_size = self.get_page_size(request)

View File

@ -65,6 +65,7 @@ DEFAULTS = {
# Pagination
'PAGE_SIZE': None,
"MAX_PAGE_SIZE": None,
# Filtering
'SEARCH_PARAM': 'search',

View File

@ -1,7 +1,7 @@
import pytest
from django.core.paginator import Paginator as DjangoPaginator
from django.db import models
from django.test import TestCase
from django.test import TestCase, override_settings
from rest_framework import (
exceptions, filters, generics, pagination, serializers, status
@ -135,6 +135,77 @@ class TestPaginationIntegration:
}
class TestPaginationSettingsIntegration:
"""
Integration tests for pagination settings.
"""
def setup_method(self):
class PassThroughSerializer(serializers.BaseSerializer):
def to_representation(self, item):
return item
class EvenItemsOnly(filters.BaseFilterBackend):
def filter_queryset(self, request, queryset, view):
return [item for item in queryset if item % 2 == 0]
class BasicPagination(pagination.PageNumberPagination):
page_size_query_param = 'page_size'
self.view = generics.ListAPIView.as_view(
serializer_class=PassThroughSerializer,
queryset=range(1, 101),
filter_backends=[EvenItemsOnly],
pagination_class=BasicPagination
)
@override_settings(
REST_FRAMEWORK={
"MAX_PAGE_SIZE": 20,
"PAGE_SIZE": 5,
}
)
def test_setting_page_size_over_maximum(self):
"""
When page_size parameter exceeds maximum allowable,
then it should be capped to the maximum.
"""
request = factory.get('/', {'page_size': 1000})
response = self.view(request)
assert response.status_code == status.HTTP_200_OK
assert len(response.data["results"]) == 20, response.data
assert response.data == {
'results': [
2, 4, 6, 8, 10, 12, 14, 16, 18, 20,
22, 24, 26, 28, 30, 32, 34, 36, 38, 40
],
'previous': None,
'next': 'http://testserver/?page=2&page_size=1000',
'count': 50
}
@override_settings(
REST_FRAMEWORK={
"MAX_PAGE_SIZE": 20,
"PAGE_SIZE": 5,
}
)
def test_setting_page_size_to_zero(self):
"""
When page_size parameter is invalid it should return to the default.
"""
request = factory.get('/', {'page_size': 0})
response = self.view(request)
assert response.status_code == status.HTTP_200_OK
assert len(response.data["results"]) == 5, response.data
assert response.data == {
'results': [2, 4, 6, 8, 10],
'previous': None,
'next': 'http://testserver/?page=2&page_size=0',
'count': 50
}
class TestPaginationDisabledIntegration:
"""
Integration tests for disabled pagination.

View File

@ -54,6 +54,7 @@ class TestSettings(TestCase):
return next((error for error in errors if error.id == error_id), None)
self.assertIsNone(api_settings.PAGE_SIZE)
self.assertIsNone(api_settings.MAX_PAGE_SIZE)
self.assertIsNone(api_settings.DEFAULT_PAGINATION_CLASS)
pagination_error = get_pagination_error('rest_framework.W001')
@ -63,11 +64,19 @@ class TestSettings(TestCase):
pagination_error = get_pagination_error('rest_framework.W001')
self.assertIsNotNone(pagination_error)
with override_settings(REST_FRAMEWORK={'MAX_PAGE_SIZE': 10}):
pagination_error = get_pagination_error('rest_framework.W001')
self.assertIsNotNone(pagination_error)
default_pagination_class = 'rest_framework.pagination.PageNumberPagination'
with override_settings(REST_FRAMEWORK={'PAGE_SIZE': 10, 'DEFAULT_PAGINATION_CLASS': default_pagination_class}):
pagination_error = get_pagination_error('rest_framework.W001')
self.assertIsNone(pagination_error)
with override_settings(REST_FRAMEWORK={'MAX_PAGE_SIZE': 10, 'DEFAULT_PAGINATION_CLASS': default_pagination_class}):
pagination_error = get_pagination_error('rest_framework.W001')
self.assertIsNone(pagination_error)
class TestSettingTypes(TestCase):
def test_settings_consistently_coerced_to_list(self):