Skip to content

Commit

Permalink
Move page settings to property
Browse files Browse the repository at this point in the history
  • Loading branch information
TheSuperiorStanislav committed Aug 21, 2024
1 parent 91695fb commit b51141c
Show file tree
Hide file tree
Showing 2 changed files with 135 additions and 17 deletions.
79 changes: 63 additions & 16 deletions rest_framework/pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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'
Expand All @@ -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)
Expand Down
73 changes: 72 additions & 1 deletion tests/test_pagination.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -135,6 +135,77 @@ def test_404_not_found_for_invalid_page(self):
}


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.
Expand Down

0 comments on commit b51141c

Please sign in to comment.