Skip to content

Commit 469ee65

Browse files
pushpit kambojpushpit kamboj
authored andcommitted
[feature] Added OpenWispPagination class with suitable default values
1 parent 9f2ab62 commit 469ee65

3 files changed

Lines changed: 283 additions & 1 deletion

File tree

docs/developer/other-utilities.rst

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,100 @@ Every openwisp module which has an API should use this class to configure
8383
its own default settings, which will be merged with the settings of the
8484
other modules.
8585

86+
``openwisp_utils.api.pagination.OpenWispPagination``
87+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
88+
89+
A reusable pagination class for DRF views that provides consistent
90+
pagination behavior across OpenWISP modules with sensible defaults and
91+
flexible configuration.
92+
93+
**Default Behavior:**
94+
95+
- 10 items per page
96+
- Clients can request custom page sizes via ``?page_size=N`` query
97+
parameter
98+
- Maximum 100 items per page to prevent performance issues
99+
- Standard page navigation via ``?page=N``
100+
101+
**Configuration via Django Settings:**
102+
103+
The pagination class reads settings dynamically, making it compatible with
104+
Django's ``override_settings`` decorator in tests. The following settings
105+
can be used to customize the default pagination behavior:
106+
107+
- ``OPENWISP_PAGINATION_PAGE_SIZE`` (default: ``10``): Default number of
108+
items per page
109+
- ``OPENWISP_PAGINATION_MAX_PAGE_SIZE`` (default: ``100``): Maximum
110+
allowed page size
111+
- ``OPENWISP_PAGINATION_PAGE_SIZE_QUERY_PARAM`` (default:
112+
``"page_size"``): Query parameter name for page size
113+
114+
**Usage in Views:**
115+
116+
.. code-block:: python
117+
118+
from openwisp_utils.api.pagination import OpenWispPagination
119+
from rest_framework.viewsets import ModelViewSet
120+
121+
122+
class DeviceViewSet(ModelViewSet):
123+
queryset = Device.objects.all()
124+
serializer_class = DeviceSerializer
125+
pagination_class = OpenWispPagination
126+
127+
**Custom Configuration via Subclassing:**
128+
129+
For view-specific pagination requirements, you can subclass
130+
``OpenWispPagination`` and override the properties:
131+
132+
.. code-block:: python
133+
134+
from openwisp_utils.api.pagination import OpenWispPagination
135+
from rest_framework.viewsets import ModelViewSet
136+
137+
138+
class LargePagination(OpenWispPagination):
139+
@property
140+
def page_size(self):
141+
return 50
142+
143+
@property
144+
def max_page_size(self):
145+
return 500
146+
147+
148+
class ReportViewSet(ModelViewSet):
149+
queryset = Report.objects.all()
150+
serializer_class = ReportSerializer
151+
pagination_class = LargePagination
152+
153+
**Global Configuration:**
154+
155+
To use ``OpenWispPagination`` as the default pagination class for all DRF
156+
views in your project, add it to ``REST_FRAMEWORK`` settings:
157+
158+
.. code-block:: python
159+
160+
REST_FRAMEWORK = {
161+
"DEFAULT_PAGINATION_CLASS": "openwisp_utils.api.pagination.OpenWispPagination",
162+
}
163+
164+
**API Request Examples:**
165+
166+
.. code-block:: bash
167+
168+
# Returns first 10 items (default page size)
169+
GET /api/devices/
170+
171+
# Returns items 11-20 (second page)
172+
GET /api/devices/?page=2
173+
174+
# Returns first 25 items (custom page size)
175+
GET /api/devices/?page_size=25
176+
177+
# Returns items 26-50 with custom page size
178+
GET /api/devices/?page=2&page_size=25
179+
86180
Storage Utilities
87181
-----------------
88182

openwisp_utils/api/pagination.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from django.conf import settings
2+
from django.core.exceptions import ImproperlyConfigured
3+
4+
try:
5+
from rest_framework.pagination import PageNumberPagination
6+
except ImportError: # pragma: nocover
7+
raise ImproperlyConfigured(
8+
"Django REST Framework is required to use "
9+
"this feature but it is not installed"
10+
)
11+
12+
13+
class OpenWispPagination(PageNumberPagination):
14+
"""Reusable pagination class with sensible defaults.
15+
16+
Configurable via Django settings: - OPENWISP_PAGINATION_PAGE_SIZE
17+
(default: 10) - OPENWISP_PAGINATION_MAX_PAGE_SIZE (default: 100) -
18+
OPENWISP_PAGINATION_PAGE_SIZE_QUERY_PARAM (default: 'page_size')
19+
"""
20+
21+
@property
22+
def page_size(self):
23+
"""Return the page size from settings or default."""
24+
if hasattr(self, "_page_size"):
25+
return self._page_size
26+
return getattr(settings, "OPENWISP_PAGINATION_PAGE_SIZE", 10)
27+
28+
@page_size.setter
29+
def page_size(self, value):
30+
self._page_size = value
31+
32+
@property
33+
def max_page_size(self):
34+
if hasattr(self, "_max_page_size"):
35+
return self._max_page_size
36+
return getattr(settings, "OPENWISP_PAGINATION_MAX_PAGE_SIZE", 100)
37+
38+
@max_page_size.setter
39+
def max_page_size(self, value):
40+
"""Allow setting max_page_size."""
41+
self._max_page_size = value
42+
43+
@property
44+
def page_size_query_param(self):
45+
if hasattr(self, "_page_size_query_param"):
46+
return self._page_size_query_param
47+
return getattr(
48+
settings, "OPENWISP_PAGINATION_PAGE_SIZE_QUERY_PARAM", "page_size"
49+
)
50+
51+
@page_size_query_param.setter
52+
def page_size_query_param(self, value):
53+
self._page_size_query_param = value

tests/test_project/tests/test_api.py

Lines changed: 136 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
from django.conf import settings
22
from django.core.exceptions import ValidationError
3-
from django.test import TestCase
3+
from django.test import TestCase, override_settings
4+
from openwisp_utils.api.pagination import OpenWispPagination
5+
from rest_framework.pagination import PageNumberPagination
6+
from rest_framework.test import APIRequestFactory
47
from test_project.serializers import ShelfSerializer
58

69
from ..models import Shelf
@@ -64,3 +67,135 @@ def test_rest_framework_settings_override(self):
6467
"TEST": True,
6568
},
6669
)
70+
71+
72+
class TestOpenWispPagination(TestCase):
73+
"""Tests for the OpenWispPagination class."""
74+
75+
def setUp(self):
76+
self.factory = APIRequestFactory()
77+
self.pagination = OpenWispPagination()
78+
79+
def test_inheritance(self):
80+
"""Test that OpenWispPagination inherits from PageNumberPagination."""
81+
self.assertIsInstance(self.pagination, PageNumberPagination)
82+
83+
def test_default_page_size(self):
84+
"""Test that default page_size is 10."""
85+
pagination = OpenWispPagination()
86+
self.assertEqual(pagination.page_size, 10)
87+
88+
def test_default_max_page_size(self):
89+
"""Test that default max_page_size is 100."""
90+
pagination = OpenWispPagination()
91+
self.assertEqual(pagination.max_page_size, 100)
92+
93+
def test_default_page_size_query_param(self):
94+
"""Test that default page_size_query_param is 'page_size'."""
95+
pagination = OpenWispPagination()
96+
self.assertEqual(pagination.page_size_query_param, "page_size")
97+
98+
@override_settings(OPENWISP_PAGINATION_PAGE_SIZE=25)
99+
def test_custom_page_size_from_settings(self):
100+
"""Test that page_size can be overridden via settings."""
101+
pagination = OpenWispPagination()
102+
self.assertEqual(pagination.page_size, 25)
103+
104+
@override_settings(OPENWISP_PAGINATION_MAX_PAGE_SIZE=500)
105+
def test_custom_max_page_size_from_settings(self):
106+
"""Test that max_page_size can be overridden via settings."""
107+
pagination = OpenWispPagination()
108+
self.assertEqual(pagination.max_page_size, 500)
109+
110+
@override_settings(OPENWISP_PAGINATION_PAGE_SIZE_QUERY_PARAM="limit")
111+
def test_custom_page_size_query_param_from_settings(self):
112+
"""Test that page_size_query_param can be overridden via settings."""
113+
pagination = OpenWispPagination()
114+
self.assertEqual(pagination.page_size_query_param, "limit")
115+
116+
def test_page_size_setter(self):
117+
"""Test that page_size can be set directly."""
118+
pagination = OpenWispPagination()
119+
pagination.page_size = 50
120+
self.assertEqual(pagination.page_size, 50)
121+
122+
def test_max_page_size_setter(self):
123+
"""Test that max_page_size can be set directly."""
124+
pagination = OpenWispPagination()
125+
pagination.max_page_size = 200
126+
self.assertEqual(pagination.max_page_size, 200)
127+
128+
def test_page_size_query_param_setter(self):
129+
"""Test that page_size_query_param can be set directly."""
130+
pagination = OpenWispPagination()
131+
pagination.page_size_query_param = "per_page"
132+
self.assertEqual(pagination.page_size_query_param, "per_page")
133+
134+
135+
class TestOpenWispPaginationIntegration(CreateMixin, TestCase):
136+
"""Integration tests for OpenWispPagination with actual queryset."""
137+
138+
shelf_model = Shelf
139+
140+
def setUp(self):
141+
self.factory = APIRequestFactory()
142+
# Create test shelves
143+
for i in range(15):
144+
self._create_shelf(name=f"shelf{i}")
145+
146+
def _get_request(self, path="/api/shelves/", data=None):
147+
"""Create a DRF Request object with query_params support."""
148+
from rest_framework.request import Request
149+
150+
wsgi_request = self.factory.get(path, data)
151+
return Request(wsgi_request)
152+
153+
def test_paginate_queryset(self):
154+
"""Test that pagination works correctly with a queryset."""
155+
pagination = OpenWispPagination()
156+
request = self._get_request()
157+
queryset = Shelf.objects.all().order_by("id")
158+
paginated = pagination.paginate_queryset(queryset, request)
159+
# Default page size is 10
160+
self.assertEqual(len(paginated), 10)
161+
162+
def test_paginate_queryset_second_page(self):
163+
"""Test that pagination returns correct items for second page."""
164+
pagination = OpenWispPagination()
165+
request = self._get_request(data={"page": 2})
166+
queryset = Shelf.objects.all().order_by("id")
167+
paginated = pagination.paginate_queryset(queryset, request)
168+
# Remaining 5 items on second page
169+
self.assertEqual(len(paginated), 5)
170+
171+
def test_paginate_queryset_custom_page_size(self):
172+
"""Test pagination with custom page size via query parameter."""
173+
pagination = OpenWispPagination()
174+
request = self._get_request(data={"page_size": 5})
175+
queryset = Shelf.objects.all().order_by("id")
176+
paginated = pagination.paginate_queryset(queryset, request)
177+
self.assertEqual(len(paginated), 5)
178+
179+
def test_paginate_queryset_respects_max_page_size(self):
180+
"""Test that page_size is capped at max_page_size."""
181+
pagination = OpenWispPagination()
182+
pagination.max_page_size = 10
183+
# Request more than max
184+
request = self._get_request(data={"page_size": 100})
185+
queryset = Shelf.objects.all().order_by("id")
186+
paginated = pagination.paginate_queryset(queryset, request)
187+
# Should be capped at max_page_size (10)
188+
self.assertEqual(len(paginated), 10)
189+
190+
def test_get_paginated_response(self):
191+
"""Test that paginated response has correct structure."""
192+
pagination = OpenWispPagination()
193+
request = self._get_request()
194+
queryset = Shelf.objects.all().order_by("id")
195+
pagination.paginate_queryset(queryset, request)
196+
response = pagination.get_paginated_response([])
197+
self.assertIn("count", response.data)
198+
self.assertIn("next", response.data)
199+
self.assertIn("previous", response.data)
200+
self.assertIn("results", response.data)
201+
self.assertEqual(response.data["count"], 15)

0 commit comments

Comments
 (0)