Skip to content

[fix] Allow cascade deletions in ReadOnlyAdmin#596

Open
UltraBot05 wants to merge 1 commit intoopenwisp:masterfrom
UltraBot05:fix/readonlyadmin-cascade-delete
Open

[fix] Allow cascade deletions in ReadOnlyAdmin#596
UltraBot05 wants to merge 1 commit intoopenwisp:masterfrom
UltraBot05:fix/readonlyadmin-cascade-delete

Conversation

@UltraBot05
Copy link

@UltraBot05 UltraBot05 commented Feb 24, 2026

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

ReadOnlyAdmin.has_delete_permission previously returned False unconditionally, which blocked cascade deletions from parent models (like Organization).

Updated the method to check request.resolver_match.url_name. It now correctly blocks direct deletes on the model's own views while delegating to the standard Django permission check for cascade deletes.

Screenshot

Screenshot 1 Shows the deletion of org -> Failure page
Screenshot 2 Shows the working Fix

#256-before #256-after

@coderabbitai
Copy link

coderabbitai bot commented Feb 24, 2026

📝 Walkthrough

Walkthrough

The changes modify ReadOnlyAdmin.has_delete_permission to deny deletion only for requests whose resolver URL name matches the model-specific delete view or the changelist view; for other request contexts it delegates to super().has_delete_permission(request, obj). A new test verifies that a direct delete URL returns False while a simulated cascade-delete context (resolver_match.url_name set to "index") results in True.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 2

❌ Failed checks (2 inconclusive)

Check name Status Explanation Resolution
Linked Issues check ❓ Inconclusive The PR references issue #256 about commit message formatting, but the actual code changes implement cascade deletion fixes to ReadOnlyAdmin, which are unrelated to the stated issue's objectives. Verify whether the correct issue number is referenced. The code changes appear to implement cascade deletion functionality, but issue #256 concerns commit message formatting, suggesting a possible issue linkage mismatch.
Out of Scope Changes check ❓ Inconclusive The code changes properly implement cascade deletion functionality for ReadOnlyAdmin and include appropriate tests, but the referenced issue #256 concerns commit message formatting rather than admin permission logic. Clarify the correct issue number that this PR should reference, as the current linkage to issue #256 appears misaligned with the actual implementation changes.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: updating ReadOnlyAdmin.has_delete_permission to allow cascade deletions while still blocking direct deletes on the model's own views.
Description check ✅ Passed The PR description follows the template structure with all required sections completed: checklist items marked, issue reference provided, clear description of changes, and supporting screenshots included.

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

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

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

❤️ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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_utils/admin.py`:
- Around line 36-44: has_delete_permission currently blocks delete for delete
and changelist URLs but not the change view, so the delete button can still
appear; update the check in has_delete_permission to also include
f"{opts.app_label}_{opts.model_name}_change" in the tuple of blocked url_name
values (use the existing local variables opts and url_name to locate the logic)
so the method returns False for the change view as well and otherwise falls back
to super().has_delete_permission(request, obj).

In `@tests/test_project/tests/test_admin.py`:
- Around line 122-142: has_delete_permission currently dereferences
request.resolver_match.url_name and can raise AttributeError for requests where
resolver_match is None; update ReadOnlyAdmin.has_delete_permission to guard
using url_name = getattr(request.resolver_match, "url_name", None) and then
check the two URL names (f"{opts.app_label}_{opts.model_name}_delete" and
f"{opts.app_label}_{opts.model_name}_changelist") before returning False,
otherwise return super().has_delete_permission(request, obj). Add a unit test in
tests/test_admin.py that sets request.resolver_match = None (or uses a
RequestFactory request without URL resolution) and asserts the call to
modeladmin.has_delete_permission(request) does not raise and returns the same
value as super().has_delete_permission for that request.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 782c8d9 and c567a9a.

📒 Files selected for processing (2)
  • openwisp_utils/admin.py
  • tests/test_project/tests/test_admin.py
📜 Review details
🧰 Additional context used
🧬 Code graph analysis (1)
tests/test_project/tests/test_admin.py (2)
openwisp_utils/admin.py (2)
  • ReadOnlyAdmin (15-59)
  • has_delete_permission (36-44)
tests/test_project/models.py (1)
  • RadiusAccounting (81-101)
🔇 Additional comments (1)
openwisp_utils/admin.py (1)

36-44: Unnecessary defensive programming: resolver_match is guaranteed in Django admin context.

In Django admin, request.resolver_match is always set by the URL dispatcher before has_delete_permission() is called. The method is only invoked during normal admin operations where the request has been properly routed. Direct attribute access is the standard pattern used throughout Django admin. A guard is not needed.

@UltraBot05 UltraBot05 force-pushed the fix/readonlyadmin-cascade-delete branch from c567a9a to da00fb6 Compare February 24, 2026 09:54
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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

36-44: ⚠️ Potential issue | 🟡 Minor

Block the change view URL to keep ReadOnlyAdmin truly read-only.

has_delete_permission() doesn’t block the model’s _change URL, so the delete UI can still appear on the change form even though deletion is later denied. If the goal is to keep direct delete actions hidden, include _change in the blocked list.

✅ Suggested fix
         if url_name in (
             f"{opts.app_label}_{opts.model_name}_delete",
             f"{opts.app_label}_{opts.model_name}_changelist",
+            f"{opts.app_label}_{opts.model_name}_change",
         ):
             return False
Django admin url_name for change view and delete button visibility behavior
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_utils/admin.py` around lines 36 - 44, The has_delete_permission
method currently only blocks the delete and changelist URLs, but not the model
change view so the delete button still appears; inside has_delete_permission
(use opts = self.model._meta and url_name variable) add the change-view URL
pattern f"{opts.app_label}_{opts.model_name}_change" to the blocked tuple so the
method returns False for that URL and the delete UI is hidden.

36-44: ⚠️ Potential issue | 🟠 Major

Guard resolver_match before dereferencing url_name.

request.resolver_match can be None in some request contexts (eg. certain tests or non-resolved requests), which will raise AttributeError here. Safer to guard with getattr and treat a missing url_name as “not blocked.”

🐛 Suggested fix
-        url_name = request.resolver_match.url_name
+        url_name = getattr(request.resolver_match, "url_name", None)
Django HttpRequest resolver_match None behavior and when it's populated in admin views
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_utils/admin.py` around lines 36 - 44, The has_delete_permission
method dereferences request.resolver_match.url_name without guarding for
resolver_match possibly being None; change it to first retrieve resolver_match =
getattr(request, "resolver_match", None) and then url_name =
getattr(resolver_match, "url_name", None) (or equivalent) and only perform the
blocked-url_name check when url_name is truthy; otherwise return
super().has_delete_permission(request, obj). This preserves the existing checks
against opts.app_label/opts.model_name while avoiding AttributeError in tests or
unresolved requests.
🤖 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_utils/admin.py`:
- Around line 36-44: The has_delete_permission method currently only blocks the
delete and changelist URLs, but not the model change view so the delete button
still appears; inside has_delete_permission (use opts = self.model._meta and
url_name variable) add the change-view URL pattern
f"{opts.app_label}_{opts.model_name}_change" to the blocked tuple so the method
returns False for that URL and the delete UI is hidden.
- Around line 36-44: The has_delete_permission method dereferences
request.resolver_match.url_name without guarding for resolver_match possibly
being None; change it to first retrieve resolver_match = getattr(request,
"resolver_match", None) and then url_name = getattr(resolver_match, "url_name",
None) (or equivalent) and only perform the blocked-url_name check when url_name
is truthy; otherwise return super().has_delete_permission(request, obj). This
preserves the existing checks against opts.app_label/opts.model_name while
avoiding AttributeError in tests or unresolved requests.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c567a9a and da00fb6.

📒 Files selected for processing (2)
  • openwisp_utils/admin.py
  • tests/test_project/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). (14)
  • 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.0.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.10 | 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~=5.1.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=5.0.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=5.0.0
  • GitHub Check: Python==3.11 | django~=4.2.0
🔇 Additional comments (1)
tests/test_project/tests/test_admin.py (1)

122-141: Good coverage for direct delete vs cascade-delete context.

The subtests clearly distinguish the blocked delete URL from a cascade-like request context.

@UltraBot05 UltraBot05 changed the title [admin] Allow cascade deletions in ReadOnlyAdmin [fix] Allow cascade deletions in ReadOnlyAdmin Feb 24, 2026
@UltraBot05 UltraBot05 force-pushed the fix/readonlyadmin-cascade-delete branch 2 times, most recently from 2c77953 to c9402ce Compare February 24, 2026 10:07
ReadOnlyAdmin.has_delete_permission previously returned False
unconditionally, which blocked cascade deletions from parent
models (like Organization).

Updated the method to check request.resolver_match.url_name.
It now correctly blocks direct deletes on the model's own views
while delegating to the standard Django permission check for
cascade deletes.
@UltraBot05 UltraBot05 force-pushed the fix/readonlyadmin-cascade-delete branch from c9402ce to c640a98 Compare February 24, 2026 10:11
@coveralls
Copy link

coveralls commented Feb 24, 2026

Coverage Status

coverage: 97.253% (+0.004%) from 97.249%
when pulling c640a98 on UltraBot05:fix/readonlyadmin-cascade-delete
into 782c8d9 on openwisp:master.

Copy link
Member

@nemesifier nemesifier left a comment

Choose a reason for hiding this comment

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

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