From 1c4e5183716e3cbcd3d4f98e52a460774df24a0b Mon Sep 17 00:00:00 2001 From: pushpit kamboj Date: Fri, 6 Feb 2026 12:53:35 +0530 Subject: [PATCH] [feature] Added OpenWispPagination class with suitable default values --- docs/developer/other-utilities.rst | 39 ++++++++++++++++++ openwisp_utils/api/pagination.py | 17 ++++++++ tests/test_project/tests/test_api.py | 61 ++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+) create mode 100644 openwisp_utils/api/pagination.py diff --git a/docs/developer/other-utilities.rst b/docs/developer/other-utilities.rst index 5b145076..8a69212d 100644 --- a/docs/developer/other-utilities.rst +++ b/docs/developer/other-utilities.rst @@ -83,6 +83,45 @@ Every openwisp module which has an API should use this class to configure its own default settings, which will be merged with the settings of the other modules. +``openwisp_utils.api.pagination.OpenWispPagination`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A reusable pagination class for DRF views that provides consistent +pagination behavior across OpenWISP modules with sensible defaults. + +- **10** items per page (``page_size``) +- **100** max items per page (``max_page_size``) +- Custom page size via ``?page_size=N`` query parameter + +**Usage in Views:** + +.. code-block:: python + + from openwisp_utils.api.pagination import OpenWispPagination + from rest_framework.viewsets import ModelViewSet + + + class DeviceViewSet(ModelViewSet): + queryset = Device.objects.all() + serializer_class = DeviceSerializer + pagination_class = OpenWispPagination + +**API Request Examples:** + +.. code-block:: bash + + # Returns first 10 items (default page size) + GET /api/v1/controller/devices/ + + # Returns items 11-20 (second page) + GET /api/v1/controller/devices/?page=2 + + # Returns first 25 items (custom page size) + GET /api/v1/controller/devices/?page_size=25 + + # Returns items 26-50 with custom page size + GET /api/v1/controller/devices/?page=2&page_size=25 + Storage Utilities ----------------- diff --git a/openwisp_utils/api/pagination.py b/openwisp_utils/api/pagination.py new file mode 100644 index 00000000..495d0c0c --- /dev/null +++ b/openwisp_utils/api/pagination.py @@ -0,0 +1,17 @@ +from django.core.exceptions import ImproperlyConfigured + +try: + from rest_framework.pagination import PageNumberPagination +except ImportError: # pragma: nocover + raise ImproperlyConfigured( + "Django REST Framework is required to use " + "this feature but it is not installed" + ) + + +class OpenWispPagination(PageNumberPagination): + """Reusable pagination class with sensible defaults.""" + + page_size = 10 + max_page_size = 100 + page_size_query_param = "page_size" diff --git a/tests/test_project/tests/test_api.py b/tests/test_project/tests/test_api.py index a862d0b5..fd3f85a0 100644 --- a/tests/test_project/tests/test_api.py +++ b/tests/test_project/tests/test_api.py @@ -1,6 +1,10 @@ from django.conf import settings from django.core.exceptions import ValidationError from django.test import TestCase +from openwisp_utils.api.pagination import OpenWispPagination +from rest_framework.pagination import PageNumberPagination +from rest_framework.request import Request +from rest_framework.test import APIRequestFactory from test_project.serializers import ShelfSerializer from ..models import Shelf @@ -64,3 +68,60 @@ def test_rest_framework_settings_override(self): "TEST": True, }, ) + + +class TestOpenWispPagination(CreateMixin, TestCase): + shelf_model = Shelf + + def setUp(self): + self.factory = APIRequestFactory() + self.pagination = OpenWispPagination() + for i in range(15): + self._create_shelf(name=f"shelf{i}") + + def _get_request(self, path="/api/shelves/", data=None): + return Request(self.factory.get(path, data)) + + def test_inheritance(self): + self.assertIsInstance(self.pagination, PageNumberPagination) + + def test_default_attributes(self): + self.assertEqual(self.pagination.page_size, 10) + self.assertEqual(self.pagination.max_page_size, 100) + self.assertEqual(self.pagination.page_size_query_param, "page_size") + + def test_paginate_queryset(self): + request = self._get_request() + queryset = Shelf.objects.all().order_by("id") + paginated = self.pagination.paginate_queryset(queryset, request) + self.assertEqual(len(paginated), 10) + + def test_paginate_queryset_second_page(self): + request = self._get_request(data={"page": 2}) + queryset = Shelf.objects.all().order_by("id") + paginated = self.pagination.paginate_queryset(queryset, request) + self.assertEqual(len(paginated), 5) + + def test_paginate_queryset_custom_page_size(self): + request = self._get_request(data={"page_size": 5}) + queryset = Shelf.objects.all().order_by("id") + paginated = self.pagination.paginate_queryset(queryset, request) + self.assertEqual(len(paginated), 5) + + def test_paginate_queryset_respects_max_page_size(self): + self.pagination.max_page_size = 10 + request = self._get_request(data={"page_size": 100}) + queryset = Shelf.objects.all().order_by("id") + paginated = self.pagination.paginate_queryset(queryset, request) + self.assertEqual(len(paginated), 10) + + def test_get_paginated_response(self): + request = self._get_request() + queryset = Shelf.objects.all().order_by("id") + self.pagination.paginate_queryset(queryset, request) + response = self.pagination.get_paginated_response([]) + self.assertIn("count", response.data) + self.assertIn("next", response.data) + self.assertIn("previous", response.data) + self.assertIn("results", response.data) + self.assertEqual(response.data["count"], 15)