Skip to content

[Fix] Cascade deletion blocked by UpgradeOperation #256#391

Open
UltraBot05 wants to merge 14 commits into
openwisp:masterfrom
UltraBot05:issues/256-delete-org-blocked
Open

[Fix] Cascade deletion blocked by UpgradeOperation #256#391
UltraBot05 wants to merge 14 commits into
openwisp:masterfrom
UltraBot05:issues/256-delete-org-blocked

Conversation

@UltraBot05

@UltraBot05 UltraBot05 commented Mar 6, 2026

Copy link
Copy Markdown

Implemented has_delete_permission to allow deletions when triggered by a parent object or bulk actions. This mirrors the logic from openwisp-utils #596.

Added unit tests to verify the permission logic.

Fixes #256

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

Description of Changes

Fixes the Organization deletion blocker caused by UpgradeOperation and BatchUpgradeOperation records whose admin classes blocked all cascade deletes.

This PR is the companion to openwisp-utils#596. Rather than duplicating permission logic locally, it imports and uses BlockDeleteAllowCascadeMixin directly from openwisp_utils.admin, keeping behavior fully aligned with the utils implementation.

Key changes:

  1. UpgradeOperationInline now inherits BlockDeleteAllowCascadeMixin directly (it's a StackedInline, not a ReadOnlyAdmin, so the mixin must be applied explicitly).
  2. UpgradeOperationAdmin and BatchUpgradeOperationAdmin inherit through ReadOnlyAdmin - BlockDeleteAllowCascadeMixin automatically; their old hardcoded has_delete_permission overrides have been removed.
  3. Tests: unit tests covering cascade delete via parent delete view, bulk delete via delete_selected, own delete view blocked, change view blocked, unrelated URL blocked, None resolver_match blocked. Full integration tests for superuser and non-superuser cascade deletion.

Dependency: requires openwisp-utils#596 to be merged first. The CI workflow temporarily installs the utils fork branch until that merge happens.

Note on GitHub conflict banner: GitHub's web editor shows "too complex to resolve" on admin.py - this is a web UI limitation, not a real conflict. git merge upstream/master confirms the branch is fully up to date.

Screenshot

Before these changes:
image

After These changes:
image

@coderabbitai

coderabbitai Bot commented Mar 6, 2026

Copy link
Copy Markdown

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

Added CascadeDeletePermissionMixin and applied it to UpgradeOperationInline and BatchUpgradeOperationAdmin in openwisp_firmware_upgrader/admin.py. The mixin implements has_delete_permission(self, request, obj=None) to allow deletion for superusers, when obj is None (cascade/delete confirmation), when the resolved admin URL ends with _delete, or when the changelist URL ends with _changelist and the action is delete_selected (bulk delete). All other delete attempts are denied. Tests were added to verify cascade delete via parent delete view, bulk delete via delete_selected, blocking deletion from a normal change view, superuser override, and integration behavior for deleting an Organization with related BatchUpgradeOperation.

Sequence Diagram(s)

sequenceDiagram
    participant AdminUser as Admin User
    participant Request as HTTP Request
    participant AdminView as AdminView (has_delete_permission)
    participant DB as Database

    AdminUser->>Request: submit delete action (single or bulk)
    Request->>AdminView: resolve URL & call has_delete_permission(request, obj)
    AdminView-->>Request: check
    alt superuser
        AdminView->>Request: return True
    else obj is None (cascade confirm) or URL ends with "_delete" or action == "delete_selected"
        AdminView->>Request: return True
    else
        AdminView->>Request: return False
    end
    alt permission granted
        Request->>DB: perform delete cascade
        DB-->>AdminView: deletion result
        AdminView-->>AdminUser: show success/redirect
    else permission denied
        AdminView-->>AdminUser: show permission denied
    end
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR fully addresses issue #256 by implementing has_delete_permission logic that allows cascade deletions from parent (Organization) while preventing unauthorized single-row deletions, with comprehensive unit tests validating all permission scenarios.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing cascade deletion in UpgradeOperationInline: new CascadeDeletePermissionMixin, updated inline/admin base classes, and comprehensive test coverage—no out-of-scope modifications detected.
Title check ✅ Passed The title clearly summarizes the main change: fixing cascade deletion that was blocked by UpgradeOperation records, which aligns with the PR's core objective to resolve issue #256.
Description check ✅ Passed The PR description is comprehensive and addresses all required template sections with detailed information about changes, testing, and the fixed issue.

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

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

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.

Comment thread openwisp_firmware_upgrader/admin.py Outdated
Comment thread openwisp_firmware_upgrader/tests/test_admin.py
@kilo-code-bot

kilo-code-bot Bot commented Mar 6, 2026

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Overview

This PR has successfully addressed all previously identified issues. The implementation now uses BlockDeleteAllowCascadeMixin from openwisp_utils (following the DRY principle) instead of a local mixin, and all security concerns have been resolved.

Previous Issues - Now Resolved

Issue Status Resolution
obj is None fallback vulnerability ✅ Fixed Now returns False when resolver_match is None
delete_selected spoofing concern ✅ Fixed Handled by BlockDeleteAllowCascadeMixin in openwisp_utils
Missing integration test for delete protection ✅ Fixed test_cascade_delete_integration verifies actual deletion
Non-superuser scenario not tested ✅ Fixed test_cascade_delete_integration_non_superuser added
Superuser bypass on unrelated URLs ✅ Fixed Now returns False for unrelated admin URLs

Implementation Review

Changes made since previous review:

  1. Removed local CascadeDeletePermissionMixin class
  2. Now imports BlockDeleteAllowCascadeMixin from openwisp_utils.admin
  3. Applied mixin only to UpgradeOperationInline (appropriate since BatchUpgradeOperationAdmin and UpgradeOperationAdmin use ReadOnlyAdmin)
  4. Updated tests to reflect the stricter permission logic:
    • no resolver_match returns False - properly denies permission when context is unknown
    • unrelated admin URL returns False - prevents permission escalation
    • own delete view must be blocked - prevents direct deletion bypass

Test coverage:

  • Unit tests for all permission scenarios (cascade, bulk, single-row, superuser, non-superuser)
  • Integration test for superuser cascade deletion
  • Integration test for non-superuser deletion blocking (returns 403 as expected)

Files Reviewed (2 files)

  • openwisp_firmware_upgrader/admin.py - Uses BlockDeleteAllowCascadeMixin from openwisp_utils; removed local mixin implementation
  • openwisp_firmware_upgrader/tests/test_admin.py - Updated tests with proper assertions for stricter permission logic

Reviewed by kimi-k2.5-0127 · 198,264 tokens

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@openwisp_firmware_upgrader/tests/test_admin.py`:
- Around line 926-952: Add an integration test that uses Django's test client
instead of mocking to exercise the real admin delete flow: in the
TestUpgradeOperationInlineDeletePermission test case create a superuser, create
an Organization and related BatchUpgradeOperation/UpgradeOperation instances,
log in with client.force_login or client.login, then perform a client.get to the
admin delete confirmation view for the Organization (URL name
admin:openwisp_users_organization_delete) and client.post the confirmation form
to trigger the cascade delete; finally assert that the related
UpgradeOperation/BatchUpgradeOperation records are removed and that the response
redirects as expected. Ensure this replaces or augments the subTest that
currently stubs resolver_match/POST.get so the regression that occurred during
real admin deletes is covered by a full client-based flow referencing
UpgradeOperationInline and BatchUpgradeOperation.
- Around line 932-950: The test hard-codes admin URL names (e.g.,
"admin:firmware_upgrader_batchupgradeoperation_change" and
"admin:openwisp_users_organization_changelist") which breaks when models are
swapped; update the test to build resolver_match.url_name dynamically using the
models' metadata: use ModelClass._meta.app_label and ModelClass._meta.model_name
to compose the admin names (prefix "admin:" + app_label + "_" + model_name +
"_change" or "_changelist" / "_delete" as needed) before calling
inline.has_delete_permission, ensuring the assertions use these derived values
rather than literals.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ebd3acd1-aabc-48c6-8b5b-dd71c054d1ec

📥 Commits

Reviewing files that changed from the base of the PR and between a503c23 and 04ba94f.

📒 Files selected for processing (2)
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/tests/test_admin.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). (5)
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=5.1.0
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2026-02-23T21:44:41.470Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/websockets.py:233-246
Timestamp: 2026-02-23T21:44:41.470Z
Learning: In openwisp-firmware-upgrader, the `upgrade_operations` attribute on `BatchUpgradeOperation` is a cached property that returns a queryset, so it should be accessed as `batch_operation.upgrade_operations.all()` rather than being called as a method.

Applied to files:

  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/tests/test_admin.py
📚 Learning: 2026-02-27T19:08:56.218Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 383
File: openwisp_firmware_upgrader/tests/test_admin.py:50-50
Timestamp: 2026-02-27T19:08:56.218Z
Learning: In tests for openwisp-firmware-upgrader, derive app_label values from model meta instead of hard-coding. Specifically, let BaseTestAdmin compute app_label from Build._meta.app_label and config_app_label from Device._meta.app_label to ensure tests remain correct if models are swapped. This pattern should apply to all test files under openwisp_firmware_upgrader/tests.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py
📚 Learning: 2026-02-25T11:20:13.903Z
Learnt from: pandafy
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: runtests.py:27-38
Timestamp: 2026-02-25T11:20:13.903Z
Learning: In openwisp-firmware-upgrader, runtests.py now uses `execute_from_command_line()` to run Django tests in-process rather than via subprocess, so the `patch = subprocess` coverage directive is not needed in pyproject.toml.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py
🔇 Additional comments (2)
openwisp_firmware_upgrader/admin.py (1)

246-254: LGTM: the permission gate matches the intended admin flows.

Making obj optional keeps the inline hook compatible with Django’s call pattern, and the _delete / delete_selected checks allow cascade and bulk deletions while still blocking deletion from the normal change view.

openwisp_firmware_upgrader/tests/test_admin.py (1)

4-25: LGTM.

The added imports are all used by the new inline-permission test and keep the test setup focused.

Comment thread openwisp_firmware_upgrader/tests/test_admin.py Outdated
Comment thread openwisp_firmware_upgrader/tests/test_admin.py Outdated
@UltraBot05 UltraBot05 force-pushed the issues/256-delete-org-blocked branch from 04ba94f to 9b18c1d Compare March 6, 2026 15:22

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

♻️ Duplicate comments (2)
openwisp_firmware_upgrader/admin.py (1)

246-253: ⚠️ Potential issue | 🔴 Critical

Restrict the bulk-delete shortcut to actual changelist requests.

Line 252 trusts any POST carrying action=delete_selected, so a crafted POST to a *_change page can re-enable inline row deletion and bypass the single-row safeguard. Require a *_changelist route before honoring that action.

🔒 Proposed fix
     def has_delete_permission(self, request, obj=None):
         if request.resolver_match:
             url_name = request.resolver_match.url_name
             # Allow cascade deletions from parent delete views or bulk / mass operations
             if url_name and url_name.endswith("_delete"):
                 return True
-            if request.POST.get("action") == "delete_selected":
+            if (
+                url_name
+                and url_name.endswith("_changelist")
+                and request.POST.get("action") == "delete_selected"
+            ):
                 return True
         return False
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_firmware_upgrader/admin.py` around lines 246 - 253, The bulk-delete
shortcut currently trusts any POST with action="delete_selected" in
has_delete_permission; restrict it by also verifying the current route is a
changelist route: when checking request.POST.get("action") == "delete_selected",
require request.resolver_match.url_name to end with "_changelist" (similar to
the existing "_delete" check) before returning True. Update
has_delete_permission to only allow the delete_selected shortcut for actual
changelist routes using the existing request.resolver_match.url_name logic.
openwisp_firmware_upgrader/tests/test_admin.py (1)

930-950: ⚠️ Potential issue | 🟡 Minor

Derive these stubbed admin names from model metadata.

These hard-coded openwisp_users / firmware_upgrader labels make the test brittle in swapped-model setups. Build the synthetic url_name values from _meta.app_label / _meta.model_name instead of literals.

♻️ Example refactor
 class TestUpgradeOperationInlineDeletePermission(TestCase):
     def test_upgrade_operation_inline_delete_permission(self):
         inline = UpgradeOperationInline(BatchUpgradeOperation, AdminSite())
+        batch_url_base = (
+            f"{BatchUpgradeOperation._meta.app_label}_"
+            f"{BatchUpgradeOperation._meta.model_name}"
+        )

         with self.subTest("cascade delete via parent delete view"):
             request = MagicMock()
-            request.resolver_match.url_name = "admin:openwisp_users_organization_delete"
+            request.resolver_match.url_name = f"{batch_url_base}_delete"
             request.POST.get.return_value = None
             self.assertTrue(inline.has_delete_permission(request))

         with self.subTest("bulk delete via delete_selected action"):
             request = MagicMock()
-            request.resolver_match.url_name = (
-                "admin:openwisp_users_organization_changelist"
-            )
+            request.resolver_match.url_name = f"{batch_url_base}_changelist"
             request.POST.get.side_effect = lambda key, default=None: (
                 "delete_selected" if key == "action" else default
             )
             self.assertTrue(inline.has_delete_permission(request))

         with self.subTest("normal change view — delete must be blocked"):
             request = MagicMock()
-            request.resolver_match.url_name = (
-                "admin:firmware_upgrader_batchupgradeoperation_change"
-            )
+            request.resolver_match.url_name = f"{batch_url_base}_change"
             request.POST.get.return_value = None
             self.assertFalse(inline.has_delete_permission(request))

Based on learnings, derive app_label values from model meta instead of hard-coding. Specifically, let BaseTestAdmin compute app_label from Build._meta.app_label and config_app_label from Device._meta.app_label to ensure tests remain correct if models are swapped.

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

In `@openwisp_firmware_upgrader/tests/test_admin.py` around lines 930 - 950, Tests
use hard-coded admin URL name strings which break when models are swapped;
change the test to derive these names from model metadata instead of literals.
In the test file where you construct request.resolver_match.url_name for checks
of inline.has_delete_permission, compute the app_label and model_name from the
relevant model classes (e.g., use Build._meta.app_label and
Build._meta.model_name for the batchchange/changelist views, and
Device._meta.app_label / Device._meta.model_name for the organization/delete
examples) — update BaseTestAdmin to expose these derived values (e.g., app_label
and config_app_label) and replace all literal
"openwisp_users"/"firmware_upgrader" strings with
f"admin:{app_label}_{model_name}_change" or
f"admin:{app_label}_{model_name}_changelist" etc., so the url_name always
reflects the models' _meta values.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@openwisp_firmware_upgrader/admin.py`:
- Around line 246-253: The bulk-delete shortcut currently trusts any POST with
action="delete_selected" in has_delete_permission; restrict it by also verifying
the current route is a changelist route: when checking
request.POST.get("action") == "delete_selected", require
request.resolver_match.url_name to end with "_changelist" (similar to the
existing "_delete" check) before returning True. Update has_delete_permission to
only allow the delete_selected shortcut for actual changelist routes using the
existing request.resolver_match.url_name logic.

In `@openwisp_firmware_upgrader/tests/test_admin.py`:
- Around line 930-950: Tests use hard-coded admin URL name strings which break
when models are swapped; change the test to derive these names from model
metadata instead of literals. In the test file where you construct
request.resolver_match.url_name for checks of inline.has_delete_permission,
compute the app_label and model_name from the relevant model classes (e.g., use
Build._meta.app_label and Build._meta.model_name for the batchchange/changelist
views, and Device._meta.app_label / Device._meta.model_name for the
organization/delete examples) — update BaseTestAdmin to expose these derived
values (e.g., app_label and config_app_label) and replace all literal
"openwisp_users"/"firmware_upgrader" strings with
f"admin:{app_label}_{model_name}_change" or
f"admin:{app_label}_{model_name}_changelist" etc., so the url_name always
reflects the models' _meta values.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 914c0e82-e21a-45fd-b71e-0575038a5c6d

📥 Commits

Reviewing files that changed from the base of the PR and between 04ba94f and 9b18c1d.

📒 Files selected for processing (2)
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/tests/test_admin.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). (4)
  • GitHub Check: Python==3.13 | django~=4.2.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=5.1.0
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2026-02-27T19:08:56.218Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 383
File: openwisp_firmware_upgrader/tests/test_admin.py:50-50
Timestamp: 2026-02-27T19:08:56.218Z
Learning: In tests for openwisp-firmware-upgrader, derive app_label values from model meta instead of hard-coding. Specifically, let BaseTestAdmin compute app_label from Build._meta.app_label and config_app_label from Device._meta.app_label to ensure tests remain correct if models are swapped. This pattern should apply to all test files under openwisp_firmware_upgrader/tests.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py
📚 Learning: 2026-02-25T11:20:13.903Z
Learnt from: pandafy
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: runtests.py:27-38
Timestamp: 2026-02-25T11:20:13.903Z
Learning: In openwisp-firmware-upgrader, runtests.py now uses `execute_from_command_line()` to run Django tests in-process rather than via subprocess, so the `patch = subprocess` coverage directive is not needed in pyproject.toml.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py
📚 Learning: 2026-02-23T21:44:41.470Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/websockets.py:233-246
Timestamp: 2026-02-23T21:44:41.470Z
Learning: In openwisp-firmware-upgrader, the `upgrade_operations` attribute on `BatchUpgradeOperation` is a cached property that returns a queryset, so it should be accessed as `batch_operation.upgrade_operations.all()` rather than being called as a method.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py
📚 Learning: 2026-02-21T20:21:04.228Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/migrations/0015_add_group_to_batchupgradeoperation.py:16-26
Timestamp: 2026-02-21T20:21:04.228Z
Learning: In openwisp-firmware-upgrader project, the team prefers not to add explicit `related_name` to ForeignKey fields in migrations when the default reverse accessor (`<model>_set`) is acceptable. They are comfortable with Django's default naming convention.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py

@UltraBot05 UltraBot05 force-pushed the issues/256-delete-org-blocked branch from 9b18c1d to c41ab6a Compare March 6, 2026 15:33
Implemented has_delete_permission to allow deletions when triggered by a parent object or bulk actions. Mirrors logic from openwisp-utils #596.

Added unit tests to verify the permission logic.

Fixes openwisp#256
@UltraBot05 UltraBot05 force-pushed the issues/256-delete-org-blocked branch from c41ab6a to 21bd3fd Compare March 6, 2026 19:35

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@openwisp_firmware_upgrader/admin.py`:
- Around line 326-346: The has_delete_permission implementation in
openwisp_firmware_upgrader.admin (method has_delete_permission) duplicates the
same logic in UpgradeOperationInline; extract the shared logic into a new mixin
(e.g., CascadeDeletePermissionMixin) that implements has_delete_permission, then
have both the current admin class and UpgradeOperationInline inherit from that
mixin; ensure the mixin preserves the existing checks (superuser short-circuit,
obj None handling, resolver_match URL suffix checks for "_delete" and
"_changelist" with delete_selected) and remove the duplicated method from both
classes so they rely on the mixin.
- Around line 246-266: In has_delete_permission, the check "elif obj is None" is
nested under the falsy request.resolver_match branch so it won't run during
admin cascade delete confirmations which do have a resolver_match; change the
logic so the obj is None check is evaluated independently (e.g., an explicit "if
obj is None: return True" placed after the superuser check and
before/independent of the resolver_match handling) while keeping the existing
resolver_match/url_name checks and superuser short-circuit intact.

In `@openwisp_firmware_upgrader/tests/test_admin.py`:
- Around line 927-958: The test misses coverage for the branches in
UpgradeOperationInline.has_delete_permission where request.resolver_match is
None (obj is None fallback) and where the request.user is a superuser
(early-return true); update
TestUpgradeOperationInlineDeletePermission.test_upgrade_operation_inline_delete_permission
to add two subTests: one that sets request.resolver_match = None and calls
inline.has_delete_permission(request, obj=None) asserting True, and another that
sets request.user.is_superuser = True with a resolver_match.url_name and calls
inline.has_delete_permission(request, obj=MagicMock()) asserting True so both
branches in has_delete_permission are exercised.
- Around line 960-974: The current test test_cascade_delete_integration only
covers superuser flow via _login(); add a second integration test that logs in
as a non-superuser (e.g., an organization admin), grants or assigns the
necessary admin delete permission for the Organization, and then performs the
same POST to the admin delete URL to verify the URL-based permission logic and
cascade deletion; locate the has_delete_permission method in admin.py and the
Organization model to ensure the test sets up the org-admin user and permissions
correctly, then assert the Organization and related BatchUpgradeOperation are
(or are not) deleted as expected.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b355d147-72f5-40f4-b4a8-2ceb1aa837a9

📥 Commits

Reviewing files that changed from the base of the PR and between 9b18c1d and 21bd3fd.

📒 Files selected for processing (2)
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/tests/test_admin.py
📜 Review details
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/websockets.py:233-246
Timestamp: 2026-02-23T21:44:41.470Z
Learning: In openwisp-firmware-upgrader, the `upgrade_operations` attribute on `BatchUpgradeOperation` is a cached property that returns a queryset, so it should be accessed as `batch_operation.upgrade_operations.all()` rather than being called as a method.
📚 Learning: 2026-02-25T11:20:13.903Z
Learnt from: pandafy
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: runtests.py:27-38
Timestamp: 2026-02-25T11:20:13.903Z
Learning: In openwisp-firmware-upgrader, runtests.py now uses `execute_from_command_line()` to run Django tests in-process rather than via subprocess, so the `patch = subprocess` coverage directive is not needed in pyproject.toml.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py
📚 Learning: 2026-02-27T19:08:56.218Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 383
File: openwisp_firmware_upgrader/tests/test_admin.py:50-50
Timestamp: 2026-02-27T19:08:56.218Z
Learning: In tests for openwisp-firmware-upgrader, derive app_label values from model meta instead of hard-coding. Specifically, let BaseTestAdmin compute app_label from Build._meta.app_label and config_app_label from Device._meta.app_label to ensure tests remain correct if models are swapped. This pattern should apply to all test files under openwisp_firmware_upgrader/tests.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py
📚 Learning: 2026-02-23T21:44:41.470Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/websockets.py:233-246
Timestamp: 2026-02-23T21:44:41.470Z
Learning: In openwisp-firmware-upgrader, the `upgrade_operations` attribute on `BatchUpgradeOperation` is a cached property that returns a queryset, so it should be accessed as `batch_operation.upgrade_operations.all()` rather than being called as a method.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/admin.py
📚 Learning: 2026-02-21T20:21:04.228Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/migrations/0015_add_group_to_batchupgradeoperation.py:16-26
Timestamp: 2026-02-21T20:21:04.228Z
Learning: In openwisp-firmware-upgrader project, the team prefers not to add explicit `related_name` to ForeignKey fields in migrations when the default reverse accessor (`<model>_set`) is acceptable. They are comfortable with Django's default naming convention.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py
🔇 Additional comments (1)
openwisp_firmware_upgrader/tests/test_admin.py (1)

4-8: LGTM!

Imports are appropriate for the new test cases.

Comment thread openwisp_firmware_upgrader/admin.py Outdated
Comment thread openwisp_firmware_upgrader/admin.py Outdated
Comment thread openwisp_firmware_upgrader/tests/test_admin.py Outdated
Comment thread openwisp_firmware_upgrader/tests/test_admin.py
Comment thread openwisp_firmware_upgrader/admin.py Outdated
Comment thread openwisp_firmware_upgrader/admin.py Outdated
Comment thread openwisp_firmware_upgrader/tests/test_admin.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
openwisp_firmware_upgrader/admin.py (2)

612-632: 🛠️ Refactor suggestion | 🟠 Major

DRY violation: Identical permission logic duplicated.

This method is an exact copy of UpgradeOperationInline.has_delete_permission. Additionally, it has the same logic issue where the elif obj is None branch (line 630) is unreachable during admin requests.

Extract the shared logic into a reusable mixin to avoid maintenance burden and ensure consistency.

♻️ Proposed refactor using a mixin
class CascadeDeletePermissionMixin:
    """
    Mixin that allows cascade/bulk deletions while blocking
    single-row deletion in the change view.
    """
    def has_delete_permission(self, request, obj=None):
        if getattr(getattr(request, "user", None), "is_superuser", False) is True:
            return True
        if obj is None:
            return True
        if request.resolver_match:
            url_name = request.resolver_match.url_name
            if url_name and url_name.endswith("_delete"):
                return True
            if (
                url_name
                and url_name.endswith("_changelist")
                and request.POST.get("action") == "delete_selected"
            ):
                return True
        return False

Then inherit from it:

-class UpgradeOperationInline(admin.StackedInline):
+class UpgradeOperationInline(CascadeDeletePermissionMixin, admin.StackedInline):
     model = UpgradeOperation
-    # ... remove the has_delete_permission method
-class BatchUpgradeOperationAdmin(ReadonlyUpgradeOptionsMixin, ReadOnlyAdmin, BaseAdmin):
+class BatchUpgradeOperationAdmin(CascadeDeletePermissionMixin, ReadonlyUpgradeOptionsMixin, ReadOnlyAdmin, BaseAdmin):
     # ... remove the has_delete_permission method
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_firmware_upgrader/admin.py` around lines 612 - 632, The
has_delete_permission implementation in openwisp_firmware_upgrader.admin
(duplicated from UpgradeOperationInline.has_delete_permission) should be
extracted into a shared CascadeDeletePermissionMixin to remove duplication;
create a mixin (e.g., CascadeDeletePermissionMixin) that implements
has_delete_permission with the correct branch order (check superuser, then if
obj is None return True, then evaluate request.resolver_match/url_name cascade
or bulk delete checks) and update both the current admin class and
UpgradeOperationInline to inherit this mixin and remove their local
has_delete_permission implementations.

342-345: ⚠️ Potential issue | 🟠 Major

Logic issue: elif obj is None branch is unreachable during cascade delete.

The elif obj is None branch (line 344) only executes when request.resolver_match is falsy. However, Django admin requests always have a valid resolver_match, so this branch will never execute during cascade delete confirmation pages.

The obj is None check should be independent of the resolver_match condition to properly handle the cascade delete confirmation scenario.

🔧 Proposed fix
     def has_delete_permission(self, request, obj=None):
         # Superusers can delete anything (but skip if mocked)
         if getattr(getattr(request, "user", None), "is_superuser", False) is True:
             return True
+        # Allow if obj is None (general permission check, not instance-specific)
+        if obj is None:
+            return True
         # Check if this is a cascade delete from a parent view or bulk action
         if request.resolver_match:
             url_name = request.resolver_match.url_name
             # Allow cascade deletions from parent delete views or bulk / mass operations
             if url_name and url_name.endswith("_delete"):
                 return True
             if (
                 url_name
                 and url_name.endswith("_changelist")
                 and request.POST.get("action") == "delete_selected"
             ):
                 return True
-        # Allow if obj is None (cascade delete check during confirmation page)
-        # or if we can't determine the URL (internal permission check)
-        elif obj is None:
-            return True
         return False
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_firmware_upgrader/admin.py` around lines 342 - 345, The current
branch combines the resolver_match check with the obj is None check making "elif
obj is None" unreachable; instead, handle the cascade-delete case independently
by checking "if obj is None: return True" before or as a separate conditional
from the resolver_match logic that uses request.resolver_match, ensuring the obj
None case is evaluated regardless of request.resolver_match; update the block
that currently reads "elif obj is None" (the conditional using
request.resolver_match) to use a standalone "if obj is None: return True" so
cascade delete confirmation pages are allowed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@openwisp_firmware_upgrader/tests/test_admin.py`:
- Around line 1489-1516: Add two missing subtests to fully exercise
inline.has_delete_permission: one that sets request.user.is_superuser = True
(explicit boolean, not a MagicMock) and calls
inline.has_delete_permission(request, obj=MagicMock()) to hit the superuser
early-return path; and another that sets request.user.is_superuser = False and
request.resolver_match = None and calls inline.has_delete_permission(request,
obj=None) to exercise the fallback when resolver_match is falsy. Ensure the
tests reference the same inline object used elsewhere and keep the subTest
labels similar to existing ones for clarity.
- Around line 1491-1494: The tests set request.resolver_match.url_name
incorrectly including the "admin:" namespace; update assignments that build the
URL name (the ones using
f"admin:{Organization._meta.app_label}_{Organization._meta.model_name}_delete")
to remove the "admin:" prefix so they assign
f"{Organization._meta.app_label}_{Organization._meta.model_name}_delete"
instead; apply the same change for the two other occurrences that construct URL
names (the other similar f-strings near the blocks that reference
resolver_match.url_name).

---

Duplicate comments:
In `@openwisp_firmware_upgrader/admin.py`:
- Around line 612-632: The has_delete_permission implementation in
openwisp_firmware_upgrader.admin (duplicated from
UpgradeOperationInline.has_delete_permission) should be extracted into a shared
CascadeDeletePermissionMixin to remove duplication; create a mixin (e.g.,
CascadeDeletePermissionMixin) that implements has_delete_permission with the
correct branch order (check superuser, then if obj is None return True, then
evaluate request.resolver_match/url_name cascade or bulk delete checks) and
update both the current admin class and UpgradeOperationInline to inherit this
mixin and remove their local has_delete_permission implementations.
- Around line 342-345: The current branch combines the resolver_match check with
the obj is None check making "elif obj is None" unreachable; instead, handle the
cascade-delete case independently by checking "if obj is None: return True"
before or as a separate conditional from the resolver_match logic that uses
request.resolver_match, ensuring the obj None case is evaluated regardless of
request.resolver_match; update the block that currently reads "elif obj is None"
(the conditional using request.resolver_match) to use a standalone "if obj is
None: return True" so cascade delete confirmation pages are allowed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: bbbe8b50-f10b-430a-9574-43f1022f125d

📥 Commits

Reviewing files that changed from the base of the PR and between 21bd3fd and 9249209.

📒 Files selected for processing (2)
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/tests/test_admin.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~=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.13 | django~=4.2.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.12 | 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.11 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=5.1.0
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/websockets.py:233-246
Timestamp: 2026-02-23T21:44:41.470Z
Learning: In openwisp-firmware-upgrader, the `upgrade_operations` attribute on `BatchUpgradeOperation` is a cached property that returns a queryset, so it should be accessed as `batch_operation.upgrade_operations.all()` rather than being called as a method.
📚 Learning: 2026-02-27T19:08:56.218Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 383
File: openwisp_firmware_upgrader/tests/test_admin.py:50-50
Timestamp: 2026-02-27T19:08:56.218Z
Learning: In tests for openwisp-firmware-upgrader, derive app_label values from model meta instead of hard-coding. Specifically, let BaseTestAdmin compute app_label from Build._meta.app_label and config_app_label from Device._meta.app_label to ensure tests remain correct if models are swapped. This pattern should apply to all test files under openwisp_firmware_upgrader/tests.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py
📚 Learning: 2026-02-25T11:20:13.903Z
Learnt from: pandafy
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: runtests.py:27-38
Timestamp: 2026-02-25T11:20:13.903Z
Learning: In openwisp-firmware-upgrader, runtests.py now uses `execute_from_command_line()` to run Django tests in-process rather than via subprocess, so the `patch = subprocess` coverage directive is not needed in pyproject.toml.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py
📚 Learning: 2026-02-21T20:21:04.228Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/migrations/0015_add_group_to_batchupgradeoperation.py:16-26
Timestamp: 2026-02-21T20:21:04.228Z
Learning: In openwisp-firmware-upgrader project, the team prefers not to add explicit `related_name` to ForeignKey fields in migrations when the default reverse accessor (`<model>_set`) is acceptable. They are comfortable with Django's default naming convention.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py
📚 Learning: 2026-02-25T00:17:23.479Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/static/firmware-upgrader/js/upgrade-progress.js:458-463
Timestamp: 2026-02-25T00:17:23.479Z
Learning: In the openwisp/openwisp-firmware-upgrader repository, focus review comments on real, actionable issues rather than theoretical edge cases, trivialities, or potential future problems. Avoid flagging issues that are functionally correct but could theoretically become problems only if the code were significantly refactored or used in different contexts.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py
📚 Learning: 2026-02-23T21:44:41.470Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/websockets.py:233-246
Timestamp: 2026-02-23T21:44:41.470Z
Learning: In openwisp-firmware-upgrader, the `upgrade_operations` attribute on `BatchUpgradeOperation` is a cached property that returns a queryset, so it should be accessed as `batch_operation.upgrade_operations.all()` rather than being called as a method.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/admin.py
🔇 Additional comments (1)
openwisp_firmware_upgrader/tests/test_admin.py (1)

1518-1532: Integration test validates cascade deletion for superusers only.

The _login() method logs in as a superuser, so this test exercises the superuser early-return path in has_delete_permission. Since the original issue #256 may have affected non-superuser organization admins as well, consider adding an integration test with a non-superuser to verify the URL-based permission logic works in a real request context.

However, the test does effectively validate that the cascade delete flow works end-to-end, which provides confidence that issue #256 is fixed for superusers.

Comment thread openwisp_firmware_upgrader/tests/test_admin.py Outdated
Comment thread openwisp_firmware_upgrader/tests/test_admin.py
@UltraBot05 UltraBot05 force-pushed the issues/256-delete-org-blocked branch from 0b69191 to e683bb0 Compare March 9, 2026 06:29
@UltraBot05 UltraBot05 force-pushed the issues/256-delete-org-blocked branch from e683bb0 to e684f6b Compare March 9, 2026 06:31

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@openwisp_firmware_upgrader/admin.py`:
- Around line 70-80: The allowlist currently treats any url_name ending with
"_delete" as safe, which also permits this admin's own single-row delete; update
the guard that uses request.resolver_match.url_name so it excludes the admin's
own delete view by comparing against the admin model's expected admin url_name
(build using self.model._meta.app_label and self.model._meta.model_name), e.g.
only allow when url_name.endswith("_delete") AND url_name !=
f"{self.model._meta.app_label}_{self.model._meta.model_name}_delete"; keep the
existing bulk-changelist check as-is.

In `@openwisp_firmware_upgrader/tests/test_admin.py`:
- Around line 1562-1570: The test currently accepts both 200 and 403 outcomes
which masks regressions; modify the test around the self.client.post(delete_url,
...) call to set an explicit permission state for the test user (either make the
user a superuser or assign the specific permission required to delete the
Organization and BatchUpgradeOperation) and then assert only the expected result
(e.g. assertEqual(response.status_code, 200) and assert the
Organization/BatchUpgradeOperation no longer exist, or
assertEqual(response.status_code, 403) and assert they still exist). Update
references to delete_url, self.client.post,
Organization.objects.filter(pk=org.pk).exists(), and
BatchUpgradeOperation.objects.filter(pk=batch.pk).exists() accordingly and
remove the conditional branch that accepts both statuses.
- Around line 1489-1506: The two subtests call
inline.has_delete_permission(request) without an obj, so the function returns
early at the "obj is None" guard and never exercises the URL-based branches;
update both subtests to pass a non-None obj (e.g., a simple Organization
instance or a MagicMock standing in for the model instance) when calling
inline.has_delete_permission so the logic that inspects
request.resolver_match.url_name (using Organization._meta.app_label and
Organization._meta.model_name) and request.POST.get("action") for
"delete_selected" is actually executed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 14c53784-0f5f-425d-86bc-5e2d2d895a00

📥 Commits

Reviewing files that changed from the base of the PR and between 9249209 and 0b69191.

📒 Files selected for processing (2)
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/tests/test_admin.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.11 | 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.11 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.13 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=5.2.0
🧰 Additional context used
🧠 Learnings (9)
📓 Common learnings
Learnt from: atif09
Repo: openwisp/openwisp-firmware-upgrader PR: 387
File: openwisp_firmware_upgrader/admin.py:420-423
Timestamp: 2026-03-07T01:16:24.447Z
Learning: In openwisp-firmware-upgrader, UpgradeOperation and BatchUpgradeOperation always share the same organization (BatchUpgradeOperation.build.category.organization). A user who can view an UpgradeOperation can always view its related BatchUpgradeOperation. Model-level has_view_permission(request) is sufficient for batch breadcrumb visibility in UpgradeOperationAdmin.change_view; object-level permission checks are not needed.
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/websockets.py:233-246
Timestamp: 2026-02-23T21:44:41.470Z
Learning: In openwisp-firmware-upgrader, the `upgrade_operations` attribute on `BatchUpgradeOperation` is a cached property that returns a queryset, so it should be accessed as `batch_operation.upgrade_operations.all()` rather than being called as a method.
📚 Learning: 2026-03-07T01:16:15.471Z
Learnt from: atif09
Repo: openwisp/openwisp-firmware-upgrader PR: 387
File: openwisp_firmware_upgrader/admin.py:420-423
Timestamp: 2026-03-07T01:16:15.471Z
Learning: In openwisp-firmware-upgrader, UpgradeOperation and BatchUpgradeOperation always share the same organization (BatchUpgradeOperation.build.category.organization). A user who can view an UpgradeOperation can always view its related BatchUpgradeOperation. For the UpgradeOperationAdmin.change_view, model-level has_view_permission(request) suffices to show breadcrumbs for batch, and object-level permission checks are not required.

Applied to files:

  • openwisp_firmware_upgrader/admin.py
📚 Learning: 2026-02-23T21:44:41.470Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/websockets.py:233-246
Timestamp: 2026-02-23T21:44:41.470Z
Learning: In openwisp-firmware-upgrader, the `upgrade_operations` attribute on `BatchUpgradeOperation` is a cached property that returns a queryset, so it should be accessed as `batch_operation.upgrade_operations.all()` rather than being called as a method.

Applied to files:

  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/tests/test_admin.py
📚 Learning: 2026-03-07T01:16:24.447Z
Learnt from: atif09
Repo: openwisp/openwisp-firmware-upgrader PR: 387
File: openwisp_firmware_upgrader/admin.py:420-423
Timestamp: 2026-03-07T01:16:24.447Z
Learning: In openwisp-firmware-upgrader, UpgradeOperation and BatchUpgradeOperation always share the same organization (BatchUpgradeOperation.build.category.organization). A user who can view an UpgradeOperation can always view its related BatchUpgradeOperation. Model-level has_view_permission(request) is sufficient for batch breadcrumb visibility in UpgradeOperationAdmin.change_view; object-level permission checks are not needed.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py
📚 Learning: 2026-02-27T19:08:56.218Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 383
File: openwisp_firmware_upgrader/tests/test_admin.py:50-50
Timestamp: 2026-02-27T19:08:56.218Z
Learning: In tests for openwisp-firmware-upgrader, derive app_label values from model meta instead of hard-coding. Specifically, let BaseTestAdmin compute app_label from Build._meta.app_label and config_app_label from Device._meta.app_label to ensure tests remain correct if models are swapped. This pattern should apply to all test files under openwisp_firmware_upgrader/tests.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py
📚 Learning: 2026-02-25T11:20:13.903Z
Learnt from: pandafy
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: runtests.py:27-38
Timestamp: 2026-02-25T11:20:13.903Z
Learning: In openwisp-firmware-upgrader, runtests.py now uses `execute_from_command_line()` to run Django tests in-process rather than via subprocess, so the `patch = subprocess` coverage directive is not needed in pyproject.toml.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py
📚 Learning: 2026-02-21T20:21:04.228Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/migrations/0015_add_group_to_batchupgradeoperation.py:16-26
Timestamp: 2026-02-21T20:21:04.228Z
Learning: In openwisp-firmware-upgrader project, the team prefers not to add explicit `related_name` to ForeignKey fields in migrations when the default reverse accessor (`<model>_set`) is acceptable. They are comfortable with Django's default naming convention.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py
📚 Learning: 2026-02-25T00:17:23.479Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/static/firmware-upgrader/js/upgrade-progress.js:458-463
Timestamp: 2026-02-25T00:17:23.479Z
Learning: In the openwisp/openwisp-firmware-upgrader repository, focus review comments on real, actionable issues rather than theoretical edge cases, trivialities, or potential future problems. Avoid flagging issues that are functionally correct but could theoretically become problems only if the code were significantly refactored or used in different contexts.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py
📚 Learning: 2026-02-26T17:22:26.110Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 0
File: :0-0
Timestamp: 2026-02-26T17:22:26.110Z
Learning: In the openwisp/openwisp-firmware-upgrader repository, avoid making suggestions about UI/frontend code (JavaScript DOM traversal, CSS selectors, jQuery operations) unless there is a clear, objective bug (e.g., syntax error, undefined variable). UI code structure and DOM traversal paths should be verified through manual testing by the maintainers, as suggestions without access to the actual rendered DOM can break functionality.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py

Comment thread openwisp_firmware_upgrader/admin.py Outdated
Comment thread openwisp_firmware_upgrader/tests/test_admin.py Outdated
Comment thread openwisp_firmware_upgrader/tests/test_admin.py Outdated
coderabbitai[bot]
coderabbitai Bot previously approved these changes Mar 9, 2026
@UltraBot05

UltraBot05 commented Mar 9, 2026

Copy link
Copy Markdown
Author

@nemesifier Hi! Apologies for the silence from my end for the past few days - I was tied up with University Exams!

I've pushed the latest updates to handle the UpgradeOperation side of the #256 deletion bug. All the structural feedback from the bots has been resolved: the permission rules are now DRYed out into a shared CascadeDeletePermissionMixin, the URL guard explicitly blocks the model's own single-row delete view to prevent spoofing, and I added a non-superuser integration test to properly exercise the URL-routing paths instead of relying on the superuser early-return.

This pairs with #596 over at openwisp-utils to fully resolve the issue. The CI is completely green, so it's ready for your review whenever you have time!

@UltraBot05 UltraBot05 changed the title [admin] Fix cascade deletion blocked by UpgradeOperation #256 [Fix] Cascade deletion blocked by UpgradeOperation #256 Mar 9, 2026

@Viscous106 Viscous106 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@UltraBot05 LGTM ! All the test case are running and the bug seem to have fixed on testing on my local machine there is just one minute change i have suggested above .

Comment thread openwisp_firmware_upgrader/admin.py Outdated
coderabbitai[bot]
coderabbitai Bot previously approved these changes Mar 14, 2026
@openwisp-companion

Copy link
Copy Markdown
{
  "failures": [
    {
      "type": "Build/Infrastructure/Other",
      "message": "The installation of `openwisp-ipam` failed due to repeated 502 errors from GitHub.",
      "details": "HTTPSConnectionPool(host='github.com', port=443): Max retries exceeded with url: /openwisp/openwisp-ipam/archive/refs/heads/1.3.tar.gz (Caused by ResponseError('too many 502 error responses'))"
    }
  ]
}

Build/Infrastructure/Other

Hello @UltraBot05,
(Analysis for commit 560ff13)

The CI build failed because it was unable to download the openwisp-ipam package from GitHub. The logs indicate repeated 502 errors, which suggests a temporary issue with the GitHub API or network connectivity.

Fix:
This appears to be a transient infrastructure issue. The CI build will likely succeed if retried. If the problem persists, it might indicate a more significant network or GitHub API issue that needs to be addressed by the infrastructure team.

@UltraBot05

UltraBot05 commented Jun 21, 2026

Copy link
Copy Markdown
Author

@nemesifier following up on your next steps from the May 19 message on utils#596 I've updated this PR to align with the stricter BlockDeleteAllowCascadeMixin pattern.

Changes made:

  • Removed the local CascadeDeletePermissionMixin entirely
  • UpgradeOperationInline now uses BlockDeleteAllowCascadeMixin directly from openwisp_utils.admin
  • UpgradeOperationAdmin and BatchUpgradeOperationAdmin inherit through ReadOnlyAdmin which already includes the mixin..

Also resolved the merge conflict with upstream master (the BaseUpgradeAdmin refactor)

Also merged latest upstream/master into the branch - git merge confirms it's up to date, but GitHub's web UI is showing a false conflict banner on admin.py with the "too complex to resolve in web editor" message.
I think that the branch is clean and mergeable from the command line, since the CLI shows up to date.

Re: your note about Device

test again in a full OpenWISP environment. If org deletion still reports Device, that is a separate blocker in the device admin and should be fixed in the related app.

Got it, if org deletion still reports that blocker after these two merge, it belongs to openwisp-controller and I'll open a separate issue there per firmware-upgrader#256.

Edit: Updated the PR Description

@UltraBot05

Copy link
Copy Markdown
Author

Tested locally with a full setup MRO (org → category → build → device → batch upgrade operation → upgrade operation).

Results:

  • With operations in in-progress status: blocked as expected - BaseUpgradeAdmin.has_delete_permission intentionally prevents deletion of in-progress operations, even via cascade. Correct behavior.
  • With operations in a completed status and active device: only Device blocks deletion. Mass upgrade operation and upgrade operation are fully resolved by this PR + utils#596.
  • With operations in completed status and deactivated device: org deletion succeeds completely - confirmation page shows full cascade tree and deletion goes through.

The Device blocker with active devices is the separate openwisp-controller issue you mentioned. Will open a follow-up issue there once these two merge.

@openwisp-companion

Copy link
Copy Markdown

Commit Message Failure

Hello @UltraBot05,
(Analysis for commit 211e6de)

The commit message is invalid because it does not follow the expected format.

Fix:

Please reformat your commit message to adhere to the following convention:

[tag] Capitalized short title #<issue>

<description>

Fixes #<issue>

Example:

[feature] Add user authentication #123

This commit introduces user authentication by implementing JWT tokens
for API requests.

Fixes #123

Test Failure

The test test_cascade_delete_integration in openwisp_firmware_upgrader.tests.test_admin.TestUpgradeOperationInlineDeletePermission failed with an AssertionError: 403 != 200. This indicates that a permission check returned a 403 Forbidden status code when a 200 OK was expected.

Fix:

Review the has_delete_permission method in openwisp_firmware_upgrader/admin.py and the associated test logic to ensure that delete permissions are correctly handled, especially for operations that might be in progress. The test failure suggests that the logic for determining delete permissions might be too restrictive or that the test setup is not correctly simulating the expected state.

@UltraBot05 UltraBot05 force-pushed the issues/256-delete-org-blocked branch from 211e6de to 2b6acc6 Compare June 25, 2026 06:48
…wisp#256

super(ReadOnlyAdmin, self) was skipping BlockDeleteAllowCascadeMixin in the MRO, preventing cascade delete requests from being correctly identified and allowed. Changed to super() to follow the full MRO. Also set status="success" in test_cascade_delete_integration to avoid the intentional in-progress deletion block.

Fixes openwisp#256
@UltraBot05 UltraBot05 force-pushed the issues/256-delete-org-blocked branch from 2b6acc6 to 945f4e9 Compare June 25, 2026 06:54
@openwisp-companion

Copy link
Copy Markdown
<h3>Test Failures in Admin Operations</h3>

Hello @UltraBot05,
(Analysis for commit 945f4e9)

  • Test Failure: The test test_batch_upgrade_operation_admin_bulk_delete_by_status failed because the expected message "Some selected operations are still in progress and cannot be deleted. Remove them from the selection and try again." was not found in the response.

  • Fix: Review the logic in openwisp_firmware_upgrader/admin.py for handling bulk deletion of upgrade operations, specifically in the UpgradeOperationAdmin.delete_selected method, to ensure the correct message is returned or the condition for deletion is met.

  • Test Failure: The test test_upgrade_operation_admin_delete_by_status failed because the delete URL /admin/firmware_upgrader/upgradeoperation/505cb530-b85a-465e-bb72-eb641dcf04e5/delete/ was not found in the response.

  • Fix: This indicates that the delete view for a specific upgrade operation was not accessible or not rendered correctly. Check the UpgradeOperationAdmin.has_delete_permission method in openwisp_firmware_upgrader/admin.py to ensure it correctly determines delete permissions, especially for operations that are not in progress.

  • Test Failure: The test test_upgrade_operation_admin_delete_selected_action_present failed because the option <option value="delete_selected"> was not found in the response.

  • Fix: This suggests that the "Delete selected" action might not be correctly registered or displayed for the UpgradeOperation model in the admin interface. Verify the actions attribute in UpgradeOperationAdmin and ensure delete_selected is correctly included and that the necessary permissions are set.

  • Test Failure: The test test_batch_upgrade_operation_admin_bulk_delete_by_status also failed with an AttributeError: Can't pickle local object 'convert_exception_to_response.<locals>.inner'. This error typically occurs when trying to pickle objects that cannot be serialized, often due to nested functions or closures.

  • Fix: This is likely related to how subtests or exceptions are handled within the test case. Review the unittest.case.subTest usage and exception handling in openwisp_firmware_upgrader/tests/test_admin.py to ensure that any objects passed to subtests or caught exceptions are picklable.

  • Test Failure: The test test_batch_upgrade_operation_admin_delete_by_status failed with an AttributeError: Can't pickle local object 'convert_exception_to_response.<locals>.inner'.

  • Fix: Similar to the above, this points to an issue with pickling objects within subtests or exception handling. Examine the test's structure in openwisp_firmware_upgrader/tests/test_admin.py for potential pickling issues.

  • Test Failure: The test test_upgrade_operation_admin_delete_selected_action_present also failed with an AttributeError: Can't pickle local object 'convert_exception_to_response.<locals>.inner'.

  • Fix: Again, this is likely a pickling issue related to subtests or exception handling within the test. Inspect the test implementation in openwisp_firmware_upgrader/tests/test_admin.py.

…penwisp#256

Reverts the super(ReadOnlyAdmin, self) change which broke existing delete behavior in BaseUpgradeAdmin. Also sets status="success" in test_cascade_delete_integration to avoid the intentional in-progress deletion block.

Fixes openwisp#256
@openwisp-companion

Copy link
Copy Markdown

Syntax Warnings in Python Files

Hello @UltraBot05,
(Analysis for commit a0f6822)

The CI build failed due to several SyntaxWarning: invalid escape sequence warnings. These warnings indicate that the backslash (\) in string literals is being interpreted as an escape character, which can lead to unexpected behavior or errors.

Fix:

To resolve this, you need to escape the backslashes in the problematic string literals by doubling them (\\). This will ensure that the backslashes are treated as literal characters.

The warnings are occurring in the following files and lines:

  • openwisp_firmware_upgrader/upgraders/openwrt.py, lines 94, 95, 96, 97, 99, 100, 101
  • openwisp_firmware_upgrader/tests/test_openwrt_upgrader.py, lines 195, 196, 197, 198

Please review these lines and escape the backslashes accordingly.

@UltraBot05

Copy link
Copy Markdown
Author

@nemesifier
Quick update on the CI failures: I investigated further and found that the failing tests (test_upgrade_operation_admin_delete_selected_action_present, test_batch_upgrade_operation_admin_bulk_delete_by_status, etc.) also fail on upstream/master at 567acae with no changes from this branch applied.

Verified by checking out upstream/master directly and running those tests - same failures.

Could you let me know if there's something I'm missing about how these tests are expected to interact with the BlockDeleteAllowCascadeMixin on ReadOnlyAdmin?

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug] Deleting organization is prevented by Mass Upgrade Operation

3 participants