Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions docs/developer/other-utilities.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----------------

Expand Down
17 changes: 17 additions & 0 deletions openwisp_utils/api/pagination.py
Original file line number Diff line number Diff line change
@@ -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"
7 changes: 6 additions & 1 deletion tests/test_project/api/urls.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
from django.urls import re_path
from rest_framework.routers import DefaultRouter

from . import views

router = DefaultRouter()
router.register(r"shelves", views.ShelfViewSet, basename="shelf")

urlpatterns = [
re_path(
r"^receive_project/(?P<pk>[^/\?]+)/$",
views.receive_project,
name="receive_project",
)
),
*router.urls,
]
13 changes: 12 additions & 1 deletion tests/test_project/api/views.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from django.http import JsonResponse
from django.utils.translation import gettext_lazy as _
from django.views import View
from openwisp_utils.api.pagination import OpenWispPagination
from rest_framework import viewsets

from ..models import Project
from ..models import Project, Shelf
from ..serializers import ShelfSerializer


class ReceiveProjectView(View):
Expand All @@ -24,3 +27,11 @@ def get(self, request, pk):


receive_project = ReceiveProjectView.as_view()


class ShelfViewSet(viewsets.ModelViewSet):
"""ViewSet for Shelf model with OpenWispPagination."""

queryset = Shelf.objects.all()
serializer_class = ShelfSerializer
pagination_class = OpenWispPagination
77 changes: 77 additions & 0 deletions tests/test_project/tests/test_api.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
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 test_project.serializers import ShelfSerializer

from ..models import Shelf
Expand Down Expand Up @@ -64,3 +66,78 @@ def test_rest_framework_settings_override(self):
"TEST": True,
},
)


class TestOpenWispPagination(CreateMixin, TestCase):
shelf_model = Shelf

def setUp(self):
super().setUp()
self.url = "/api/v1/shelves/"
# Create 21 shelves to test pagination across multiple pages
for i in range(21):
self._create_shelf(name=f"shelf{i}")

def test_list_shelf_api_pagination(self):
"""Test shelf list API with default pagination."""
number_of_shelves = 21
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["count"], number_of_shelves)
self.assertIsNotNone(response.data["next"])
self.assertIn("page=2", response.data["next"])
self.assertIsNone(response.data["previous"])
self.assertEqual(len(response.data["results"]), 10)

next_response = self.client.get(response.data["next"])
self.assertEqual(next_response.status_code, 200)
self.assertEqual(next_response.data["count"], number_of_shelves)
self.assertIsNotNone(next_response.data["next"])
self.assertIn("page=3", next_response.data["next"])
self.assertIsNotNone(next_response.data["previous"])
# Page 1 is the default, so DRF doesn't include page=1 in the previous URL
self.assertIn(self.url, next_response.data["previous"])
self.assertEqual(len(next_response.data["results"]), 10)

third_response = self.client.get(next_response.data["next"])
self.assertEqual(third_response.status_code, 200)
self.assertEqual(third_response.data["count"], number_of_shelves)
self.assertIsNone(third_response.data["next"])
self.assertIsNotNone(third_response.data["previous"])
self.assertIn("page=2", third_response.data["previous"])
self.assertEqual(len(third_response.data["results"]), 1)

def test_list_shelf_api_custom_page_size(self):
"""Test shelf list API with custom page_size parameter."""
number_of_shelves = 21
page_size = 5
url_with_page_size = f"{self.url}?page_size={page_size}"

response = self.client.get(url_with_page_size)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["count"], number_of_shelves)
self.assertIsNotNone(response.data["next"])
self.assertIn(f"page_size={page_size}", response.data["next"])
self.assertIn("page=2", response.data["next"])
self.assertIsNone(response.data["previous"])
self.assertEqual(len(response.data["results"]), page_size)

next_response = self.client.get(response.data["next"])
self.assertEqual(next_response.status_code, 200)
self.assertEqual(next_response.data["count"], number_of_shelves)
self.assertIsNotNone(next_response.data["next"])
self.assertIn(f"page_size={page_size}", next_response.data["next"])
self.assertIn("page=3", next_response.data["next"])
self.assertIsNotNone(next_response.data["previous"])
self.assertIn(f"page_size={page_size}", next_response.data["previous"])
# Page 1 is the default, so DRF doesn't include page=1 in the previous URL
self.assertIn(url_with_page_size, next_response.data["previous"])
self.assertEqual(len(next_response.data["results"]), page_size)

def test_pagination_attributes(self):
"""Test OpenWispPagination class attributes."""
pagination = OpenWispPagination()
self.assertIsInstance(pagination, PageNumberPagination)
self.assertEqual(pagination.page_size, 10)
self.assertEqual(pagination.max_page_size, 100)
self.assertEqual(pagination.page_size_query_param, "page_size")
Loading