Skip to content

Commit

Permalink
feat: add light pagination
Browse files Browse the repository at this point in the history
  • Loading branch information
joaodaher committed Oct 16, 2024
1 parent 1158a95 commit f249bcd
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 0 deletions.
1 change: 1 addition & 0 deletions drf_kit/pagination/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from drf_kit.pagination.custom_pagination import CustomPagePagination
from drf_kit.pagination.light_pagination import LightPagePagination
81 changes: 81 additions & 0 deletions drf_kit/pagination/light_pagination.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# ruff: noqa: ERA001
# We are keeping the comments here to show what is removed from parent implementation

from django.core.paginator import InvalidPage
from django.core.paginator import Page as DefaultPage
from django.core.paginator import Paginator as DefaultPaginator
from rest_framework.exceptions import NotFound
from rest_framework.response import Response

from drf_kit.pagination.custom_pagination import CustomPagePagination


class LightPage(DefaultPage):
def has_next(self):
return len(self.object_list) >= self.paginator.per_page


class LightPaginator(DefaultPaginator):
def validate_number(self, number):
try:
value = int(number)
except ValueError:
value = 0
return max(value, 0)

def page(self, number):
"""Return a Page object for the given 1-based page number."""
number = self.validate_number(number)
bottom: int = (number - 1) * self.per_page
top: int = bottom + self.per_page
# if top + self.orphans >= self.count:
# top = self.count
return LightPage(self.object_list[bottom:top], number, self)

@property
def count(self):
raise NotImplementedError()


class LightPagePagination(CustomPagePagination):
django_paginator_class = LightPaginator

def get_paginated_response(self, data):
return Response(
{
# 'count': self.page.paginator.count,
"next": self.get_next_link(),
"previous": self.get_previous_link(),
"results": data,
}
)

def paginate_queryset(self, queryset, request, view=None):
"""
Paginate a queryset if required, either returning a
page object, or `None` if pagination is not configured for this view.
"""
page_size = self.get_page_size(request)
if not page_size:
return None

paginator = self.django_paginator_class(queryset, page_size)
page_number = request.query_params.get(self.page_query_param, self.page_start)
if page_number in self.last_page_strings:
page_number = paginator.num_pages

try:
self.page = paginator.page(int(page_number) + self._shift)
except InvalidPage as exc:
msg = self.invalid_page_message.format(
page_number=page_number,
message=str(exc),
)
raise NotFound(msg) from exc

if self.template is not None:
# The browsable API should display pagination controls.
self.display_page_controls = True

self.request = request
return list(self.page)
63 changes: 63 additions & 0 deletions test_app/tests/tests_views/tests_paginated_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,66 @@ def test_invalid_page(self):

response = self.client.get(url, {"page_size": 12, "page": 15000})
self.assertResponseNotFound(response=response, expected_item={"detail": "Invalid page."})


class TestLightPaginatedView(TestPaginatedView):
url = "/spells-light"

def test_first_page(self):
url = self.url

response = self.client.get(url, {"page_size": 12})

self.assertEqual(list(range(1, 13)), [spell["id"] for spell in response.json()["results"]])
self.assertRegex(response.json()["next"], r"page=2")
self.assertEqual(None, response.json()["previous"])
self.assertNotIn("count", response.json())

def test_second_page(self):
url = self.url

response = self.client.get(url, {"page_size": 12, "page": 2})

self.assertEqual(list(range(13, 25)), [spell["id"] for spell in response.json()["results"]])
self.assertRegex(response.json()["next"], r"page=3")
self.assertNotRegex(response.json()["previous"], r"page=\d+")
self.assertNotIn("count", response.json())

def test_third_page(self):
url = self.url

response = self.client.get(url, {"page_size": 12, "page": 3})

self.assertEqual(list(range(25, 37)), [spell["id"] for spell in response.json()["results"]])
self.assertRegex(response.json()["next"], r"page=4")
self.assertRegex(response.json()["previous"], r"page=2")
self.assertNotIn("count", response.json())

def test_fourth_page(self):
url = self.url

response = self.client.get(url, {"page_size": 12, "page": 4})

self.assertEqual(list(range(37, 49)), [spell["id"] for spell in response.json()["results"]])
self.assertRegex(response.json()["next"], r"page=5")
self.assertRegex(response.json()["previous"], r"page=3")
self.assertNotIn("count", response.json())

def test_last_page(self):
url = self.url

response = self.client.get(url, {"page_size": 12, "page": 5})

self.assertEqual(list(range(49, 50)), [spell["id"] for spell in response.json()["results"]])
self.assertEqual(None, response.json().get("next"))
self.assertRegex(response.json()["previous"], r"page=4")
self.assertNotIn("count", response.json())

def test_invalid_page(self):
url = self.url

response = self.client.get(url, {"page_size": 12, "page": 15000})
self.assertEqual([], response.json()["results"])
self.assertEqual(None, response.json().get("next"))
self.assertRegex(response.json()["previous"], r"page=14999")
self.assertNotIn("count", response.json())
6 changes: 6 additions & 0 deletions test_app/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@
"spell",
)

router.register(
r"spells-light",
views.SpellLightViewSet,
"spell-light",
)

router.register(
r"wizards/(?P<wizard_id>[^/.]+)/patronus",
views.WizardPatronusViewSet,
Expand Down
5 changes: 5 additions & 0 deletions test_app/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from rest_framework import status
from rest_framework.response import Response

from drf_kit import pagination
from drf_kit.views import (
BulkMixin,
ModelViewSet,
Expand Down Expand Up @@ -85,6 +86,10 @@ class SpellViewSet(ReadOnlyModelViewSet):
serializer_detail_class = serializers.SpellSerializer


class SpellLightViewSet(SpellViewSet):
pagination_class = pagination.LightPagePagination


class SpellCastViewSet(ModelViewSet):
queryset = models.SpellCast.objects.all()
serializer_class = serializers.SpellCastSerializer
Expand Down

0 comments on commit f249bcd

Please sign in to comment.