Skip to content

[feature] Add support to reversion to the REST API #894#994

Open
dee077 wants to merge 5 commits intoopenwisp:masterfrom
dee077:feature/894-rest-api-revisions
Open

[feature] Add support to reversion to the REST API #894#994
dee077 wants to merge 5 commits intoopenwisp:masterfrom
dee077:feature/894-rest-api-revisions

Conversation

@dee077
Copy link
Copy Markdown
Member

@dee077 dee077 commented Mar 20, 2025

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 #894.

Description of Changes

Introduces three new REST API endpoints for managing revisions:

  • List all revisions with optional filtering by model:
    GET /controller/reversion/?model=<model_name>

  • Inspect a specific revision using its ID:
    GET /controller/reversion/<revision_id>/

  • Restore a revision based on its ID:
    POST /controller/reversion/<revision_id>/restore/

Let me know if any changes are needed in the response data whether anything should be included, excluded, or modified. Also, please share any suggestions for adding more filters or adjusting the response format for the restore endpoint

@dee077 dee077 force-pushed the feature/894-rest-api-revisions branch from 8690016 to 3fdfcaf Compare March 21, 2025 08:34
@coveralls
Copy link
Copy Markdown

coveralls commented Mar 21, 2025

Coverage Status

coverage: 98.642%. first build — dee077:feature/894-rest-api-revisions into openwisp:master

@dee077 dee077 force-pushed the feature/894-rest-api-revisions branch from 3fdfcaf to 98ca619 Compare March 21, 2025 09:03
Copy link
Copy Markdown
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.

Good work! The code is clean and well-structured.

However, this PR seems to be missing its main goal: ensuring that every versioned model admin has a corresponding versioned REST API that logs revisions—tracking who changed what—each time a modification is made.

Please check my comments below for further details.

Comment thread openwisp_controller/config/api/filters.py Outdated
Comment thread openwisp_controller/config/api/urls.py Outdated
@dee077 dee077 changed the title [feature] Rivision for Rest Api changes [feature] Revisions for Rest Api changes Mar 25, 2025
@nemesifier nemesifier changed the title [feature] Revisions for Rest Api changes [feature] Add support to reversion to the REST API Mar 27, 2025
@dee077 dee077 force-pushed the feature/894-rest-api-revisions branch from 84a68d6 to df5c3fa Compare April 12, 2025 13:56
@dee077
Copy link
Copy Markdown
Member Author

dee077 commented Apr 12, 2025

I’ve added REST API revision support using django-reversion. Here’s a summary

What’s Added

  • AutoRevisionMixin uses RevisionMixin suggested by @pandafy which wraps API requests in reversion.create_revision().
  • Automatically sets the user and a comment for each revision.

New Endpoints:

  • controller/<str:model_slug>/revision/ – List revisions
  • controller/<str:model_slug>/revision/<str:pk>/ – Revision detail
  • controller/<str:model_slug>/revision/<str:pk>/restore/ – Restore a revision
    These endpoints are placed above similar paths like controller/device/<str:pk>/ to avoid conflicts, as pk will never be revision.

How It Works:

  • Each change creates an entry in the reversion_revision table.
  • The corresponding object snapshot is stored in reversion_version with the same revision_id.

Issues Noticed

  • When using AutoRevisionMixin in the DeviceDetailView, the devicegroup_change_handler behaves unexpectedly, so the template doesn’t update on group change.
  • Fails the test: test_device_api_change_group.
  • Tried the mixin in other modules (connection, geo, pki), observed complexity increase (some entries show +10 additional SQL queries).

Question

Given the unexpected behavior in signals and added complexity:
Is this approach okay to proceed with, or alternative design for API revision support is needed?

Copy link
Copy Markdown
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.

Looks on the right track.

I will do a manual round of testing soon, in the meantime see my comments below.

We'll need to add this to pki, geo and connection.

Comment thread openwisp_controller/config/api/urls.py Outdated
'controller/<str:model_slug>/revision/<str:pk>/restore/',
api_views.revision_restore,
name='revision_restore',
),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What if we shorten this to just model? I think it would be clear enough and more concise. It's a small thing but nonetheless over time these simplifications help to keep maintenance sane.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I have another doubt on this, how are we enforcing that we are enabling this only on the models that support reversions in this app? Eg: Device, Template, Vpn, etc. Would this work for any model?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Currently, empty revisions are being stored with only user and comment entries, which does not offer anything.

Copy link
Copy Markdown
Member Author

@dee077 dee077 May 21, 2025

Choose a reason for hiding this comment

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

Added a fix for this
For reference these are the models that are getting revisions

Screenshot from 2025-05-22 02-40-35

Comment thread openwisp_controller/config/api/filters.py Outdated
Comment thread openwisp_controller/config/api/serializers.py
Comment thread openwisp_controller/config/api/serializers.py
Comment thread openwisp_controller/mixins.py Outdated
Comment thread openwisp_controller/mixins.py Outdated
Comment thread openwisp_controller/config/api/serializers.py
@pandafy pandafy moved this from To do (general) to In progress in OpenWISP Contributor's Board May 6, 2025
@dee077
Copy link
Copy Markdown
Member Author

dee077 commented May 16, 2025

Updates

  • Fix single quotes
  • Added the AutoRevisionMixin across the module. This caused 2 big issues
  • The test was failing due to AutoRevisionMixin wrapping the view in a transaction.atomic() block, which delayed the post_save signal (registered via transaction.on_commit) until the outermost transaction committed. Since the outer transaction wasn’t triggered when PUT request generating the response,that returned outdated data while the database was updated afterward. Resolved by setting revision_atomic = False in the mixin to avoid unnecessary nesting.
  • Some GeoTestsApi were inconsistently counting queries due to Django Reversion caching the ContentType lookup (used for versioning). On the first run, the lookup incurred extra queries, on subsequent runs, caching reduced them, leading to assertion failures. Fixed by adding ContentType.objects.clear_cache() in the setUp method of the test case to ensure consistent query counts.

Copy link
Copy Markdown
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.

Can you point to the lines of code that are generating the additional queries?

Have you found out what additional queries are being generated that before weren't?

If we can see the query it would be faster to verify whether all the queries are needed, the increase seems pretty significant.

@dee077 dee077 force-pushed the feature/894-rest-api-revisions branch from 34dbfcf to 47e1c70 Compare May 17, 2025 08:29
Copy link
Copy Markdown
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.

Can you point to the lines of code that are generating the additional queries?
Have you found out what additional queries are being generated that before weren't?

Here's a diff of the queries generated by the test_patch_deviceconnectoin_detail,

https://www.diffchecker.com/ToUeQeyZ/

From my analysis, it appears that django-rivision creates a version for all related objects (here - DeviceConnection, Config and Device). This is inline with VersionAdmin behaviour.

Comment thread openwisp_controller/config/api/serializers.py Outdated
Comment thread openwisp_controller/config/api/serializers.py Outdated
Comment thread openwisp_controller/config/api/filters.py Outdated
Comment thread openwisp_controller/config/api/views.py Outdated
Comment thread openwisp_controller/config/api/views.py Outdated
Comment thread openwisp_controller/config/api/views.py Outdated
@dee077
Copy link
Copy Markdown
Member Author

dee077 commented May 21, 2025

Updates

  • Again added test with increased queries.
  • The queryset uses Version objects because there is no reverse relationship from Revision to Version, but each Version is linked to a Revision.
  • Rename RevisionSerializer to VersionSerializer
  • In VersionSerializer directly defined the fields instead of using SerializerMethodField
  • Added a filter by revision_id in RevisionListView to allow retrieving versions linked to the same revision, which helps group related changes made at the same time.
  • Added test for revision_id filter
  • Small qa fix for double quotes.

@dee077
Copy link
Copy Markdown
Member Author

dee077 commented May 21, 2025

Updates

  • Removed RevisionFilter class as no longer using model filter by params
  • Added additional check for registered model

With this approach, only registered models will store revisions, while unregistered models can still include the mixin without any side effects. This allows in the future to safely add the mixin to new POST, PUT, or PATCH requests without needing to worry about whether the model is registered with reversion.

@dee077 dee077 force-pushed the feature/894-rest-api-revisions branch 2 times, most recently from 36b017e to 92b7954 Compare May 22, 2025 08:26
Copy link
Copy Markdown
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.

@dee077 can you please run openwisp-qa-format here again with the latest version of openwisp-utils? See openwisp/openwisp-utils#456.

@dee077 dee077 force-pushed the feature/894-rest-api-revisions branch 2 times, most recently from fd1faab to 3afb759 Compare June 4, 2025 09:49
Copy link
Copy Markdown
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.

I did a round of manual testing with the Templates model. Changing the model fields from the API endpoints creates revisions as expected. Kudos!


Is it possible to list all revisions of a particular object? Similar to this view
https://demo.openwisp.io/admin/config/template/e450012e-a482-48ad-9b2b-f0faa93b9cac/history/

Screenshot from 2025-06-10 18-42-38

If not, it really hurts the functionality of the REST API endpoints.

This end point is missing from the issue description. Let's discuss it with @nemesifier first before proceeding on this front.

Comment thread openwisp_controller/config/api/views.py Outdated
Comment on lines +312 to +318
def get_queryset(self):
model = self.kwargs.get("model").lower()
queryset = self.queryset.filter(content_type__model=model)
revision_id = self.request.query_params.get("revision_id")
if revision_id:
queryset = queryset.filter(revision_id=revision_id)
return self.queryset.filter(content_type__model=model)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The way this method is written breaks the convention of Django/DRF. It is also inconsistent with OpenWISP.

Suggested change
def get_queryset(self):
model = self.kwargs.get("model").lower()
queryset = self.queryset.filter(content_type__model=model)
revision_id = self.request.query_params.get("revision_id")
if revision_id:
queryset = queryset.filter(revision_id=revision_id)
return self.queryset.filter(content_type__model=model)
def get_queryset(self):
queryset = super().get_queryset()
model = self.kwargs.get("model", "").lower()
queryset = queryset.filter(content_type__model=model)
if revision_id := self.request.query_params.get("revision_id"):
queryset = queryset.filter(revision_id=revision_id)
return queryset

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Right now, it is not filtering the queryset with the revision_id, since the return statement filters the queryset with just content_type__model.

And, there's no test which verifies the filtering. This is bad.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Filtering using revision_id should be done with django_filters instead.

https://django-filter.readthedocs.io/en/stable/guide/rest_framework.html#quickstart

You can check out other API views for details.

Comment thread openwisp_controller/config/api/views.py Outdated
Comment on lines +327 to +329
def get_queryset(self):
model = self.kwargs.get("model").lower()
return self.queryset.filter(content_type__model=model)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
def get_queryset(self):
model = self.kwargs.get("model").lower()
return self.queryset.filter(content_type__model=model)
def get_queryset(self):
model = self.kwargs.get("model").lower()
return super().get_querset().filter(content_type__model=model)

Maybe, we can create a Mixin class which filters the queryset like this.

Comment thread openwisp_controller/config/api/views.py Outdated
Comment on lines +338 to +340
def get_queryset(self):
model = self.kwargs.get("model").lower()
return self.queryset.filter(content_type__model=model)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same here!

Comment thread openwisp_controller/config/api/views.py Outdated
Comment on lines +345 to +352
with transaction.atomic():
with reversion.create_revision():
for version in versions:
version.revert()
reversion.set_user(request.user)
reversion.set_comment(
f"Restored to previous revision: {self.kwargs.get('pk')}"
)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why don't we use Revision.revert() instead of reverting individual versions?

@nemesifier nemesifier changed the title [feature] Add support to reversion to the REST API [feature] Add support to reversion to the REST API #894 Aug 25, 2025
@github-project-automation github-project-automation Bot moved this to Backlog in 25.09 Release Sep 6, 2025
@nemesifier nemesifier moved this from Backlog to In progress in 25.09 Release Sep 9, 2025
@nemesifier nemesifier moved this from In progress to Backlog in 25.09 Release Sep 12, 2025
@dee077 dee077 force-pushed the feature/894-rest-api-revisions branch from 3afb759 to 27e70d9 Compare January 6, 2026 10:09
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 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 django-reversion support to the REST API. Introduces AutoRevisionMixin that wraps authenticated POST/PUT/PATCH requests for reversion-registered models to create revisions, set the revision user, and add a comment. Exposes revision data via ReversionSerializer and ReversionListFilter and adds three API endpoints: reversion list, detail, and restore. Applies AutoRevisionMixin across config, connection, geo, and pki API views and adds tests exercising listing, detail, and restore flows; many tests' query-count assertions were updated.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client/API Request
    participant View as API View (with AutoRevisionMixin)
    participant Mixin as AutoRevisionMixin
    participant Reversion as django-reversion
    participant DB as Database

    Client->>View: POST/PUT/PATCH (authenticated)
    View->>Mixin: dispatch(request)
    Note right of Mixin: check method & model registration
    alt model registered & mutation
        Mixin->>Reversion: create_revision(atomic=self.revision_atomic)
        Reversion->>DB: begin revision transaction
        Mixin->>View: call original dispatch()
        View->>DB: perform create/update
        Mixin->>Reversion: set revision user & comment
        Reversion->>DB: save Version records
        Reversion->>DB: commit transaction
        View-->>Client: response
    else not applicable
        Mixin->>View: call original dispatch()
        View-->>Client: response
    end
Loading
sequenceDiagram
    participant Client as Client
    participant RevAPI as Reversion API (List/Detail/Restore)
    participant Serializer as ReversionSerializer
    participant Reversion as django-reversion
    participant DB as Database

    Client->>RevAPI: GET /controller/{model}/reversion/
    RevAPI->>DB: query Version objects filtered by content_type and params
    DB-->>RevAPI: Version queryset
    RevAPI->>Serializer: serialize versions
    Serializer-->>RevAPI: JSON payload
    RevAPI-->>Client: response

    Client->>RevAPI: POST /controller/{model}/reversion/{pk}/restore/
    RevAPI->>DB: fetch Version(s) by revision_id / pk
    DB-->>RevAPI: Version and Revision
    RevAPI->>Reversion: revert revision inside transaction
    Reversion->>DB: restore object state(s)
    DB-->>RevAPI: commit
    RevAPI->>Serializer: serialize restored Version records
    RevAPI-->>Client: restored revision payload
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • nemesifier
  • devkapilbansal
  • codesankalp
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title '[feature] Add support to reversion to the REST API #894' follows the required format with correct type prefix and clearly relates to the main change of adding reversion support to REST API endpoints.
Description check ✅ Passed The PR description covers all required template sections: checklist items, issue reference (Closes #894), and detailed description of changes. Documentation update is noted as pending but other items are completed.
Linked Issues check ✅ Passed The PR implementation fully addresses issue #894 requirements: revisions are saved with user tracking via AutoRevisionMixin [config/views.py, mixins.py], three endpoints list/inspect/restore revisions [config/api/urls.py, views.py], django-reversion integration is applied across multiple models, and comprehensive tests were added [test_api.py].
Out of Scope Changes check ✅ Passed All changes are scoped to the reversion feature: AutoRevisionMixin implementation, revision API endpoints, serializers, filters, and integration across multiple API view modules are all directly aligned with issue #894 requirements.
Bug Fixes ✅ Passed PR adds new feature (REST API revision endpoints), not fixing a bug. Issue #894 is a feature request. Bug-fix check only applies if PR fixes a bug.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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
Copy Markdown
Contributor

@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: 4

🤖 Fix all issues with AI Agents
In @openwisp_controller/config/api/urls.py:
- Around line 16-30: RevisionListView.get_queryset currently builds a filtered
queryset into a local variable but returns self.queryset (ignoring the filter);
update RevisionListView.get_queryset to return the filtered queryset variable
(the one applying revision_id from request.GET) instead of self.queryset. Also
standardize error behavior across endpoints by replacing the use of
get_list_or_404 in RevisionRestoreView with the same safe filter/empty-list
pattern used by the list and detail views (i.e., perform a .filter(...) on the
model revisions and return an empty list when the model is invalid/unregistered
instead of raising 404), making the endpoints consistent.

In @openwisp_controller/config/tests/test_api.py:
- Around line 1662-1667: The test is asserting two results after filtering by
revision_id which is incorrect; update the test in
openwisp_controller/config/tests/test_api.py to assert the actual number of
versions for that specific revision (e.g., assert len(response_json) == 1 or
better: compute expected_count =
ModelVersion.objects.filter(revision_id=revision_id).count() and assert
equality), and if failures persist also fix RevisionListView.get_queryset to
apply the 'revision_id' GET filter (ensure get_queryset checks
request.GET.get("revision_id") and filters the queryset by revision_id before
returning).
🧹 Nitpick comments (2)
openwisp_controller/pki/tests/test_api.py (1)

26-28: Consider adding ContentType.objects.clear_cache() for test stability.

The geo tests added ContentType.objects.clear_cache() in setUp to prevent inconsistent query counts due to django-reversion's ContentType caching. This test class may benefit from the same approach to avoid potential flakiness.

🔎 Suggested fix
+from django.contrib.contenttypes.models import ContentType
+
 class TestPkiApi(
     AssertNumQueriesSubTestMixin,
     TestAdminMixin,
     TestPkiMixin,
     TestOrganizationMixin,
     AuthenticationMixin,
     TestCase,
 ):
     def setUp(self):
         super().setUp()
         self._login()
+        ContentType.objects.clear_cache()
openwisp_controller/connection/tests/test_api.py (1)

497-497: Add ContentType.objects.clear_cache() to setUp method for test stability.

The connection test's setUp method should include ContentType.objects.clear_cache() to match the pattern established in geo tests (line 304 of openwisp_controller/geo/tests/test_api.py). This helps prevent cache-related query count inconsistencies across test runs.

Also applies to: 542-542, 556-556

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d6323bb and 27e70d9.

📒 Files selected for processing (12)
  • openwisp_controller/config/api/serializers.py
  • openwisp_controller/config/api/urls.py
  • openwisp_controller/config/api/views.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/connection/api/views.py
  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/geo/api/views.py
  • openwisp_controller/geo/tests/test_api.py
  • openwisp_controller/mixins.py
  • openwisp_controller/pki/api/views.py
  • openwisp_controller/pki/tests/test_api.py
  • tests/openwisp2/sample_config/api/views.py
🧰 Additional context used
🧬 Code graph analysis (10)
openwisp_controller/mixins.py (1)
openwisp_controller/config/controller/views.py (1)
  • dispatch (71-72)
openwisp_controller/connection/tests/test_api.py (1)
openwisp_controller/config/tests/utils.py (1)
  • assertNumQueries (384-388)
openwisp_controller/connection/api/views.py (1)
openwisp_controller/mixins.py (1)
  • AutoRevisionMixin (43-62)
openwisp_controller/geo/tests/test_api.py (1)
openwisp_controller/config/tests/utils.py (1)
  • assertNumQueries (384-388)
openwisp_controller/pki/tests/test_api.py (1)
openwisp_controller/config/tests/utils.py (1)
  • assertNumQueries (384-388)
openwisp_controller/geo/api/views.py (1)
openwisp_controller/mixins.py (3)
  • AutoRevisionMixin (43-62)
  • ProtectedAPIMixin (39-40)
  • RelatedDeviceProtectedAPIMixin (30-36)
tests/openwisp2/sample_config/api/views.py (1)
openwisp_controller/config/api/views.py (3)
  • RevisionListView (306-318)
  • RevisionRestoreView (332-357)
  • VersionDetailView (321-329)
openwisp_controller/config/api/views.py (2)
openwisp_controller/mixins.py (1)
  • AutoRevisionMixin (43-62)
openwisp_controller/config/api/serializers.py (1)
  • VersionSerializer (382-404)
openwisp_controller/config/tests/test_api.py (1)
openwisp_controller/config/api/views.py (3)
  • post (141-147)
  • post (154-160)
  • post (342-357)
openwisp_controller/pki/api/views.py (1)
openwisp_controller/mixins.py (2)
  • AutoRevisionMixin (43-62)
  • ProtectedAPIMixin (39-40)
🪛 Ruff (0.14.10)
openwisp_controller/config/api/serializers.py

392-404: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

openwisp_controller/config/api/views.py

61-61: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


76-76: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


169-169: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


342-342: Unused method argument: args

(ARG002)

🔇 Additional comments (27)
openwisp_controller/geo/tests/test_api.py (2)

304-304: Good practice: Clearing ContentType cache for consistent query counts.

This addresses the test flakiness caused by Django Reversion caching ContentType lookups, which was causing inconsistent SQL query counts across test runs. This is the correct approach to stabilize the assertNumQueries assertions.


499-499: Query count adjustments for revision tracking overhead.

The increased query counts across these tests reflect the additional database operations introduced by AutoRevisionMixin for creating revisions on POST, PUT, and PATCH requests. These increases are expected and consistent with the revision-tracking feature.

Also applies to: 530-530, 541-541, 571-571, 582-582, 608-608, 632-632, 727-727, 765-765, 832-832, 872-872, 895-895, 914-914, 931-931, 945-945

openwisp_controller/pki/tests/test_api.py (1)

54-54: Query count adjustments are consistent with revision tracking.

The increased query counts for CA and certificate mutation operations (POST, PUT, PATCH, renew, revoke) correctly reflect the additional database operations from AutoRevisionMixin.

Also applies to: 64-64, 80-80, 100-100, 127-127, 140-140, 164-164, 175-175, 192-192, 208-208, 224-224, 256-256, 267-267, 292-292, 303-303

tests/openwisp2/sample_config/api/views.py (2)

108-117: LGTM!

The new revision-related view classes follow the established pattern in this sample config module, providing extension points for customization while delegating to the base implementations.


134-136: LGTM!

The view bindings are correctly exposed following the same pattern as other views in the module.

openwisp_controller/mixins.py (2)

43-62: Well-implemented revision tracking mixin.

The implementation correctly:

  • Limits revision creation to POST, PUT, PATCH (excluding DELETE per review feedback)
  • Checks reversion.is_registered(model) to avoid empty revisions for unregistered models
  • Uses revision_atomic = False to prevent transaction nesting issues with post_save handlers
  • Sets user and comment metadata within the revision context

The comment format "API request: {method} {path}" provides useful debugging context for support scenarios.


47-48: The defensive handling is already in place and works correctly.

The code safely handles views that don't define a queryset attribute (e.g., DeviceConnenctionListCreateView and DeviceConnectionDetailView which rely on get_queryset() only). When queryset is undefined, both qs and model become None, and the condition on line 52 (and model) prevents the revision block from executing—this is safe and acceptable behavior. However, consider whether revision tracking should be added to these connection detail views by explicitly defining their queryset attributes, as other views in the codebase do.

openwisp_controller/config/api/serializers.py (1)

382-404: VersionSerializer structure is well-designed.

The serializer correctly exposes revision metadata through the revision relation using the source parameter, which is cleaner than using SerializerMethodField as suggested in past reviews.

Note: The static analysis hint about ClassVar for the fields list is a false positive — Django REST Framework's Meta.fields is intentionally a mutable class attribute following DRF conventions.

openwisp_controller/config/tests/test_api.py (2)

1-1: LGTM!

Import added correctly to support revision-wrapped device creation in tests.


1676-1683: LGTM!

The restore test correctly verifies that the device is restored to its original name after calling the restore endpoint.

openwisp_controller/geo/api/views.py (4)

17-21: LGTM!

Import correctly updated to include AutoRevisionMixin for revision tracking.


64-66: LGTM!

AutoRevisionMixin correctly added to DeviceCoordinatesView. The mixin will safely skip revision creation if the model is not registered with reversion.


112-117: LGTM!

AutoRevisionMixin correctly added to DeviceLocationView.


213-215: LGTM!

AutoRevisionMixin consistently added to all location and floorplan views following the same pattern.

Also applies to: 223-227, 232-234, 242-246

openwisp_controller/connection/api/views.py (4)

16-21: LGTM!

Import correctly updated to include AutoRevisionMixin.


65-66: LGTM!

AutoRevisionMixin correctly added to CommandListCreateView.


85-95: LGTM!

AutoRevisionMixin correctly added to credential views.


125-141: LGTM!

AutoRevisionMixin correctly added to device connection views.

openwisp_controller/config/api/views.py (6)

1-7: LGTM!

Imports correctly added for reversion support, transaction handling, and Version model.


57-80: LGTM!

AutoRevisionMixin correctly added to template and VPN views.


93-160: LGTM!

AutoRevisionMixin correctly added to device-related views including list, detail, activate, and deactivate views.


163-189: LGTM!

AutoRevisionMixin correctly added to device group views.


321-329: Consider adding organization-based permission checks.

The VersionDetailView currently only filters by model type. Any authenticated user with API access can view version history of any object across all organizations. Depending on security requirements, you may want to add organization-based filtering similar to other protected views.


371-373: LGTM!

View instances correctly defined for the new revision endpoints.

openwisp_controller/pki/api/views.py (3)

13-13: LGTM!

Import correctly updated to include AutoRevisionMixin.


33-56: LGTM!

AutoRevisionMixin correctly added to CA views including list, detail, and renew operations.


69-84: LGTM!

AutoRevisionMixin correctly added to certificate views. The CertRevokeRenewBaseView base class ensures derived views (CertRevokeView, CertRenewView) also get revision tracking.

Comment thread openwisp_controller/config/api/serializers.py Outdated
Comment thread openwisp_controller/config/api/urls.py Outdated
Comment thread openwisp_controller/config/api/views.py Outdated
Comment thread openwisp_controller/config/tests/test_api.py Outdated
Copy link
Copy Markdown
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.

@dee077 there's conflicts with master

@github-project-automation github-project-automation Bot moved this from Backlog to In progress in 25.09 Release Jan 6, 2026
@github-project-automation github-project-automation Bot moved this from Backlog to In progress in 26.05 Release Jan 20, 2026
@dee077 dee077 force-pushed the feature/894-rest-api-revisions branch from 27e70d9 to 8edd817 Compare February 11, 2026 21:36
Copy link
Copy Markdown
Contributor

@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: 2

🤖 Fix all issues with AI agents
In `@openwisp_controller/connection/api/views.py`:
- Around line 136-145: The code in assert_parent_exists (called from initial)
references ValidationError and NotFound but they are not imported; add the
missing imports at the top of the module: import ValidationError from
django.core.exceptions and NotFound from rest_framework.exceptions so that the
exceptions used in assert_parent_exists and the NotFound raised for device_id
are defined.

In `@openwisp_controller/connection/tests/test_api.py`:
- Line 564: The POST deviceconnection query count rose due to repeated
ContentType and reversion work; update AutoRevisionMixin to cache ContentType
lookups (use ContentType.objects.get_for_model(model, for_concrete_model=False)
with a simple per-process or per-request dict on the mixin/class to avoid
repeated DB hits) and use that cached value when creating revisions, and also
ensure any queryset used by the device connection create path (the viewset or
DeviceConnection queryset) uses select_related for the FK fields involved in
revision creation so the revision logic doesn't trigger extra queries; remove
test reliance on ContentType.objects.clear_cache() once caching is in place.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 27e70d9 and 8edd817.

📒 Files selected for processing (12)
  • openwisp_controller/config/api/serializers.py
  • openwisp_controller/config/api/urls.py
  • openwisp_controller/config/api/views.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/connection/api/views.py
  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/geo/api/views.py
  • openwisp_controller/geo/tests/test_api.py
  • openwisp_controller/mixins.py
  • openwisp_controller/pki/api/views.py
  • openwisp_controller/pki/tests/test_api.py
  • tests/openwisp2/sample_config/api/views.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • openwisp_controller/config/api/urls.py
  • openwisp_controller/mixins.py
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2026-01-15T15:05:49.557Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/management/commands/clear_last_ip.py:38-42
Timestamp: 2026-01-15T15:05:49.557Z
Learning: In Django projects, when using select_related() to traverse relations (for example, select_related("organization__config_settings")), the traversed relation must not be deferred. If you also use .only() in the same query, include the relation name or FK field (e.g., "organization" or "organization_id") in the .only() list to avoid the error "Field X cannot be both deferred and traversed using select_related at the same time." Apply this guideline to Django code in openwisp_controller/config/management/commands/clear_last_ip.py and similar modules by ensuring any select_related with an accompanying only() includes the related field names to prevent deferred/traversed conflicts.

Applied to files:

  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/geo/tests/test_api.py
  • openwisp_controller/geo/api/views.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/api/serializers.py
  • openwisp_controller/pki/api/views.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/api/views.py
  • openwisp_controller/connection/api/views.py
📚 Learning: 2026-01-15T15:07:17.354Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/geo/estimated_location/tests/tests.py:172-175
Timestamp: 2026-01-15T15:07:17.354Z
Learning: In this repository, flake8 enforces E501 (line too long) via setup.cfg (max-line-length = 88) while ruff ignores E501 via ruff.toml. Therefore, use '# noqa: E501' on lines that intentionally exceed 88 characters to satisfy flake8 without affecting ruff checks. This applies to Python files across the project (any .py) and is relevant for tests as well. Use sparingly and only where breaking lines is not feasible without hurting readability or functionality.

Applied to files:

  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/geo/tests/test_api.py
  • tests/openwisp2/sample_config/api/views.py
  • openwisp_controller/geo/api/views.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/api/serializers.py
  • openwisp_controller/pki/api/views.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/api/views.py
  • openwisp_controller/connection/api/views.py
📚 Learning: 2026-01-12T22:27:48.342Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: tests/openwisp2/sample_config/migrations/0008_whoisinfo_organizationconfigsettings_whois_enabled.py:18-67
Timestamp: 2026-01-12T22:27:48.342Z
Learning: In tests/openwisp2/sample_config/models.py and corresponding test migrations, the WHOISInfo model intentionally includes an additional "details" field not present in the base AbstractWHOISInfo model. This is a testing pattern to verify that swappable models (CONFIG_WHOISINFO_MODEL) can be extended with custom fields without errors.

Applied to files:

  • openwisp_controller/geo/tests/test_api.py
🧬 Code graph analysis (10)
openwisp_controller/connection/tests/test_api.py (1)
openwisp_controller/config/tests/utils.py (1)
  • assertNumQueries (390-394)
openwisp_controller/geo/tests/test_api.py (1)
openwisp_controller/config/tests/utils.py (1)
  • assertNumQueries (390-394)
tests/openwisp2/sample_config/api/views.py (1)
openwisp_controller/config/api/views.py (5)
  • RevisionListView (306-318)
  • RevisionRestoreView (332-357)
  • TemplateDetailView (65-69)
  • TemplateListCreateView (57-62)
  • VersionDetailView (321-329)
openwisp_controller/geo/api/views.py (1)
openwisp_controller/mixins.py (2)
  • AutoRevisionMixin (41-60)
  • ProtectedAPIMixin (37-38)
openwisp_controller/config/tests/test_api.py (5)
openwisp_controller/geo/base/models.py (1)
  • device (60-61)
openwisp_controller/config/tests/utils.py (1)
  • _create_device (31-44)
openwisp_controller/config/base/base.py (1)
  • json (277-284)
openwisp_controller/config/api/views.py (3)
  • post (141-147)
  • post (154-160)
  • post (342-357)
openwisp_controller/config/models.py (1)
  • Device (15-22)
openwisp_controller/config/api/serializers.py (1)
openwisp_controller/serializers.py (1)
  • BaseSerializer (5-12)
openwisp_controller/pki/api/views.py (1)
openwisp_controller/mixins.py (2)
  • AutoRevisionMixin (41-60)
  • ProtectedAPIMixin (37-38)
openwisp_controller/pki/tests/test_api.py (1)
openwisp_controller/config/tests/utils.py (1)
  • assertNumQueries (390-394)
openwisp_controller/config/api/views.py (2)
openwisp_controller/mixins.py (1)
  • AutoRevisionMixin (41-60)
openwisp_controller/config/api/serializers.py (1)
  • VersionSerializer (382-404)
openwisp_controller/connection/api/views.py (1)
openwisp_controller/mixins.py (2)
  • AutoRevisionMixin (41-60)
  • ProtectedAPIMixin (37-38)
🪛 Ruff (0.15.0)
openwisp_controller/config/api/serializers.py

[warning] 392-404: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

openwisp_controller/config/api/views.py

[warning] 61-61: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


[warning] 76-76: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


[warning] 169-169: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


[warning] 342-342: Unused method argument: args

(ARG002)

openwisp_controller/connection/api/views.py

[error] 143-143: Undefined name ValidationError

(F821)


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

(B904)


[error] 145-145: Undefined name NotFound

(F821)

⏰ 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). (11)
  • GitHub Check: Python==3.12 | 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.1.0
  • GitHub Check: Python==3.10 | 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.10 | django~=5.2.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.11 | django~=4.2.0
🔇 Additional comments (13)
openwisp_controller/pki/tests/test_api.py (2)

50-55: CA query‑count updates look consistent with revision overhead.
Thanks for aligning the expected query budgets for CA create/update/renew paths with the new behavior.

Also applies to: 59-66, 70-83, 91-102, 122-129, 134-142, 160-166


171-177: Cert query‑count updates look consistent with revision overhead.
The new expectations for cert create/update/renew/revoke flows look in line with the added revision work.

Also applies to: 180-196, 203-210, 214-226, 247-258, 263-269, 288-294, 299-305

openwisp_controller/geo/api/views.py (1)

18-23: AutoRevisionMixin wiring across geo views looks consistent.
No concerns with the added mixin inheritance.

Also applies to: 98-101, 146-151, 299-301, 309-313, 318-320, 328-332

openwisp_controller/geo/tests/test_api.py (3)

401-405: ContentType cache reset in setUp is a good stabilization step.
This should keep the query‑count assertions deterministic across runs.


588-600: Location/FloorPlan query‑budget updates look aligned.
Thanks for updating the expected query counts to reflect the revision-aware flow.

Also applies to: 620-634, 638-645, 668-675, 679-686, 696-712, 716-736


824-840: DeviceLocation query‑budget updates look aligned.
The revised expectations here are consistent with the rest of the revision-enabled tests.

Also applies to: 852-871, 920-936, 961-978, 983-1001, 1006-1021, 1022-1038, 1039-1051

openwisp_controller/config/api/views.py (1)

57-190: AutoRevisionMixin integration across config views looks good.
No concerns with the updated inheritance.

tests/openwisp2/sample_config/api/views.py (1)

31-36: Revision view pass‑throughs are wired correctly.
No issues with the sample config wrappers and view bindings.

Also applies to: 108-117, 134-136

openwisp_controller/connection/api/views.py (1)

66-72: AutoRevisionMixin + parent‑existence guard integration looks good.
The added mixin wiring and existence checks are straightforward.

Also applies to: 102-110, 136-145, 151-167

openwisp_controller/connection/tests/test_api.py (1)

609-609: Consistent +9 query overhead for PUT/PATCH operations.

Same reversion overhead pattern as the POST test. No additional concerns beyond the comment above.

Also applies to: 623-623

openwisp_controller/pki/api/views.py (3)

33-33: AutoRevisionMixin consistently applied to all write-capable views.

MRO ordering is correct (ProtectedAPIMixinAutoRevisionMixin → DRF view), and CrlDownloadView is appropriately excluded since it's read-only. The mixin's guard (reversion.is_registered(model)) ensures safe behavior even if Ca/Cert models aren't registered with reversion.

Also applies to: 39-39, 44-44, 69-69, 75-77, 82-82


13-13: Import looks good.

Clean addition of AutoRevisionMixin alongside existing ProtectedAPIMixin import.


44-55: No action needed — Ca and Cert are properly registered with django-reversion.

Both CaAdmin and CertAdmin inherit from VersionAdmin, which automatically registers the models with django-reversion. The AutoRevisionMixin will successfully create revisions for renew/revoke operations on these models.

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

Comment thread openwisp_controller/connection/api/views.py Outdated
"failure_reason": "",
}
with self.assertNumQueries(12):
with self.assertNumQueries(23):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Query count nearly doubled (12 → 23) for POST deviceconnection.

The +11 query increase from reversion tracking is substantial. While the PR discussion acknowledges increased queries from ContentType lookups and reversion operations, this level of overhead on every write API call could impact performance under load.

Consider whether select_related or caching of ContentType lookups within AutoRevisionMixin could reduce this overhead. The PR comments mention ContentType.objects.clear_cache() was needed in tests, which suggests ContentType caching does help in production — worth verifying that it's effective at reducing repeated lookups in real requests.

🤖 Prompt for AI Agents
In `@openwisp_controller/connection/tests/test_api.py` at line 564, The POST
deviceconnection query count rose due to repeated ContentType and reversion
work; update AutoRevisionMixin to cache ContentType lookups (use
ContentType.objects.get_for_model(model, for_concrete_model=False) with a simple
per-process or per-request dict on the mixin/class to avoid repeated DB hits)
and use that cached value when creating revisions, and also ensure any queryset
used by the device connection create path (the viewset or DeviceConnection
queryset) uses select_related for the FK fields involved in revision creation so
the revision logic doesn't trigger extra queries; remove test reliance on
ContentType.objects.clear_cache() once caching is in place.

dee077 added 3 commits April 29, 2026 13:31
Implemented three endpoints:
1. List all revisions with optional filtering by model.
2. Inspect a specific revision by its ID.
3. Restore a revision using its ID.

 Fixes openwisp#894
@dee077 dee077 force-pushed the feature/894-rest-api-revisions branch from 8edd817 to 6d602b5 Compare May 4, 2026 23:37
@kilo-code-bot
Copy link
Copy Markdown

kilo-code-bot Bot commented May 4, 2026

Code Review Summary

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 1
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

CRITICAL

File Line Issue
openwisp_controller/config/api/views.py 323 Security: Reversion views missing authentication. ReversionListView, ReversionDetailView, and ReversionRestoreView removed ProtectedAPIMixin inheritance, leaving them without authentication/permission checks. All other API views in this codebase use ProtectedAPIMixin.

WARNING

File Line Issue
openwisp_controller/config/api/views.py 355 Logic issue: Looking up restored revision by (user, comment) is unreliable. Concurrent restores by the same user or duplicate comments could return wrong revision. Use the revision ID returned by revision.revert() or store the created revision reference directly.
Changes Since Last Review
  1. Multi-tenancy filtering added - BaseReversionView.get_queryset() now filters by organization for non-superusers (good security improvement)
  2. ProtectedAPIMixin removed from reversion views - This removes authentication requirements (security regression)
  3. ReversionRestoreView changed - Now fetches revision directly and looks up restored revision by user+comment
Files Reviewed (13 files)
  • openwisp_controller/mixins.py - AutoRevisionMixin added
  • openwisp_controller/config/api/views.py - Reversion views added (2 issues found)
  • openwisp_controller/config/api/serializers.py - ReversionSerializer added
  • openwisp_controller/config/api/filters.py - ReversionListFilter added
  • openwisp_controller/config/api/urls.py - Reversion URLs added
  • openwisp_controller/connection/api/views.py - AutoRevisionMixin applied
  • openwisp_controller/geo/api/views.py - AutoRevisionMixin applied
  • openwisp_controller/pki/api/views.py - AutoRevisionMixin applied
  • Test files updated with revised query counts

Fix these issues in Kilo Cloud


Reviewed by kimi-k2.5-0127 · 1,258,608 tokens

Copy link
Copy Markdown
Contributor

@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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
openwisp_controller/pki/tests/test_api.py (1)

26-28: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing ContentType.objects.clear_cache() in setUp — same flakiness risk as geo tests.

TestGeoApi.setUp added ContentType.objects.clear_cache() (see geo/tests/test_api.py line 407) specifically because reversion caches ContentType lookups in memory across tests, causing non-deterministic query counts depending on test execution order. TestPkiApi uses AssertNumQueriesSubTestMixin with tightly specified counts and is equally susceptible.

💡 Proposed fix
+from django.contrib.contenttypes.models import ContentType
 ...
 class TestPkiApi(...):
     def setUp(self):
         super().setUp()
         self._login()
+        ContentType.objects.clear_cache()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openwisp_controller/pki/tests/test_api.py` around lines 26 - 28, The
TestPkiApi setUp currently calls super().setUp() and self._login() but misses
clearing ContentType cache, which can cause flaky query-count assertions; update
TestPkiApi.setUp to call ContentType.objects.clear_cache() (via the django
ContentType manager) before executing any login or test setup logic so
reversion's cached ContentType lookups won't affect AssertNumQueriesSubTestMixin
assertions.
openwisp_controller/connection/tests/test_api.py (1)

430-432: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing ContentType.objects.clear_cache() in setUp — same risk as geo/PKI tests.

Reversion caches ContentType lookups in memory. If any earlier test in the suite has already warmed the DeviceConnection ContentType, the assertNumQueries(23) / assertNumQueries(22) assertions may see 1–2 fewer queries and fail non-deterministically. The pattern established in TestGeoApi.setUp (ContentType.objects.clear_cache()) should be applied here too.

💡 Proposed fix
+from django.contrib.contenttypes.models import ContentType
 ...
 class TestConnectionApi(...):
     def setUp(self):
         super().setUp()
         self._login()
+        ContentType.objects.clear_cache()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openwisp_controller/connection/tests/test_api.py` around lines 430 - 432, In
the setUp method add a call to ContentType.objects.clear_cache() before
self._login() to avoid stale ContentType lookups affecting query-count
assertions; update the setUp function (the setUp method in the test class in
openwisp_controller/connection/tests/test_api.py) to call
ContentType.objects.clear_cache() first, then proceed with self._login(),
mirroring the pattern used in TestGeoApi to ensure deterministic
assertNumQueries checks for DeviceConnection content type.
♻️ Duplicate comments (2)
openwisp_controller/connection/api/views.py (1)

140-145: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

ValidationError and NotFound are not imported — NameError at runtime.

assert_parent_exists() catches ValidationError (not imported) and raises NotFound (not imported). When a non-existent or invalid-UUID device_id is passed, the except clause evaluation raises a NameError instead of a clean 404, resulting in a 500 response.

This was flagged in a previous review and remains unresolved.

🔧 Proposed fix
+from django.core.exceptions import ValidationError
 from django.utils.translation import gettext_lazy as _
+from rest_framework.exceptions import NotFound
 from rest_framework.generics import (
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openwisp_controller/connection/api/views.py` around lines 140 - 145, The
assert_parent_exists method catches ValidationError and raises NotFound but
neither exception is imported, causing a NameError at runtime; import
ValidationError (from django.core.exceptions) and NotFound (from
rest_framework.exceptions) and update the module imports so assert_parent_exists
can reference ValidationError and NotFound without errors — locate the
assert_parent_exists function and add the two imports at the top of the file to
restore correct 404 behavior.
openwisp_controller/config/api/views.py (1)

308-349: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Scope reversion queries to supported, authorized controller objects.

These endpoints currently filter Version rows only by ContentType(model=...). That leaves two holes: the slug is not uniquely tied to one app, and the queryset is never intersected with the caller’s accessible devices/templates/vpns/device groups. A user who passes the generic permission gate can therefore enumerate, inspect, or restore revisions for every object of that content type. Resolve model through an explicit controller-model allowlist and then restrict Version.object_id with the same scoped queryset the corresponding resource API uses.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openwisp_controller/config/api/views.py` around lines 308 - 349,
BaseReversionView.get_queryset currently filters Versions only by
ContentType(model=...) which allows model name collision and unauthorized
access; change it to first validate the requested model against an explicit
controller-model allowlist (resolve the model string to a safe Model class) and
then build the same scoped queryset used by that resource's API (e.g., the
devices/templates/vpns/device groups list view logic) to get only objects the
caller is authorized to see; finally filter Version by content_type and by
object_id__in=scoped_qs.values_list('pk', flat=True) so ReversionListView,
ReversionDetailView and ReversionRestoreView operate only on allowed, scoped
objects and retain ProtectedAPIMixin checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@openwisp_controller/config/api/views.py`:
- Around line 336-345: The POST handler currently uses get_object_or_404 on qs
with revision_id=kwargs["pk"] which fails for grouped-change revisions and
returns versions from the old revision; change post to load the Revision model
instance directly by id (use kwargs["pk"]) instead of calling .get() on the
Version queryset, then confirm the loaded Revision contains at least one Version
that belongs to the allowed queryset from get_queryset() (to validate
permissions), call revision.revert() inside the with reversion.create_revision()
transaction, set user/comment as before, capture the new restore Revision object
created by reversion (the new revision created by revert) and use that new
Revision to filter versions (replace versions = qs.filter(revision=revision)
with versions = qs.filter(revision=new_revision)) so grouped-change revisions
and returned data are correct.

In `@openwisp_controller/connection/api/views.py`:
- Around line 156-162: The get_queryset override in
DeviceConnenctionListCreateView is redundant because
BaseDeviceConnection.get_queryset already applies
.filter(device_id=self.kwargs["device_id"]) and .order_by("-created"); remove
the DeviceConnenctionListCreateView.get_queryset method entirely (or replace its
body with a simple return super().get_queryset() if you prefer) so the class
relies on BaseDeviceConnection.get_queryset to avoid duplicate
filtering/ordering.

In `@openwisp_controller/mixins.py`:
- Around line 41-42: AutoRevisionMixin currently subclasses RevisionMixin
causing redundant create_revision contexts and unnecessary wrapping on all
requests; change AutoRevisionMixin to stop inheriting RevisionMixin and
implement its own dispatch logic that only opens a single create_revision
context when needed (e.g., for write methods and registered models), call
set_user/set_comment once inside that context, and ensure revision_atomic
remains available on the mixin (keep the revision_atomic attribute and any
helper methods used by dispatch); remove or avoid calling super().dispatch()
from RevisionMixin so you don't get the second nested create_revision.
- Around line 44-60: The dispatch() revision logic currently checks
request.user.is_authenticated on the raw Django HttpRequest (so
token-authenticated users are skipped); remove the upfront
request.user.is_authenticated check and instead: keep the method check
(POST/PUT/PATCH), model and reversion.is_registered(model) guards, wrap
super().dispatch(request, *args, **kwargs) inside the
reversion.create_revision(...) block, then after calling super() use
self.request.user (the DRF-authenticated user) when calling
reversion.set_user(...) and reversion.set_comment(...); reference the dispatch
method in this mixin (and RevisionMixin.dispatch if present) and ensure you only
call reversion.set_user when getattr(self, "request", None) yields a user that
.is_authenticated.

---

Outside diff comments:
In `@openwisp_controller/connection/tests/test_api.py`:
- Around line 430-432: In the setUp method add a call to
ContentType.objects.clear_cache() before self._login() to avoid stale
ContentType lookups affecting query-count assertions; update the setUp function
(the setUp method in the test class in
openwisp_controller/connection/tests/test_api.py) to call
ContentType.objects.clear_cache() first, then proceed with self._login(),
mirroring the pattern used in TestGeoApi to ensure deterministic
assertNumQueries checks for DeviceConnection content type.

In `@openwisp_controller/pki/tests/test_api.py`:
- Around line 26-28: The TestPkiApi setUp currently calls super().setUp() and
self._login() but misses clearing ContentType cache, which can cause flaky
query-count assertions; update TestPkiApi.setUp to call
ContentType.objects.clear_cache() (via the django ContentType manager) before
executing any login or test setup logic so reversion's cached ContentType
lookups won't affect AssertNumQueriesSubTestMixin assertions.

---

Duplicate comments:
In `@openwisp_controller/config/api/views.py`:
- Around line 308-349: BaseReversionView.get_queryset currently filters Versions
only by ContentType(model=...) which allows model name collision and
unauthorized access; change it to first validate the requested model against an
explicit controller-model allowlist (resolve the model string to a safe Model
class) and then build the same scoped queryset used by that resource's API
(e.g., the devices/templates/vpns/device groups list view logic) to get only
objects the caller is authorized to see; finally filter Version by content_type
and by object_id__in=scoped_qs.values_list('pk', flat=True) so
ReversionListView, ReversionDetailView and ReversionRestoreView operate only on
allowed, scoped objects and retain ProtectedAPIMixin checks.

In `@openwisp_controller/connection/api/views.py`:
- Around line 140-145: The assert_parent_exists method catches ValidationError
and raises NotFound but neither exception is imported, causing a NameError at
runtime; import ValidationError (from django.core.exceptions) and NotFound (from
rest_framework.exceptions) and update the module imports so assert_parent_exists
can reference ValidationError and NotFound without errors — locate the
assert_parent_exists function and add the two imports at the top of the file to
restore correct 404 behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1e07db45-2719-42e4-9c29-5cda009b8253

📥 Commits

Reviewing files that changed from the base of the PR and between 8edd817 and 6d602b5.

📒 Files selected for processing (13)
  • openwisp_controller/config/api/filters.py
  • openwisp_controller/config/api/serializers.py
  • openwisp_controller/config/api/urls.py
  • openwisp_controller/config/api/views.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/connection/api/views.py
  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/geo/api/views.py
  • openwisp_controller/geo/tests/test_api.py
  • openwisp_controller/mixins.py
  • openwisp_controller/pki/api/views.py
  • openwisp_controller/pki/tests/test_api.py
  • tests/openwisp2/sample_config/api/views.py
📜 Review details
⏰ 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). (12)
  • GitHub Check: Python==3.12 | django~=4.2.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.12 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Kilo Code Review
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp}

📄 CodeRabbit inference engine (Custom checks)

**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp}: Flag potential security vulnerabilities in code
Avoid unnecessary comments or docstrings for code that is already clear
Code formatting is compact and readable. Do not add excessive blank lines, especially inside function or method bodies
Flag unused or redundant code
Ensure variables, functions, classes, and files have descriptive and consistent names
New code must handle errors properly: log errors that cannot be resolved by the user with error level, log unusual conditions with warning level, log important background actions with info level, and provide user-facing messages for errors that the user can solve autonomously

Files:

  • openwisp_controller/pki/api/views.py
  • openwisp_controller/config/api/serializers.py
  • tests/openwisp2/sample_config/api/views.py
  • openwisp_controller/config/api/filters.py
  • openwisp_controller/mixins.py
  • openwisp_controller/geo/api/views.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/api/urls.py
  • openwisp_controller/config/api/views.py
  • openwisp_controller/connection/api/views.py
  • openwisp_controller/geo/tests/test_api.py
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp,sql}

📄 CodeRabbit inference engine (Custom checks)

Flag obvious performance regressions, such as heavy loops, repeated I/O, or unoptimized queries

Files:

  • openwisp_controller/pki/api/views.py
  • openwisp_controller/config/api/serializers.py
  • tests/openwisp2/sample_config/api/views.py
  • openwisp_controller/config/api/filters.py
  • openwisp_controller/mixins.py
  • openwisp_controller/geo/api/views.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/api/urls.py
  • openwisp_controller/config/api/views.py
  • openwisp_controller/connection/api/views.py
  • openwisp_controller/geo/tests/test_api.py
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp,sh,bash,sql}

📄 CodeRabbit inference engine (Custom checks)

Cryptic or non-obvious code (regex, complex bash commands, or hard-to-read code) must include a concise comment explaining why it is needed and why the complexity is acceptable

Files:

  • openwisp_controller/pki/api/views.py
  • openwisp_controller/config/api/serializers.py
  • tests/openwisp2/sample_config/api/views.py
  • openwisp_controller/config/api/filters.py
  • openwisp_controller/mixins.py
  • openwisp_controller/geo/api/views.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/api/urls.py
  • openwisp_controller/config/api/views.py
  • openwisp_controller/connection/api/views.py
  • openwisp_controller/geo/tests/test_api.py
**/*.{py,html}

📄 CodeRabbit inference engine (Custom checks)

For Django pull requests, ensure all user-facing strings are marked as translatable using the Django i18n framework

Files:

  • openwisp_controller/pki/api/views.py
  • openwisp_controller/config/api/serializers.py
  • tests/openwisp2/sample_config/api/views.py
  • openwisp_controller/config/api/filters.py
  • openwisp_controller/mixins.py
  • openwisp_controller/geo/api/views.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/api/urls.py
  • openwisp_controller/config/api/views.py
  • openwisp_controller/connection/api/views.py
  • openwisp_controller/geo/tests/test_api.py
🧠 Learnings (3)
📚 Learning: 2026-01-15T15:05:49.557Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/management/commands/clear_last_ip.py:38-42
Timestamp: 2026-01-15T15:05:49.557Z
Learning: In Django projects, when using select_related() to traverse relations (for example, select_related("organization__config_settings")), the traversed relation must not be deferred. If you also use .only() in the same query, include the relation name or FK field (e.g., "organization" or "organization_id") in the .only() list to avoid the error "Field X cannot be both deferred and traversed using select_related at the same time." Apply this guideline to Django code in openwisp_controller/config/management/commands/clear_last_ip.py and similar modules by ensuring any select_related with an accompanying only() includes the related field names to prevent deferred/traversed conflicts.

Applied to files:

  • openwisp_controller/pki/api/views.py
  • openwisp_controller/config/api/serializers.py
  • openwisp_controller/config/api/filters.py
  • openwisp_controller/mixins.py
  • openwisp_controller/geo/api/views.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/api/urls.py
  • openwisp_controller/config/api/views.py
  • openwisp_controller/connection/api/views.py
  • openwisp_controller/geo/tests/test_api.py
📚 Learning: 2026-02-17T19:13:10.088Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/whois/commands.py:0-0
Timestamp: 2026-02-17T19:13:10.088Z
Learning: In reviews for the openwisp/openwisp-controller repository, do not propose changes based on Ruff warnings. The project does not use Ruff as its linter; ignore Ruff-related suggestions and follow the repository’s established linting and configuration rules. This guidance applies to all Python files under the openwisp_controller directory.

Applied to files:

  • openwisp_controller/pki/api/views.py
  • openwisp_controller/config/api/serializers.py
  • openwisp_controller/config/api/filters.py
  • openwisp_controller/mixins.py
  • openwisp_controller/geo/api/views.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/api/urls.py
  • openwisp_controller/config/api/views.py
  • openwisp_controller/connection/api/views.py
  • openwisp_controller/geo/tests/test_api.py
📚 Learning: 2026-01-15T15:07:17.354Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/geo/estimated_location/tests/tests.py:172-175
Timestamp: 2026-01-15T15:07:17.354Z
Learning: In this repository, flake8 enforces E501 (line too long) via setup.cfg (max-line-length = 88) while ruff ignores E501 via ruff.toml. Therefore, use '# noqa: E501' on lines that intentionally exceed 88 characters to satisfy flake8 without affecting ruff checks. This applies to Python files across the project (any .py) and is relevant for tests as well. Use sparingly and only where breaking lines is not feasible without hurting readability or functionality.

Applied to files:

  • openwisp_controller/pki/api/views.py
  • openwisp_controller/config/api/serializers.py
  • tests/openwisp2/sample_config/api/views.py
  • openwisp_controller/config/api/filters.py
  • openwisp_controller/mixins.py
  • openwisp_controller/geo/api/views.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/api/urls.py
  • openwisp_controller/config/api/views.py
  • openwisp_controller/connection/api/views.py
  • openwisp_controller/geo/tests/test_api.py
🔇 Additional comments (5)
openwisp_controller/config/api/filters.py (1)

148-189: ReversionListFilter looks correct.

Filter definitions, label assignment, and Meta configuration are consistent with the rest of the file. No issues.

openwisp_controller/geo/api/views.py (1)

113-358: AutoRevisionMixin correctly placed in all affected geo API views.

Addition of AutoRevisionMixin is consistent across DeviceCoordinatesView, DeviceLocationView, FloorPlanListCreateView, FloorPlanDetailView, LocationListCreateView, and LocationDetailView. MRO is sensible in all cases.

openwisp_controller/geo/tests/test_api.py (1)

404-407: ContentType.objects.clear_cache() in setUp is the correct fix for reversion-related query count flakiness.

openwisp_controller/pki/api/views.py (1)

33-82: AutoRevisionMixin applied correctly across all PKI write views; CrlDownloadView (read-only) correctly excluded.

openwisp_controller/mixins.py (1)

57-57: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

f-string uses double quotes — project convention requires single quotes.

Line 57: f"API request: {request.method} {request.get_full_path()}" should use single quotes. As per coding guidelines and past review feedback, single-quote strings are used consistently throughout this repository.

💡 Proposed fix
-                reversion.set_comment(
-                    f"API request: {request.method} {request.get_full_path()}"
-                )
+                reversion.set_comment(
+                    f'API request: {request.method} {request.get_full_path()}'
+                )
			> Likely an incorrect or invalid review comment.

Comment thread openwisp_controller/config/api/views.py Outdated
Comment thread openwisp_controller/connection/api/views.py Outdated
Comment thread openwisp_controller/mixins.py Outdated
Comment thread openwisp_controller/mixins.py
@dee077
Copy link
Copy Markdown
Member Author

dee077 commented May 4, 2026

Question

  • Should we use all these filters or reduce them
  • Is the increase in no of queries too much? Should I spend some more time finding out if there is a way to reduce them?

Pending

  • Add more test scenarios based on the filter
  • Update no of queries to fix failing tests

@openwisp-companion
Copy link
Copy Markdown

Multiple Flake8 Failures and Test Failures

Hello @dee077,
(Analysis for commit 6d602b5)

There are multiple issues that need to be addressed:

  1. Code Style Failures:
  • F401 'reversion.models.Revision' imported but unused in openwisp_controller/config/api/views.py.
  • F821 undefined name 'ValidationError' in openwisp_controller/config/api/views.py.
  • F821 undefined name 'NotFound' in openwisp_controller/config/api/views.py.

Please fix these unused imports and undefined names manually.

  1. Test Failures:
  • The test test_post_cert_revoke_api in openwisp_controller/pki/tests/test_api.py failed because it expected 8 SQL queries but only 7 were executed. This indicates a potential issue with how the test is set up or how the code is interacting with the database.

Please address these issues and push a new commit.

Copy link
Copy Markdown
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.

@dee077 why are the queries almost doubling?

Copy link
Copy Markdown
Contributor

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@openwisp_controller/config/api/views.py`:
- Around line 308-312: The BaseReversionView currently uses
ProtectedAPIMixin/FilterByOrganizationManaged which filters by organization__in
but reversion.models.Version has no organization field; override get_queryset on
BaseReversionView (and any ReversionList/ReversionDetail/ReversionRestore views
that inherit it) to perform access control by resolving the ContentType and then
restricting Versions to only those whose (object_id, content_type) refer to
model instances the current user may access (for example by querying the target
model for pk__in=version_ids & applying the existing organization/user
permission filter used for that model), or alternatively implement a custom
organization_field traversal that maps Version -> content_object.organization
before filtering; ensure you use the Version model and
self.kwargs["model"]/content_type to build the filtered queryset rather than
delegating to FilterByOrganizationManaged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 392716f5-17c4-460c-aeb5-50a84085626c

📥 Commits

Reviewing files that changed from the base of the PR and between 6d602b5 and 64c4300.

📒 Files selected for processing (6)
  • openwisp_controller/config/api/views.py
  • openwisp_controller/connection/api/views.py
  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/geo/tests/test_api.py
  • openwisp_controller/mixins.py
  • openwisp_controller/pki/tests/test_api.py
📜 Review details
⏰ 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). (11)
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.13 | django~=5.1.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
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=4.2.0
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp}

📄 CodeRabbit inference engine (Custom checks)

**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp}: Flag potential security vulnerabilities in code
Avoid unnecessary comments or docstrings for code that is already clear
Code formatting is compact and readable. Do not add excessive blank lines, especially inside function or method bodies
Flag unused or redundant code
Ensure variables, functions, classes, and files have descriptive and consistent names
New code must handle errors properly: log errors that cannot be resolved by the user with error level, log unusual conditions with warning level, log important background actions with info level, and provide user-facing messages for errors that the user can solve autonomously

Files:

  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/mixins.py
  • openwisp_controller/connection/api/views.py
  • openwisp_controller/geo/tests/test_api.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/api/views.py
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp,sql}

📄 CodeRabbit inference engine (Custom checks)

Flag obvious performance regressions, such as heavy loops, repeated I/O, or unoptimized queries

Files:

  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/mixins.py
  • openwisp_controller/connection/api/views.py
  • openwisp_controller/geo/tests/test_api.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/api/views.py
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp,sh,bash,sql}

📄 CodeRabbit inference engine (Custom checks)

Cryptic or non-obvious code (regex, complex bash commands, or hard-to-read code) must include a concise comment explaining why it is needed and why the complexity is acceptable

Files:

  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/mixins.py
  • openwisp_controller/connection/api/views.py
  • openwisp_controller/geo/tests/test_api.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/api/views.py
**/*.{py,html}

📄 CodeRabbit inference engine (Custom checks)

For Django pull requests, ensure all user-facing strings are marked as translatable using the Django i18n framework

Files:

  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/mixins.py
  • openwisp_controller/connection/api/views.py
  • openwisp_controller/geo/tests/test_api.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/api/views.py
🧠 Learnings (3)
📚 Learning: 2026-01-15T15:05:49.557Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/management/commands/clear_last_ip.py:38-42
Timestamp: 2026-01-15T15:05:49.557Z
Learning: In Django projects, when using select_related() to traverse relations (for example, select_related("organization__config_settings")), the traversed relation must not be deferred. If you also use .only() in the same query, include the relation name or FK field (e.g., "organization" or "organization_id") in the .only() list to avoid the error "Field X cannot be both deferred and traversed using select_related at the same time." Apply this guideline to Django code in openwisp_controller/config/management/commands/clear_last_ip.py and similar modules by ensuring any select_related with an accompanying only() includes the related field names to prevent deferred/traversed conflicts.

Applied to files:

  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/mixins.py
  • openwisp_controller/connection/api/views.py
  • openwisp_controller/geo/tests/test_api.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/api/views.py
📚 Learning: 2026-02-17T19:13:10.088Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/whois/commands.py:0-0
Timestamp: 2026-02-17T19:13:10.088Z
Learning: In reviews for the openwisp/openwisp-controller repository, do not propose changes based on Ruff warnings. The project does not use Ruff as its linter; ignore Ruff-related suggestions and follow the repository’s established linting and configuration rules. This guidance applies to all Python files under the openwisp_controller directory.

Applied to files:

  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/mixins.py
  • openwisp_controller/connection/api/views.py
  • openwisp_controller/geo/tests/test_api.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/api/views.py
📚 Learning: 2026-01-15T15:07:17.354Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/geo/estimated_location/tests/tests.py:172-175
Timestamp: 2026-01-15T15:07:17.354Z
Learning: In this repository, flake8 enforces E501 (line too long) via setup.cfg (max-line-length = 88) while ruff ignores E501 via ruff.toml. Therefore, use '# noqa: E501' on lines that intentionally exceed 88 characters to satisfy flake8 without affecting ruff checks. This applies to Python files across the project (any .py) and is relevant for tests as well. Use sparingly and only where breaking lines is not feasible without hurting readability or functionality.

Applied to files:

  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/mixins.py
  • openwisp_controller/connection/api/views.py
  • openwisp_controller/geo/tests/test_api.py
  • openwisp_controller/pki/tests/test_api.py
  • openwisp_controller/config/api/views.py
🔇 Additional comments (6)
openwisp_controller/config/api/views.py (1)

336-349: Two unresolved issues from the previous review remain.

1. MultipleObjectsReturned on line 338:
get_object_or_404(qs, revision_id=kwargs["pk"]) calls .get() under the hood. If a single revision contains multiple Version rows for the same content_type (e.g. a bulk operation, or follow=["device"] tracking creating versions for several objects of the same type), this raises MultipleObjectsReturned → HTTP 500.

2. Response returns stale revision on line 345:
qs.filter(revision=revision) filters on the original revision object (before revert). After revision.revert() runs inside create_revision(), a brand-new restore revision is committed. The response therefore returns metadata pointing at the old revision_id, not the newly-created restore event, which is misleading for audit purposes.

openwisp_controller/connection/tests/test_api.py (1)

564-564: DeviceConnection write operations gained ~10 queries vs the ~3 seen elsewhere.

The elevated overhead is attributable to reversion.register(model=DeviceConnection, follow=["device"]) — reversion also versions the related Device on each DeviceConnection save, doubling the ContentType lookups and Version/Revision inserts. This was noted in the prior review; raising it here for tracking as it is still unresolved.

Also applies to: 609-609, 623-623

openwisp_controller/mixins.py (1)

40-59: AutoRevisionMixin implementation looks correct.

Both previously-raised concerns are resolved:

  • The RevisionMixin inheritance that caused double-wrapping and unnecessary context overhead is removed.
  • self.request.user (the DRF-authenticated user) is used after super().dispatch() returns, so Bearer/token-authenticated users are now correctly captured.
openwisp_controller/connection/api/views.py (1)

66-66: AutoRevisionMixin wiring looks correct.

The previously-raised issues are resolved: the missing ValidationError/NotFound imports are gone, and the redundant get_queryset override in DeviceConnenctionListCreateView was removed. The mixin silently no-ops for Credentials and Command (not registered with reversion), which is the intended behavior.

Also applies to: 102-113, 141-149

openwisp_controller/geo/tests/test_api.py (1)

404-408: ContentType.objects.clear_cache() in setUp is a correct and necessary fix.

Calling this before each test ensures the in-memory ContentType cache doesn't carry state from previous tests that would make assertNumQueries counts non-deterministic when reversion performs ContentType lookups.

openwisp_controller/pki/tests/test_api.py (1)

54-303: Query count adjustments are consistent and expected.

Every write operation (POST/PUT/PATCH) on registered Ca/Cert models gains exactly +3 queries (ContentType lookup, reversion_version, reversion_revision inserts). GET and DELETE operations are unchanged, confirming AutoRevisionMixin only triggers on the correct HTTP methods.

Comment thread openwisp_controller/config/api/views.py Outdated
@dee077
Copy link
Copy Markdown
Member Author

dee077 commented May 5, 2026

@dee077 why are the queries almost doubling?

It’s not due to nested revision contexts
Let's say if the DeviceLocation model is registered to reversion like this

reversion.register(
    model=DeviceLocation,
    follow=["device", "location"],
)

So when DeviceLocation is updated, reversion also versions for DeviceLocation, Device, and Location.

That triggers additional ContentType lookups, related object fetches, PK collection
For each of these models above, it creates the pair inserts reversion_revision + reversion_version in models.

When DeviceLocation is updated, reversion also versions DeviceLocation, Device, and Location in the same revision.

This triggers additional ContentType lookups, related object fetches, PK collection, serialization of each object, and finally one reversion_revision insert plus multiple reversion_version entries for all tracked objects in that revision, which is why the query count increases significantly.

@openwisp-companion
Copy link
Copy Markdown

The CI is failing due to transient infrastructure issues (not related to your code). I have restarted the failed jobs automatically (1/3).

Copy link
Copy Markdown
Contributor

@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: 6

♻️ Duplicate comments (1)
openwisp_controller/config/tests/test_api.py (1)

1806-1811: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Strengthen the revision_id filter assertion.

The assertion only checks the count (== 1); it does not verify that the returned version actually belongs to revision_id. If ReversionListFilter ever silently no-ops (as flagged in earlier reviews), this assertion would still pass with the wrong row. Add a per-result check on revision_id.

♻️ Suggested tightening
             self.assertEqual(response.status_code, 200)
             self.assertEqual(len(response_json["results"]), 1)
+            for version in response_json["results"]:
+                self.assertEqual(version["revision_id"], revision_id)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openwisp_controller/config/tests/test_api.py` around lines 1806 - 1811, The
test currently only asserts the count of results for the ReversionList filter;
update the subTest (the block using reverse("config_api:reversion_list",
args=[model_slug]) and response_json) to also verify that each returned item
actually belongs to the requested revision_id by iterating over
response_json["results"] and asserting each item's "revision_id" equals the
local revision_id variable (or otherwise contains the expected value), so the
test fails if the filter silently no-ops.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@openwisp_controller/config/api/views.py`:
- Around line 344-366: The post() handler is vulnerable because calling
revision.revert() will restore all Versions in that Revision (including
out-of-scope org objects); change the logic in post to either reject Revisions
that contain any Version not present in the current queryset (qs) or iterate and
revert only the visible versions: obtain the set/list of Versions for the
Revision (Revision -> related Version objects), ensure every Version is in qs
(e.g., compare IDs) before calling any revert, and if any are out-of-scope raise
Http404/PermissionDenied; alternatively replace revision.revert() with a loop
over qs.filter(revision=revision) and call revert on each Version instance so
only in-scope objects are modified, keeping the transaction/reversion context,
set_user and set_comment behavior unchanged.
- Around line 350-361: Remove the outer transaction.atomic() wrapper around
reversion.create_revision() (create_revision already opens an atomic block).
After the create_revision context (after calling revision.revert(),
reversion.set_user(request.user), reversion.set_comment(comment)), do not rely
on get_object_or_404(Revision, user=request.user, comment=comment) — instead
deterministically fetch the revision created in the context by querying the
Revision model for the latest revision for the user, e.g. use
Revision.objects.filter(user=request.user).latest("date_created") (or latest by
PK/timestamp) and raise Http404 if none; keep the calls to revision.revert(),
reversion.set_user and reversion.set_comment unchanged.
- Around line 308-320: BaseReversionView.get_queryset currently looks up
ContentType by model only and assumes every model has an organization FK; fix it
by narrowing ContentType lookup to allowed app labels or accepting an app_label
kwarg (e.g., use app_label__in=["config","connection","geo","pki"] or use
self.kwargs.get("app_label") when present) to avoid MultipleObjectsReturned, and
harden the non-superuser branch by only handling model classes that expose an
organization relation (either check for hasattr(model_class, "organization") or
maintain a whitelist of allowed model names/classes before calling
model_class.objects.filter(organization__in=...); if the model is not allowed,
return an empty queryset or raise Http404/PermissionDenied.
- Around line 323-342: Add the authentication/permission mixin to the reversion
endpoints: update ReversionListView, ReversionDetailView, and
ReversionRestoreView to include ProtectedAPIMixin (or explicitly set appropriate
permission_classes/authentication_classes) so requests are authenticated before
BaseReversionView.get_queryset() uses self.request.user; ensure the change is
applied to the class bases for those three views (ReversionListView,
ReversionDetailView, ReversionRestoreView) so anonymous users cannot reach them.

In `@openwisp_controller/config/tests/test_api.py`:
- Around line 1820-1827: The restore sub-test is incorrectly reusing
response_json from the earlier filtered-list test; update the "Test reversion
restore view" subTest to obtain the correct revision_id explicitly (e.g., call
the unfiltered list endpoint to populate response_json or reuse the earlier
captured revision_id variable created near the first list test) before building
path via reverse("config_api:reversion_restore", args=[model_slug,
revision_id]); then post to that path and add an assertion on the restore
response body (not just status_code) and/or verify the returned JSON contains
expected keys so the test no longer depends on the filtered-list result
containing exactly one row.
- Around line 1782-1827: Add a sub-test to cover the non-superuser org-scoping
path: after the existing superuser checks, call self._login(... ) to
authenticate an org-administrator (not superuser), create two Device instances
in two different organizations, then call the config_api endpoints
(reversion_list, reversion_detail, reversion_restore) and assert that
BaseReversionView.get_queryset() behavior is enforced — only revisions/versions
belonging to the admin's managed organization are returned and attempts to
restore a revision for a device in the other org return 404; also fix the typo
"sperate" → "separate" in the test name/comment. Ensure you reference
model_slug="device", Device, and the view names
reversion_list/reversion_detail/reversion_restore when adding assertions.

---

Duplicate comments:
In `@openwisp_controller/config/tests/test_api.py`:
- Around line 1806-1811: The test currently only asserts the count of results
for the ReversionList filter; update the subTest (the block using
reverse("config_api:reversion_list", args=[model_slug]) and response_json) to
also verify that each returned item actually belongs to the requested
revision_id by iterating over response_json["results"] and asserting each item's
"revision_id" equals the local revision_id variable (or otherwise contains the
expected value), so the test fails if the filter silently no-ops.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ea9fb2bb-c065-4181-b0e6-b34c666afda7

📥 Commits

Reviewing files that changed from the base of the PR and between 64c4300 and 48e1263.

📒 Files selected for processing (2)
  • openwisp_controller/config/api/views.py
  • openwisp_controller/config/tests/test_api.py
📜 Review details
⏰ 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). (12)
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Kilo Code Review
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp}

📄 CodeRabbit inference engine (Custom checks)

**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp}: Flag potential security vulnerabilities in code
Avoid unnecessary comments or docstrings for code that is already clear
Code formatting is compact and readable. Do not add excessive blank lines, especially inside function or method bodies
Flag unused or redundant code
Ensure variables, functions, classes, and files have descriptive and consistent names
New code must handle errors properly: log errors that cannot be resolved by the user with error level, log unusual conditions with warning level, log important background actions with info level, and provide user-facing messages for errors that the user can solve autonomously

Files:

  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/api/views.py
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp,sql}

📄 CodeRabbit inference engine (Custom checks)

Flag obvious performance regressions, such as heavy loops, repeated I/O, or unoptimized queries

Files:

  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/api/views.py
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp,sh,bash,sql}

📄 CodeRabbit inference engine (Custom checks)

Cryptic or non-obvious code (regex, complex bash commands, or hard-to-read code) must include a concise comment explaining why it is needed and why the complexity is acceptable

Files:

  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/api/views.py
**/*.{py,html}

📄 CodeRabbit inference engine (Custom checks)

For Django pull requests, ensure all user-facing strings are marked as translatable using the Django i18n framework

Files:

  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/api/views.py
🧠 Learnings (3)
📚 Learning: 2026-01-15T15:05:49.557Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/management/commands/clear_last_ip.py:38-42
Timestamp: 2026-01-15T15:05:49.557Z
Learning: In Django projects, when using select_related() to traverse relations (for example, select_related("organization__config_settings")), the traversed relation must not be deferred. If you also use .only() in the same query, include the relation name or FK field (e.g., "organization" or "organization_id") in the .only() list to avoid the error "Field X cannot be both deferred and traversed using select_related at the same time." Apply this guideline to Django code in openwisp_controller/config/management/commands/clear_last_ip.py and similar modules by ensuring any select_related with an accompanying only() includes the related field names to prevent deferred/traversed conflicts.

Applied to files:

  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/api/views.py
📚 Learning: 2026-02-17T19:13:10.088Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/whois/commands.py:0-0
Timestamp: 2026-02-17T19:13:10.088Z
Learning: In reviews for the openwisp/openwisp-controller repository, do not propose changes based on Ruff warnings. The project does not use Ruff as its linter; ignore Ruff-related suggestions and follow the repository’s established linting and configuration rules. This guidance applies to all Python files under the openwisp_controller directory.

Applied to files:

  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/api/views.py
📚 Learning: 2026-01-15T15:07:17.354Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/geo/estimated_location/tests/tests.py:172-175
Timestamp: 2026-01-15T15:07:17.354Z
Learning: In this repository, flake8 enforces E501 (line too long) via setup.cfg (max-line-length = 88) while ruff ignores E501 via ruff.toml. Therefore, use '# noqa: E501' on lines that intentionally exceed 88 characters to satisfy flake8 without affecting ruff checks. This applies to Python files across the project (any .py) and is relevant for tests as well. Use sparingly and only where breaking lines is not feasible without hurting readability or functionality.

Applied to files:

  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/api/views.py

Comment on lines +308 to +320
class BaseReversionView:
def get_queryset(self):
model = self.kwargs.get("model").lower()
content_type = get_object_or_404(ContentType, model=model)
qs = super().get_queryset().filter(content_type=content_type)
user = self.request.user
if user.is_superuser:
return qs
model_class = content_type.model_class()
allowed_object_ids = model_class.objects.filter(
organization__in=user.organizations_managed
).values_list("pk", flat=True)
return qs.filter(object_id__in=allowed_object_ids)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

BaseReversionView.get_queryset() has two correctness/security risks.

  1. get_object_or_404(ContentType, model=model) filters only by model name without app_label. ContentType is unique on (app_label, model) so two apps registering the same model name will trigger MultipleObjectsReturned (HTTP 500). Constrain the lookup to the apps you actually intend to expose (e.g., app_label__in=["config", "connection", "geo", "pki"]) or accept an app_label in the URL.

  2. The non-superuser branch assumes every reversion-registered model_class has an organization FK. Models without that field will raise FieldError on access. Either restrict model to a known whitelist that all expose organization, or branch on the field's existence. Without a whitelist this is also a soft authorization bypass risk if a model with a different ownership concept ever gets routed through here.

🛡️ Suggested hardening
-class BaseReversionView:
-    def get_queryset(self):
-        model = self.kwargs.get("model").lower()
-        content_type = get_object_or_404(ContentType, model=model)
-        qs = super().get_queryset().filter(content_type=content_type)
-        user = self.request.user
-        if user.is_superuser:
-            return qs
-        model_class = content_type.model_class()
-        allowed_object_ids = model_class.objects.filter(
-            organization__in=user.organizations_managed
-        ).values_list("pk", flat=True)
-        return qs.filter(object_id__in=allowed_object_ids)
+    ALLOWED_APP_LABELS = ("config", "connection", "geo", "pki")
+
+    def get_queryset(self):
+        model = self.kwargs.get("model", "").lower()
+        content_type = get_object_or_404(
+            ContentType, app_label__in=self.ALLOWED_APP_LABELS, model=model
+        )
+        qs = super().get_queryset().filter(content_type=content_type)
+        user = self.request.user
+        if user.is_superuser:
+            return qs
+        model_class = content_type.model_class()
+        if model_class is None or not hasattr(model_class, "organization"):
+            return qs.none()
+        allowed_object_ids = model_class.objects.filter(
+            organization__in=user.organizations_managed
+        ).values_list("pk", flat=True)
+        return qs.filter(object_id__in=allowed_object_ids)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openwisp_controller/config/api/views.py` around lines 308 - 320,
BaseReversionView.get_queryset currently looks up ContentType by model only and
assumes every model has an organization FK; fix it by narrowing ContentType
lookup to allowed app labels or accepting an app_label kwarg (e.g., use
app_label__in=["config","connection","geo","pki"] or use
self.kwargs.get("app_label") when present) to avoid MultipleObjectsReturned, and
harden the non-superuser branch by only handling model classes that expose an
organization relation (either check for hasattr(model_class, "organization") or
maintain a whitelist of allowed model names/classes before calling
model_class.objects.filter(organization__in=...); if the model is not allowed,
return an empty queryset or raise Http404/PermissionDenied.

Comment on lines +323 to +342
class ReversionListView(BaseReversionView, ListAPIView):
serializer_class = ReversionSerializer
queryset = Version.objects.select_related("revision").order_by(
"-revision__date_created"
)
filter_backends = [DjangoFilterBackend]
filterset_class = ReversionListFilter
pagination_class = ListViewPagination


class ReversionDetailView(BaseReversionView, RetrieveAPIView):
serializer_class = ReversionSerializer
queryset = Version.objects.select_related("revision")


class ReversionRestoreView(BaseReversionView, GenericAPIView):
serializer_class = serializers.Serializer
queryset = Version.objects.select_related("revision").order_by(
"-revision__date_created"
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Reversion endpoints lack authentication/permission classes.

ReversionListView, ReversionDetailView, and ReversionRestoreView do not include ProtectedAPIMixin (or any permission_classes/authentication_classes). All other views in this module inherit ProtectedAPIMixin to enforce auth + DRF model permissions; without it these endpoints fall back to DRF defaults and may be reachable by unauthenticated users — and self.request.user would be AnonymousUser, breaking user.organizations_managed in BaseReversionView.get_queryset().

🔒 Proposed fix
-class ReversionListView(BaseReversionView, ListAPIView):
+class ReversionListView(ProtectedAPIMixin, BaseReversionView, ListAPIView):
     serializer_class = ReversionSerializer
     ...

-class ReversionDetailView(BaseReversionView, RetrieveAPIView):
+class ReversionDetailView(ProtectedAPIMixin, BaseReversionView, RetrieveAPIView):
     serializer_class = ReversionSerializer
     ...

-class ReversionRestoreView(BaseReversionView, GenericAPIView):
+class ReversionRestoreView(ProtectedAPIMixin, BaseReversionView, GenericAPIView):
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openwisp_controller/config/api/views.py` around lines 323 - 342, Add the
authentication/permission mixin to the reversion endpoints: update
ReversionListView, ReversionDetailView, and ReversionRestoreView to include
ProtectedAPIMixin (or explicitly set appropriate
permission_classes/authentication_classes) so requests are authenticated before
BaseReversionView.get_queryset() uses self.request.user; ensure the change is
applied to the class bases for those three views (ReversionListView,
ReversionDetailView, ReversionRestoreView) so anonymous users cannot reach them.

Comment on lines +344 to +366
def post(self, request, *args, **kwargs):
qs = self.get_queryset()
revision = get_object_or_404(Revision, pk=kwargs["pk"])
if not qs.filter(revision=revision).exists():
raise Http404
comment = f"Restored to previous revision: {revision.id}"
with transaction.atomic():
with reversion.create_revision():
revision.revert()
reversion.set_user(request.user)
reversion.set_comment(comment)
restored_revision = get_object_or_404(
Revision.objects.order_by(
"-date_created"
),
user=request.user,
comment=comment,
)
versions = qs.filter(revision=restored_revision)
serializer = ReversionSerializer(
versions, many=True, context=self.get_serializer_context()
)
return Response(serializer.data, status=status.HTTP_200_OK)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Cross-organization privilege escalation in revision.revert().

The qs.filter(revision=revision).exists() check only confirms that at least one Version in the revision is in the current user's scope; revision.revert() will then restore every Version in that revision, including those belonging to objects in organizations the user does not manage. A non-superuser whose revision contains both an in-scope and an out-of-scope object can mutate the out-of-scope object via this endpoint.

Either reject revisions that contain any out-of-scope versions, or revert only the versions visible in qs (for v in qs.filter(revision=revision): v.revert()).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openwisp_controller/config/api/views.py` around lines 344 - 366, The post()
handler is vulnerable because calling revision.revert() will restore all
Versions in that Revision (including out-of-scope org objects); change the logic
in post to either reject Revisions that contain any Version not present in the
current queryset (qs) or iterate and revert only the visible versions: obtain
the set/list of Versions for the Revision (Revision -> related Version objects),
ensure every Version is in qs (e.g., compare IDs) before calling any revert, and
if any are out-of-scope raise Http404/PermissionDenied; alternatively replace
revision.revert() with a loop over qs.filter(revision=revision) and call revert
on each Version instance so only in-scope objects are modified, keeping the
transaction/reversion context, set_user and set_comment behavior unchanged.

Comment on lines +350 to +361
with transaction.atomic():
with reversion.create_revision():
revision.revert()
reversion.set_user(request.user)
reversion.set_comment(comment)
restored_revision = get_object_or_404(
Revision.objects.order_by(
"-date_created"
),
user=request.user,
comment=comment,
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Fragile lookup of the newly-created restore revision and redundant transaction nesting.

  • transaction.atomic() wrapping reversion.create_revision() is redundant — create_revision already opens an atomic block by default (and AutoRevisionMixin.revision_atomic = False exists specifically to opt out of nesting). Drop the outer transaction.atomic().
  • Looking up the newly created Revision via (user=request.user, comment=comment) is racy: two concurrent restores of the same source revision by the same user produce identical comments and get_object_or_404 will raise MultipleObjectsReturned. Capture the created revision deterministically instead, e.g. by querying Revision.objects.filter(user=request.user).latest("date_created") immediately after the with block, or by switching the lookup to use the latest version inserted in the create_revision context.
♻️ Proposed refactor
-        comment = f"Restored to previous revision: {revision.id}"
-        with transaction.atomic():
-            with reversion.create_revision():
-                revision.revert()
-                reversion.set_user(request.user)
-                reversion.set_comment(comment)
-        restored_revision = get_object_or_404(
-            Revision.objects.order_by(
-                "-date_created"
-            ),
-            user=request.user,
-            comment=comment,
-        )
+        comment = f"Restored to previous revision: {revision.id}"
+        with reversion.create_revision():
+            revision.revert()
+            reversion.set_user(request.user)
+            reversion.set_comment(comment)
+        restored_revision = (
+            Revision.objects.filter(user=request.user)
+            .order_by("-date_created")
+            .first()
+        )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openwisp_controller/config/api/views.py` around lines 350 - 361, Remove the
outer transaction.atomic() wrapper around reversion.create_revision()
(create_revision already opens an atomic block). After the create_revision
context (after calling revision.revert(), reversion.set_user(request.user),
reversion.set_comment(comment)), do not rely on get_object_or_404(Revision,
user=request.user, comment=comment) — instead deterministically fetch the
revision created in the context by querying the Revision model for the latest
revision for the user, e.g. use
Revision.objects.filter(user=request.user).latest("date_created") (or latest by
PK/timestamp) and raise Http404 if none; keep the calls to revision.revert(),
reversion.set_user and reversion.set_comment unchanged.

Comment on lines +1782 to +1827
# Todo: create sperate test for each endpoint
def test_reversion_list_and_restore_api(self):
org = self._get_org()
model_slug = "device"
with reversion.create_revision():
device = self._create_device(
organization=org,
name="test",
)
path = reverse("config_api:device_detail", args=[device.pk])
data = dict(name="change-test-device")
response = self.client.patch(path, data, content_type="application/json")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["name"], "change-test-device")

with self.subTest("Test reversion list"):
path = reverse("config_api:reversion_list", args=[model_slug])
response = self.client.get(path)
response_json = response.json()
version_id = response_json["results"][1]["id"]
revision_id = response_json["results"][1]["revision_id"]
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response_json["results"]), 2)

with self.subTest("Test revision list filter by revision id"):
path = reverse("config_api:reversion_list", args=[model_slug])
response = self.client.get(f"{path}?revision_id={revision_id}")
response_json = response.json()
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response_json["results"]), 1)

with self.subTest("Test reversion detail"):
path = reverse("config_api:reversion_detail", args=[model_slug, version_id])
response = self.client.get(path)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["id"], version_id)
self.assertEqual(response.json()["object_id"], str(device.pk))

with self.subTest("Test reversion restore view"):
revision_id = response_json["results"][0]["revision_id"]
path = reverse(
"config_api:reversion_restore", args=[model_slug, revision_id]
)
response = self.client.post(path)
self.assertEqual(response.status_code, 200)
self.assertEqual(Device.objects.get(name="test").pk, device.pk)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Test only covers a superuser; non-superuser org-scoping path is unverified.

self._login() authenticates an admin/superuser, which short-circuits BaseReversionView.get_queryset() at the is_superuser branch. The new non-superuser branch (the organizations_managed filter using model_class.objects.filter(organization__in=...)) — which is the security-critical path — has no coverage. Please add a sub-test that:

  • Logs in as an org-administrator (non-superuser).
  • Creates devices in two different organizations.
  • Asserts only devices/versions from the user's managed org are returned by reversion_list/reversion_detail, and that reversion_restore denies access (404) to a revision whose target object lives in another organization.

Also: the typo "sperate" → "separate" on line 1782.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openwisp_controller/config/tests/test_api.py` around lines 1782 - 1827, Add a
sub-test to cover the non-superuser org-scoping path: after the existing
superuser checks, call self._login(... ) to authenticate an org-administrator
(not superuser), create two Device instances in two different organizations,
then call the config_api endpoints (reversion_list, reversion_detail,
reversion_restore) and assert that BaseReversionView.get_queryset() behavior is
enforced — only revisions/versions belonging to the admin's managed organization
are returned and attempts to restore a revision for a device in the other org
return 404; also fix the typo "sperate" → "separate" in the test name/comment.
Ensure you reference model_slug="device", Device, and the view names
reversion_list/reversion_detail/reversion_restore when adding assertions.

Comment on lines +1820 to +1827
with self.subTest("Test reversion restore view"):
revision_id = response_json["results"][0]["revision_id"]
path = reverse(
"config_api:reversion_restore", args=[model_slug, revision_id]
)
response = self.client.post(path)
self.assertEqual(response.status_code, 200)
self.assertEqual(Device.objects.get(name="test").pk, device.pk)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Restore sub-test reuses response_json from the previous filter sub-test.

response_json here is the response of the filtered-list sub-test, not the unfiltered one — so results[0]["revision_id"] happens to equal the previously captured revision_id only by accident of the filter result containing exactly one row. Re-deriving the source-revision id explicitly (e.g., from the unfiltered list, or via revision_id already captured at L1802) makes the test robust to refactors. Also consider asserting the response body of the restore endpoint (status only is currently checked beyond the device name).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openwisp_controller/config/tests/test_api.py` around lines 1820 - 1827, The
restore sub-test is incorrectly reusing response_json from the earlier
filtered-list test; update the "Test reversion restore view" subTest to obtain
the correct revision_id explicitly (e.g., call the unfiltered list endpoint to
populate response_json or reuse the earlier captured revision_id variable
created near the first list test) before building path via
reverse("config_api:reversion_restore", args=[model_slug, revision_id]); then
post to that path and add an assertion on the restore response body (not just
status_code) and/or verify the returned JSON contains expected keys so the test
no longer depends on the filtered-list result containing exactly one row.

@openwisp-companion
Copy link
Copy Markdown

Black Formatting Failure

Hello @dee077,
(Analysis for commit 48e1263)

The CI pipeline failed due to a Black formatting error. This means that
some code in your commit does not adhere to the project's code style
guidelines.

Fix:
Run the command openwisp-qa-format in your local environment to automatically
format your code and resolve this issue. Then, commit the changes and push again.

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

Projects

Status: In progress
Status: In progress

Development

Successfully merging this pull request may close these issues.

[feature] REST API should store revisions with django-reversion

4 participants