Skip to content

[feature] Added OpenWispPagination class with suitable default values#587

Open
pushpitkamboj wants to merge 2 commits intoopenwisp:masterfrom
pushpitkamboj:feature/reusable_pagination
Open

[feature] Added OpenWispPagination class with suitable default values#587
pushpitkamboj wants to merge 2 commits intoopenwisp:masterfrom
pushpitkamboj:feature/reusable_pagination

Conversation

@pushpitkamboj
Copy link
Contributor

Checklist

  • I have read the OpenWISP Contributing Guidelines.
  • I have manually tested the changes proposed in this pull request.
  • I have written new test cases for new code and/or updated existing tests for changes to existing code.
  • I have updated the documentation.

Reference to Existing Issue

Closes #586

Description of Changes

  • Created OpenWispPagination class inheriting from DRF's PageNumberPagination.
  • Set sensible defaults values based on common values used across OpenWISP repositories.
  • Added comprehensive documentation with usage examples.

@coderabbitai
Copy link

coderabbitai bot commented Feb 6, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a new DRF pagination class openwisp_utils.api.pagination.OpenWispPagination (subclasses DRF PageNumberPagination) with an import-time check that raises ImproperlyConfigured if DRF is missing. The class sets defaults page_size = 10, max_page_size = 100, and page_size_query_param = "page_size". Adds documentation in docs/developer/other-utilities.rst describing the class and usage, and tests in tests/test_project/tests/test_api.py verifying inheritance, default attributes, pagination behavior across pages, custom page_size, and enforcement of max_page_size.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant DRF_View as "DRF View"
    participant OpenWispPagination as "OpenWispPagination"
    participant Database
    Client->>DRF_View: HTTP GET /api/resources?page=2&page_size=10
    DRF_View->>OpenWispPagination: extract pagination params from request
    OpenWispPagination-->>DRF_View: page_size, max_page_size, page number
    DRF_View->>Database: query with limit/offset
    Database-->>DRF_View: result set
    DRF_View->>OpenWispPagination: paginate queryset
    OpenWispPagination-->>DRF_View: paginated results + metadata (count,next,previous)
    DRF_View-->>Client: HTTP 200 with paginated payload
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Description check ❓ Inconclusive The description covers key aspects (class creation, defaults, documentation) but the author acknowledges that tests were not added/updated despite the template requirement, which is a notable gap. Verify whether the test additions in test_api.py satisfy the testing requirement, or clarify if tests should be explicitly mentioned in the description.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: adding the OpenWispPagination class with default values, which is the primary objective of the PR.
Linked Issues check ✅ Passed The PR successfully addresses issue #586: creates a reusable OpenWispPagination class with sensible defaults (#586), provides documentation and usage examples (#586), and enables consistent pagination across OpenWISP modules (#586).
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #586: pagination class implementation, tests, and documentation. No unrelated or extraneous modifications are present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@openwisp_utils/api/pagination.py`:
- Around line 21-25: The class-level attributes page_size, max_page_size, and
page_size_query_param are read at import time so `@override_settings` won't affect
tests; change them to properties that read from django.conf.settings at access
time and add a setter for page_size so DRF's paginate_queryset can assign to
self.page_size at runtime. Specifically, replace the class attributes in the
pagination class with: a `@property` page_size that returns getattr(settings,
"OPENWISP_PAGINATION_PAGE_SIZE", 10) and a `@page_size.setter` that stores an
instance attribute (e.g., self._page_size) when assigned; implement `@property`
max_page_size and `@property` page_size_query_param that return getattr(settings,
"...", default) so tests using override_settings observe changes. Ensure
references in methods use
self.page_size/self.max_page_size/self.page_size_query_param.
🧹 Nitpick comments (2)
openwisp_utils/api/pagination.py (2)

4-10: Use raise ... from None to suppress exception chaining.

The raise inside the except ImportError block should use from None to produce a clean traceback focused on the ImproperlyConfigured error, rather than showing the original ImportError alongside it. This is also flagged by Ruff B904.

♻️ Proposed fix
 try:
     from rest_framework.pagination import PageNumberPagination
-except ImportError:  # pragma: nocover
+except ImportError:  # pragma: nocover
     raise ImproperlyConfigured(
         "Django REST Framework is required to use "
         "this feature but it is not installed"
-    )
+    ) from None

13-19: Docstring formatting is collapsed into a single hard-to-read line.

The settings list in the docstring is intended to be multi-line but renders as a single run-on sentence. Consider using proper reStructuredText or plain-text formatting.

📝 Suggested docstring formatting
 class OpenWispPagination(PageNumberPagination):
-    """Reusable pagination class with sensible defaults.
-
-    Configurable via Django settings: - OPENWISP_PAGINATION_PAGE_SIZE
-    (default: 10) - OPENWISP_PAGINATION_MAX_PAGE_SIZE (default: 100) -
-    OPENWISP_PAGINATION_PAGE_SIZE_QUERY_PARAM (default: 'page_size')
-    """
+    """Reusable pagination class with sensible defaults.
+
+    Configurable via Django settings:
+
+    - OPENWISP_PAGINATION_PAGE_SIZE (default: 10)
+    - OPENWISP_PAGINATION_MAX_PAGE_SIZE (default: 100)
+    - OPENWISP_PAGINATION_PAGE_SIZE_QUERY_PARAM (default: 'page_size')
+    """
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bb843d9 and 97eff50.

📒 Files selected for processing (2)
  • docs/developer/other-utilities.rst
  • openwisp_utils/api/pagination.py
🧰 Additional context used
🪛 Ruff (0.14.14)
openwisp_utils/api/pagination.py

[warning] 7-10: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


[warning] 7-10: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
  • GitHub Check: Python==3.11 | django~=5.0.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=5.0.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=5.0.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=4.2.0
🔇 Additional comments (1)
docs/developer/other-utilities.rst (1)

86-155: Documentation looks comprehensive and well-structured.

The documentation clearly covers defaults, settings configuration, view-level usage, subclassing, and global configuration. The code examples are accurate and follow established patterns in the file.

One minor suggestion: consider adding a note that when using the global configuration (Line 153-155), the PAGE_SIZE in REST_FRAMEWORK settings is not needed because OpenWispPagination already defines its own page_size via the OPENWISP_PAGINATION_PAGE_SIZE setting. This could prevent confusion for developers familiar with DRF's standard setup where both are typically configured together.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@coveralls
Copy link

coveralls commented Feb 6, 2026

Coverage Status

coverage: 97.254% (+0.005%) from 97.249%
when pulling 49353e0 on pushpitkamboj:feature/reusable_pagination
into 24e9775 on openwisp:master.

@pushpitkamboj pushpitkamboj force-pushed the feature/reusable_pagination branch from 97eff50 to 8b19ec1 Compare February 6, 2026 08:35
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@openwisp_utils/api/pagination.py`:
- Around line 4-10: Change the bare except ImportError to capture the exception
and re-raise ImproperlyConfigured with explicit exception chaining: use "except
ImportError as err" and re-raise the ImproperlyConfigured(...) using "from err"
so the original ImportError traceback is preserved; this applies to the import
of PageNumberPagination and the subsequent raise of ImproperlyConfigured in
openwisp_utils/api/pagination.py.
🧹 Nitpick comments (2)
openwisp_utils/api/pagination.py (2)

14-19: Docstring readability: consider a proper list format.

The settings are concatenated on a single wrapped line. A structured list would be easier to scan and render better in documentation tools.

Proposed fix
     """Reusable pagination class with sensible defaults.
 
-    Configurable via Django settings: - OPENWISP_PAGINATION_PAGE_SIZE
-    (default: 10) - OPENWISP_PAGINATION_MAX_PAGE_SIZE (default: 100) -
-    OPENWISP_PAGINATION_PAGE_SIZE_QUERY_PARAM (default: 'page_size')
+    Configurable via Django settings:
+
+    - OPENWISP_PAGINATION_PAGE_SIZE (default: 10)
+    - OPENWISP_PAGINATION_MAX_PAGE_SIZE (default: 100)
+    - OPENWISP_PAGINATION_PAGE_SIZE_QUERY_PARAM (default: 'page_size')
     """

32-41: Minor: inconsistent docstrings across properties.

page_size has a docstring on its getter (Line 23), while max_page_size has one on its setter (Line 40), and page_size_query_param has none at all. Consider either adding brief docstrings consistently to all getters (or none) to keep the class uniform.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 97eff50 and 8b19ec1.

📒 Files selected for processing (2)
  • docs/developer/other-utilities.rst
  • openwisp_utils/api/pagination.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/developer/other-utilities.rst
🧰 Additional context used
🪛 Ruff (0.14.14)
openwisp_utils/api/pagination.py

[warning] 7-10: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


[warning] 7-10: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=5.0.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=5.0.0
  • GitHub Check: Python==3.11 | django~=5.0.0
🔇 Additional comments (3)
openwisp_utils/api/pagination.py (3)

1-2: LGTM!

Clean, minimal imports.


21-30: LGTM — property-based approach correctly addresses the import-time binding issue.

The getter falls back to settings at runtime, and the setter accommodates DRF's internal self.page_size = ... assignment in paginate_queryset. Since DRF creates a new paginator instance per request, the instance-level override won't leak across requests.


43-53: LGTM!

Consistent property pattern with the other two attributes. No issues.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@docs/developer/other-utilities.rst`:
- Around line 132-150: The example is missing the import for ModelViewSet which
prevents copy-paste usage; add the import for ModelViewSet (from
rest_framework.viewsets import ModelViewSet) at the top of the snippet alongside
the OpenWispPagination import so the classes LargePagination and ReportViewSet
(which subclasses ModelViewSet and references OpenWispPagination) work as shown.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8b19ec1 and 0eaa83e.

📒 Files selected for processing (1)
  • docs/developer/other-utilities.rst
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=5.0.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=5.0.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=5.0.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=4.2.0

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@pushpitkamboj pushpitkamboj force-pushed the feature/reusable_pagination branch from 0eaa83e to 469ee65 Compare February 6, 2026 08:56
coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 6, 2026
Copy link
Member

@pandafy pandafy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for picking up this issue @pushpitkamboj. The implementation can be greatly simplified. See my comments below.

Comment on lines +21 to +53
@property
def page_size(self):
"""Return the page size from settings or default."""
if hasattr(self, "_page_size"):
return self._page_size
return getattr(settings, "OPENWISP_PAGINATION_PAGE_SIZE", 10)

@page_size.setter
def page_size(self, value):
self._page_size = value

@property
def max_page_size(self):
if hasattr(self, "_max_page_size"):
return self._max_page_size
return getattr(settings, "OPENWISP_PAGINATION_MAX_PAGE_SIZE", 100)

@max_page_size.setter
def max_page_size(self, value):
"""Allow setting max_page_size."""
self._max_page_size = value

@property
def page_size_query_param(self):
if hasattr(self, "_page_size_query_param"):
return self._page_size_query_param
return getattr(
settings, "OPENWISP_PAGINATION_PAGE_SIZE_QUERY_PARAM", "page_size"
)

@page_size_query_param.setter
def page_size_query_param(self, value):
self._page_size_query_param = value
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pushpitkamboj why do we need to use Python properties here? As far as I know, we can simple do the following:

class OpenWispPagination(PageNumberPagination):
    page_size = 20 
    max_page_size = 100
    page_size_query_param = page_size 

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isnt these just hard coded values?
we are not overriding the defaults from PageNumberPagination class?

Copy link
Member

@nemesifier nemesifier Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pandafy this change seems to contradict the issue description which mentions overridable settings.

@pushpit-kamboj properties should be simple, eg:

@property
def page_size(self):
    return app_settings.API_PAGE_SIZE

The setting being OPENWISP_API_PAGE_SIZE, these should be documented in the settings page.

Comment on lines +107 to +112
- ``OPENWISP_PAGINATION_PAGE_SIZE`` (default: ``10``): Default number of
items per page
- ``OPENWISP_PAGINATION_MAX_PAGE_SIZE`` (default: ``100``): Maximum
allowed page size
- ``OPENWISP_PAGINATION_PAGE_SIZE_QUERY_PARAM`` (default:
``"page_size"``): Query parameter name for page size
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While creating the issue, I didn't think we can benefit from having this configurable.

Since you have already implemented this, I will ask you to implement this properly:

  1. All settings are defined in openwisp_utils/settings.py
  2. The docs for the settings lives at docs/user/settings.rst. Pay attention to how other settings are defined.
  3. Use a hyperlink on this page to the settings.rst section to inform the users about these settings. E..g.
You can configure different properties of OpenWispPagination class by using `OPENWISP_PAGINATION_ATTRIBUTES <link to the section>`. 

Instead of adding three settings, can we just add one setting which is a dictionary?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pushpit-kamboj this suggestion was valid, we want settings, but done properly as @pandafy described.

Settings should be prefixed as OPENWISP_API_. I would get rid of the possibility of configuring the page size query param as it would cause incompatibility issues among adopters.

Comment on lines +129 to +152
For view-specific pagination requirements, you can subclass
``OpenWispPagination`` and override the properties:

.. code-block:: python

from openwisp_utils.api.pagination import OpenWispPagination
from rest_framework.viewsets import ModelViewSet


class LargePagination(OpenWispPagination):
@property
def page_size(self):
return 50

@property
def max_page_size(self):
return 500


class ReportViewSet(ModelViewSet):
queryset = Report.objects.all()
serializer_class = ReportSerializer
pagination_class = LargePagination

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After implementing above changes, this example can be largely simplified.

Comment on lines +153 to +163
**Global Configuration:**

To use ``OpenWispPagination`` as the default pagination class for all DRF
views in your project, add it to ``REST_FRAMEWORK`` settings:

.. code-block:: python

REST_FRAMEWORK = {
"DEFAULT_PAGINATION_CLASS": "openwisp_utils.api.pagination.OpenWispPagination",
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's avoid mentioning this here. This is a DRF setting which is documented in the DRF docs.

Comment on lines +164 to +179
**API Request Examples:**

.. code-block:: bash

# Returns first 10 items (default page size)
GET /api/devices/

# Returns items 11-20 (second page)
GET /api/devices/?page=2

# Returns first 25 items (custom page size)
GET /api/devices/?page_size=25

# Returns items 26-50 with custom page size
GET /api/devices/?page=2&page_size=25

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These examples are good, but let's use a real URL, e.g. /api/v1/controller/device/.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we create a simple DRF view in the test project for any model and then test the pagination on that view? The current tests look very synthetic and do not model real world scenario.

I will suggest you to look for test in other modules, e.g. openwisp-notification / openwisp-radius on how we write tests for pagination. Here's one quick instance.

https://github.com/openwisp/openwisp-notifications/blob/5a54a856096695f0d46e2992f20a51a0905621a9/openwisp_notifications/tests/test_api.py#L85-L139

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pushpit-kamboj I think @pandafy's suggestion is good, let's make sure the test project has an API view which uses this pagination class so we can also test manually, make sure the tests use that view.

@pushpitkamboj
Copy link
Contributor Author

Hi, sorry for late response I was working on other PR which was a priority. Will work on this ASAP.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@tests/test_project/tests/test_api.py`:
- Around line 73-127: Implement settings-based overrides for OpenWispPagination
by converting its page_size, max_page_size and page_size_query_param into
`@property` getters that read OPENWISP_PAGINATION_PAGE_SIZE,
OPENWISP_PAGINATION_MAX_PAGE_SIZE and OPENWISP_PAGINATION_PAGE_SIZE_QUERY_PARAM
from django.conf.settings with the existing defaults as fallbacks (modify
pagination.py and the OpenWispPagination class accordingly). Then add tests in
tests/test_api.py that use `@override_settings` to set custom values for those
OPENWISP_PAGINATION_* keys and assert that OpenWispPagination.paginate_queryset
and get_paginated_response honor the overridden page size, max page size and
query param (reference OpenWispPagination and the test methods in
TestOpenWispPagination to extend with new `@override_settings` cases).
🧹 Nitpick comments (2)
tests/test_project/tests/test_api.py (2)

118-127: Passing an empty list to get_paginated_response is misleading.

get_paginated_response([]) works because count is derived from the paginator's queryset (set by the prior paginate_queryset call), but the empty results contradicts the assertion context. Pass the actual paginated slice for clarity.

Suggested fix
     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([])
+        paginated = self.pagination.paginate_queryset(queryset, request)
+        response = self.pagination.get_paginated_response(paginated)
         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)

111-116: This test mutates the instance rather than testing the class default.

Directly setting self.pagination.max_page_size = 10 tests DRF's clamping logic, not OpenWispPagination's default max_page_size. Consider requesting page_size=200 (exceeding the default max_page_size=100) against the unmodified instance to verify the class's own cap is enforced.

Suggested fix
     def test_paginate_queryset_respects_max_page_size(self):
-        self.pagination.max_page_size = 10
-        request = self._get_request(data={'page_size': 100})
+        request = self._get_request(data={'page_size': 200})
         queryset = Shelf.objects.all().order_by('id')
         paginated = self.pagination.paginate_queryset(queryset, request)
-        self.assertEqual(len(paginated), 10)
+        # max_page_size=100 caps the request; only 15 items exist, so we get 15
+        self.assertEqual(len(paginated), 15)
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0eaa83e and 00fcb9d.

📒 Files selected for processing (3)
  • docs/developer/other-utilities.rst
  • openwisp_utils/api/pagination.py
  • tests/test_project/tests/test_api.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/developer/other-utilities.rst
🧰 Additional context used
🧬 Code graph analysis (1)
tests/test_project/tests/test_api.py (3)
openwisp_utils/api/pagination.py (1)
  • OpenWispPagination (12-17)
tests/test_project/tests/__init__.py (1)
  • _create_shelf (24-30)
tests/test_project/models.py (1)
  • Shelf (16-65)
🪛 Ruff (0.15.0)
openwisp_utils/api/pagination.py

[warning] 6-9: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


[warning] 6-9: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=5.0.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=5.0.0
  • GitHub Check: Python==3.10 | django~=5.0.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.11 | django~=4.2.0
🔇 Additional comments (2)
tests/test_project/tests/test_api.py (2)

73-81: Test class setup looks good.

Clean use of setUp with APIRequestFactory and the CreateMixin helper. The 15-shelf dataset is a sensible choice for exercising page boundaries with page_size=10.


85-91: LGTM — inheritance and default attribute tests are straightforward.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +73 to +127
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)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add tests for settings-based overrides once the feature is implemented.

Per the linked issue #586, the pagination class should support Django settings overrides. Once the property-based approach (suggested in pagination.py) is adopted, add tests using @override_settings to verify that custom OPENWISP_PAGINATION_PAGE_SIZE, OPENWISP_PAGINATION_MAX_PAGE_SIZE, and OPENWISP_PAGINATION_PAGE_SIZE_QUERY_PARAM values are respected.

🤖 Prompt for AI Agents
In `@tests/test_project/tests/test_api.py` around lines 73 - 127, Implement
settings-based overrides for OpenWispPagination by converting its page_size,
max_page_size and page_size_query_param into `@property` getters that read
OPENWISP_PAGINATION_PAGE_SIZE, OPENWISP_PAGINATION_MAX_PAGE_SIZE and
OPENWISP_PAGINATION_PAGE_SIZE_QUERY_PARAM from django.conf.settings with the
existing defaults as fallbacks (modify pagination.py and the OpenWispPagination
class accordingly). Then add tests in tests/test_api.py that use
`@override_settings` to set custom values for those OPENWISP_PAGINATION_* keys and
assert that OpenWispPagination.paginate_queryset and get_paginated_response
honor the overridden page size, max page size and query param (reference
OpenWispPagination and the test methods in TestOpenWispPagination to extend with
new `@override_settings` cases).

@pushpitkamboj pushpitkamboj force-pushed the feature/reusable_pagination branch from 00fcb9d to 1c4e518 Compare February 12, 2026 08:04
@nemesifier
Copy link
Member

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Mar 3, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Member

@nemesifier nemesifier left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work @pushpit-kamboj, there's 1 main recommendation to address, see below.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pushpit-kamboj I think @pandafy's suggestion is good, let's make sure the test project has an API view which uses this pagination class so we can also test manually, make sure the tests use that view.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (4)
openwisp_utils/api/pagination.py (2)

5-9: ⚠️ Potential issue | 🟡 Minor

Chain the caught import exception when re-raising ImproperlyConfigured.

This remains unchained; use except ImportError as err and raise ... from err (or from None) for clearer tracebacks.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_utils/api/pagination.py` around lines 5 - 9, The except block
catching ImportError should capture the exception and chain it when re-raising
ImproperlyConfigured: change the clause to "except ImportError as err" and raise
ImproperlyConfigured(...) from err (or from None if you want to suppress the
context); ensure the raised exception still contains the same message but uses
"from err" to preserve traceback chaining for ImproperlyConfigured.

15-17: ⚠️ Potential issue | 🟠 Major

Implement settings-backed pagination attributes instead of hardcoded constants.

The current class locks values at 10/100/"page_size" and does not satisfy the stated requirement to allow settings-based overrides.

🔧 Proposed fix (single settings dictionary)
+from django.conf import settings
 from django.core.exceptions import ImproperlyConfigured

@@
 class OpenWispPagination(PageNumberPagination):
     """Reusable pagination class with sensible defaults."""
 
-    page_size = 10
-    max_page_size = 100
-    page_size_query_param = "page_size"
+    _pagination_conf = getattr(settings, "OPENWISP_PAGINATION", {})
+    page_size = _pagination_conf.get("page_size", 10)
+    max_page_size = _pagination_conf.get("max_page_size", 100)
+    page_size_query_param = _pagination_conf.get("page_size_query_param", "page_size")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_utils/api/pagination.py` around lines 15 - 17, Replace the hardcoded
pagination defaults with settings-backed values: import Django settings and read
a single settings dict (e.g. OPENWISP_PAGINATION) and use its keys to set
page_size, max_page_size, and page_size_query_param in the pagination class
instead of the constants 10/100/"page_size"; fall back to the current defaults
when keys are missing so the class-level attributes (page_size, max_page_size,
page_size_query_param) initialize from settings.get or
getattr(OPENWISP_PAGINATION, ...) with defaults.
docs/developer/other-utilities.rst (1)

89-95: ⚠️ Potential issue | 🟡 Minor

Document the settings entry point for configuring these defaults.

Please add a direct hyperlink/ref to the relevant section in docs/user/settings.rst (for OPENWISP_PAGINATION) so users can discover configuration without reading source code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/developer/other-utilities.rst` around lines 89 - 95, Add a direct
cross-reference from the pagination docs to the settings entry by inserting a
link to the OPENWISP_PAGINATION section in docs/user/settings.rst immediately
after the pagination defaults list; reference the settings section title
"OPENWISP_PAGINATION" (use an RST cross-reference such as
:ref:`OPENWISP_PAGINATION` or a relative link to the settings document) so
readers can jump straight to configuration details without reading source code.
tests/test_project/tests/test_api.py (1)

73-127: ⚠️ Potential issue | 🟠 Major

Add at least one end-to-end pagination test through a real DRF view.

Current tests are useful but still synthetic; they don’t validate pagination wiring in an actual API view flow (query params → view pagination_class → serialized response).

🧪 Suggested direction
+class ShelfListView(ModelViewSet):
+    queryset = Shelf.objects.all().order_by("id")
+    serializer_class = ShelfSerializer
+    pagination_class = OpenWispPagination
+
+# call ShelfListView.as_view({"get": "list"}) with APIRequestFactory
+# assert response.data["results"] length and pagination keys
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_project/tests/test_api.py` around lines 73 - 127, Add an
end-to-end test that exercises OpenWispPagination through a real DRF view:
inside TestOpenWispPagination define a minimal serializer (e.g. ShelfSerializer
inheriting from rest_framework.serializers.ModelSerializer) and a small
ListAPIView subclass with queryset=Shelf.objects.all() and
pagination_class=OpenWispPagination (or use as_view() from a generic view), then
use APIClient or APIRequestFactory to perform an HTTP GET (including query
params like ?page=2 or ?page_size=5) against that view, parse the returned
Response and assert the JSON contains "count", "next", "previous", "results",
that count==15 and that len(results) matches requested page size (and respects
max_page_size) to validate full request→view→pagination→response wiring.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@docs/developer/other-utilities.rst`:
- Around line 89-95: Add a direct cross-reference from the pagination docs to
the settings entry by inserting a link to the OPENWISP_PAGINATION section in
docs/user/settings.rst immediately after the pagination defaults list; reference
the settings section title "OPENWISP_PAGINATION" (use an RST cross-reference
such as :ref:`OPENWISP_PAGINATION` or a relative link to the settings document)
so readers can jump straight to configuration details without reading source
code.

In `@openwisp_utils/api/pagination.py`:
- Around line 5-9: The except block catching ImportError should capture the
exception and chain it when re-raising ImproperlyConfigured: change the clause
to "except ImportError as err" and raise ImproperlyConfigured(...) from err (or
from None if you want to suppress the context); ensure the raised exception
still contains the same message but uses "from err" to preserve traceback
chaining for ImproperlyConfigured.
- Around line 15-17: Replace the hardcoded pagination defaults with
settings-backed values: import Django settings and read a single settings dict
(e.g. OPENWISP_PAGINATION) and use its keys to set page_size, max_page_size, and
page_size_query_param in the pagination class instead of the constants
10/100/"page_size"; fall back to the current defaults when keys are missing so
the class-level attributes (page_size, max_page_size, page_size_query_param)
initialize from settings.get or getattr(OPENWISP_PAGINATION, ...) with defaults.

In `@tests/test_project/tests/test_api.py`:
- Around line 73-127: Add an end-to-end test that exercises OpenWispPagination
through a real DRF view: inside TestOpenWispPagination define a minimal
serializer (e.g. ShelfSerializer inheriting from
rest_framework.serializers.ModelSerializer) and a small ListAPIView subclass
with queryset=Shelf.objects.all() and pagination_class=OpenWispPagination (or
use as_view() from a generic view), then use APIClient or APIRequestFactory to
perform an HTTP GET (including query params like ?page=2 or ?page_size=5)
against that view, parse the returned Response and assert the JSON contains
"count", "next", "previous", "results", that count==15 and that len(results)
matches requested page size (and respects max_page_size) to validate full
request→view→pagination→response wiring.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 00fcb9d and 49353e0.

📒 Files selected for processing (3)
  • docs/developer/other-utilities.rst
  • openwisp_utils/api/pagination.py
  • tests/test_project/tests/test_api.py
📜 Review details
🧰 Additional context used
🧬 Code graph analysis (1)
tests/test_project/tests/test_api.py (3)
openwisp_utils/api/pagination.py (1)
  • OpenWispPagination (12-17)
tests/test_project/tests/__init__.py (1)
  • _create_shelf (24-30)
tests/test_project/models.py (1)
  • Shelf (16-65)
🔇 Additional comments (2)
docs/developer/other-utilities.rst (1)

113-124: Good API examples with realistic endpoints.

These request examples are clear and practical for adopters.

tests/test_project/tests/test_api.py (1)

85-127: Solid baseline coverage for paginator behavior.

Inheritance, default attributes, page navigation, max-page-size enforcement, and response envelope checks are all covered well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feature] Re-usable pagination class for DRF views

5 participants