From f249bcd6994a648a41220cf279b462c5450a6189 Mon Sep 17 00:00:00 2001 From: Joao Daher Date: Wed, 16 Oct 2024 15:13:22 -0300 Subject: [PATCH] feat: add light pagination --- drf_kit/pagination/__init__.py | 1 + drf_kit/pagination/light_pagination.py | 81 +++++++++++++++++++ .../tests_views/tests_paginated_views.py | 63 +++++++++++++++ test_app/urls.py | 6 ++ test_app/views.py | 5 ++ 5 files changed, 156 insertions(+) create mode 100644 drf_kit/pagination/light_pagination.py diff --git a/drf_kit/pagination/__init__.py b/drf_kit/pagination/__init__.py index 333a66f..79829d2 100644 --- a/drf_kit/pagination/__init__.py +++ b/drf_kit/pagination/__init__.py @@ -1 +1,2 @@ from drf_kit.pagination.custom_pagination import CustomPagePagination +from drf_kit.pagination.light_pagination import LightPagePagination diff --git a/drf_kit/pagination/light_pagination.py b/drf_kit/pagination/light_pagination.py new file mode 100644 index 0000000..cee1702 --- /dev/null +++ b/drf_kit/pagination/light_pagination.py @@ -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) diff --git a/test_app/tests/tests_views/tests_paginated_views.py b/test_app/tests/tests_views/tests_paginated_views.py index 942c698..8fc39aa 100644 --- a/test_app/tests/tests_views/tests_paginated_views.py +++ b/test_app/tests/tests_views/tests_paginated_views.py @@ -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()) diff --git a/test_app/urls.py b/test_app/urls.py index 14c9ab4..757e838 100644 --- a/test_app/urls.py +++ b/test_app/urls.py @@ -28,6 +28,12 @@ "spell", ) +router.register( + r"spells-light", + views.SpellLightViewSet, + "spell-light", +) + router.register( r"wizards/(?P[^/.]+)/patronus", views.WizardPatronusViewSet, diff --git a/test_app/views.py b/test_app/views.py index 13f9429..04c180a 100644 --- a/test_app/views.py +++ b/test_app/views.py @@ -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, @@ -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