From b51141cb6958f356fca8c65efdcab8ffb43fa571 Mon Sep 17 00:00:00 2001 From: Stanislav Khlud Date: Wed, 21 Aug 2024 09:01:54 +0700 Subject: [PATCH] Move page settings to property --- rest_framework/pagination.py | 79 ++++++++++++++++++++++++++++-------- tests/test_pagination.py | 73 ++++++++++++++++++++++++++++++++- 2 files changed, 135 insertions(+), 17 deletions(-) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index ede9591ca..47b44ec80 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -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,18 +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. - # Defaults to `None`, meaning page size is unlimited. - # It's recommended that you would set a limit to avoid api abuse. - max_page_size = api_settings.MAX_PAGE_SIZE - 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 @@ -379,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 = api_settings.MAX_PAGE_SIZE 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) @@ -590,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' @@ -600,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 = api_settings.MAX_PAGE_SIZE - # 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) diff --git a/tests/test_pagination.py b/tests/test_pagination.py index 02d443ade..dcf924bc8 100644 --- a/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -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.