Skip to content

[fix] Allow cascade deletions in ReadOnlyAdmin#596

Open
UltraBot05 wants to merge 5 commits intoopenwisp:masterfrom
UltraBot05:fix/readonlyadmin-cascade-delete
Open

[fix] Allow cascade deletions in ReadOnlyAdmin#596
UltraBot05 wants to merge 5 commits 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 blocked all deletions, preventing parent models (like Organization) from performing cascade deletes.

Key changes:

  1. URL Filtering: Uses request.resolver_match.url_name to block deletions only on the model's own delete, change, and changelist views.

  2. Safety: Added getattr guards for resolver_match to prevent crashes in contexts where URL resolution hasn't occurred.

  3. Fallback: Delegates to super().has_delete_permission for all other contexts (like cascade deletes) to maintain standard Django permission checks.

  4. Tests: Added coverage for direct admin routes, simulated cascade deletes, None resolver cases, and negative cases for staff users without delete permissions.

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

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

ReadOnlyAdmin.has_delete_permission now returns False only when the request's resolver_match.url_name equals the model-specific "delete", the model-specific "change", or the "changelist" view; in all other request contexts it delegates to super().has_delete_permission(request, obj). Tests were added covering changelist and change URLs (expect False), a non-admin "index" resolver (expect True), no resolver_match (expect True), and a cascade-delete-like fallback where super() denies deletion (expect False).

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The linked issue #256 concerns commit message formatting with trailing punctuation causing CI failures, which is unrelated to the cascade deletion fix in this PR. Verify the correct issue number is linked. The PR fixes cascade deletions in ReadOnlyAdmin, not commit message formatting. Check if issue #256 is the intended reference.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: allowing cascade deletions in ReadOnlyAdmin while maintaining read-only constraints.
Out of Scope Changes check ✅ Passed The code changes are focused and within scope: ReadOnlyAdmin.has_delete_permission logic modification and corresponding test cases match the cascade deletion objective.
Description check ✅ Passed The PR description follows the required template with all key sections completed: checklist items marked, issue reference provided, clear description of changes, and 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
📝 Coding Plan
  • Generate coding plan for human review comments

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.

Tip

Migrating from UI to YAML configuration.

Use the @coderabbitai configuration command in a PR comment to get a dump of all your UI settings in YAML format. You can then edit this YAML file and upload it to the root of your repository to configure CodeRabbit programmatically.

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.351% (+0.003%) from 97.348%
when pulling aeeec0b on UltraBot05:fix/readonlyadmin-cascade-delete
into e1d24be 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.

@kilo-code-bot
Copy link

kilo-code-bot bot commented Mar 8, 2026

Code Review Summary

Status: No Issues Found | Recommendation: Merge

This PR effectively resolves issue #256 by modifying ReadOnlyAdmin.has_delete_permission() to allow cascade deletions from parent models while still preventing direct deletion via admin views.

Implementation Review

The fix correctly:

  • Blocks delete permission for the model's own admin URLs (*_delete, *_change, *_changelist)
  • Falls back to super().has_delete_permission() for cascade deletes from parent models
  • Uses safe attribute access with getattr() guards to handle cases where resolver_match may be None

Test Coverage

Comprehensive tests were added covering all scenarios:

  • Direct admin URLs (changelist, change, delete) → returns False
  • Cascade delete from unrelated URL → returns True (when user has permission)
  • Missing resolver_match → returns True
  • Cascade delete without child permission → returns False

Files Reviewed (2 files)

  • openwisp_utils/admin.py - Core fix implementation
  • tests/test_project/tests/test_admin.py - Complete test coverage

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In `@tests/test_project/tests/test_admin.py`:
- Around line 138-154: The tests only cover happy-path fallbacks and miss
verifying behavior when a staff user lacks the child-model delete permission;
add a subTest that creates a staff user without the
"test_project.delete_radiusaccounting" permission, sets request.resolver_match
to simulate a cascade delete (e.g., mock_resolver.url_name = "index"), and
assert that modeladmin.has_delete_permission(request) delegates to
super().has_delete_permission by returning False for that user; reference the
existing modeladmin.has_delete_permission check and the permission string
"test_project.delete_radiusaccounting" to locate where to add this negative-case
test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 679d8b50-3b0b-447f-baed-4f1c9411beff

📥 Commits

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

📒 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.12 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=5.0.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.10 | 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~=4.2.0
  • GitHub Check: Python==3.13 | django~=5.1.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.0.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=5.0.0
🔇 Additional comments (1)
openwisp_utils/admin.py (1)

36-46: LGTM: direct admin deletes stay blocked while cascade checks fall back to Django permissions.

The resolver_match guard closes the old crash path, and the targeted URL-name block keeps the model’s own delete/change/changelist views read-only without breaking parent-driven cascade deletes.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
tests/test_project/tests/test_admin.py (1)

156-173: 🧹 Nitpick | 🔵 Trivial

Use a real staff user in the negative cascade test.

This mock only proves that some request.user.has_perm(...) call returned False; it does not lock in Django's actual ModelAdmin.has_delete_permission(...) behavior for a staff user without test_project.delete_radiusaccounting.

Proposed test hardening
         with self.subTest("cascade delete without child permission returns False"):
-            request = self.client.get(
-                reverse("admin:test_project_radiusaccounting_changelist")
-            ).wsgi_request
-            
+            user = User.objects.create(
+                username="readonly-staff",
+                password="pass",
+                is_staff=True,
+                is_superuser=False,
+            )
+            self.client.force_login(user)
+            request = self.client.get(reverse("admin:index")).wsgi_request
+
             mock_resolver = MagicMock()
             mock_resolver.url_name = "index"
             request.resolver_match = mock_resolver
-            
-            # Mock a staff user who lacks the specific delete permission
-            request.user = MagicMock()
-            request.user.is_active = True
-            request.user.is_staff = True
-            request.user.is_superuser = False
-            request.user.has_perm.return_value = False  # Explicitly deny perm
-            
-            # It should fall back to super().has_delete_permission and return False
+
             self.assertFalse(modeladmin.has_delete_permission(request))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_project/tests/test_admin.py` around lines 156 - 173, The negative
cascade test uses a MagicMock user which doesn't exercise Django's real
permission checks; replace the mocked request.user with a real Django User
instance (import User from django.contrib.auth.models), create/save a user with
is_active=True, is_staff=True, is_superuser=False and do NOT assign the
'test_project.delete_radiusaccounting' permission, then attach that user to
request (request.user = user or use self.client.force_login(user)) and assert
modeladmin.has_delete_permission(request) is False so the test verifies
ModelAdmin.has_delete_permission behavior for a real staff user.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/test_project/tests/test_admin.py`:
- Around line 125-154: Add a subtest that asserts
modeladmin.has_delete_permission returns False when the request is for the
direct admin delete route: create an object via _create_radius_accounting, build
a request using reverse("admin:test_project_radiusaccounting_delete",
args=[obj.pk]).wsgi_request, and
assertFalse(modeladmin.has_delete_permission(request)); this ensures the guarded
direct-admin URL name pattern for the "_delete" route is covered alongside the
existing "_change" and "_changelist" checks.

---

Duplicate comments:
In `@tests/test_project/tests/test_admin.py`:
- Around line 156-173: The negative cascade test uses a MagicMock user which
doesn't exercise Django's real permission checks; replace the mocked
request.user with a real Django User instance (import User from
django.contrib.auth.models), create/save a user with is_active=True,
is_staff=True, is_superuser=False and do NOT assign the
'test_project.delete_radiusaccounting' permission, then attach that user to
request (request.user = user or use self.client.force_login(user)) and assert
modeladmin.has_delete_permission(request) is False so the test verifies
ModelAdmin.has_delete_permission behavior for a real staff user.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: abbed59d-9171-4cc3-b23e-631b1f672e66

📥 Commits

Reviewing files that changed from the base of the PR and between 5242e19 and 979e5b6.

📒 Files selected for processing (1)
  • 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.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~=4.2.0
  • GitHub Check: Python==3.11 | django~=5.0.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.11 | 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.10 | django~=5.0.0
  • GitHub Check: Python==3.12 | django~=5.0.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.11 | django~=4.2.0

@openwisp-companion
Copy link

Multiple CI Failures Detected

Hello @UltraBot05,
(Analysis for commit 979e5b6)

1. Code Style/QA Failures

  • Explanation: The CI pipeline failed due to code style violations detected by black and flake8. Specifically, flake8 reported "W293 blank line contains whitespace" in tests/test_project/tests/test_admin.py on lines 155, 160, 164, and 171. black also indicated formatting issues.
  • Remediation: Run the openwisp-qa-format command in your local environment. This command will automatically fix the detected whitespace and formatting issues in the specified files.

2. Commit Message Failure

  • Explanation: The commit message validation failed because it did not adhere to the OpenWISP commit message conventions. The provided commit message is missing the required structure, including a capitalized title, a blank line after the title, and a footer with a closing keyword and issue number (e.g., Fixes #<issue>).

  • Remediation: Reformat the commit message to follow the expected structure. Here's a template:

    [prefix] Capitalized short title #<issue_number>
    
    Detailed description of the changes made.
    This section should explain the what and why of the commit.
    
    Fixes #<issue_number>
    

    For this specific commit, a corrected message might look like:

    [test] Added negative case for cascade delete permission #<issue_number>
    
    Added a test case to cover the scenario where cascade delete permission is not granted.
    
    Fixes #<issue_number>
    

    Replace <issue_number> with the actual issue number.

@UltraBot05 UltraBot05 force-pushed the fix/readonlyadmin-cascade-delete branch from 979e5b6 to a6dd1fd Compare March 9, 2026 04:44
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 (1)
tests/test_project/tests/test_admin.py (1)

125-136: 🧹 Nitpick | 🔵 Trivial

Add test coverage for the direct _delete admin route.

The implementation blocks three URL names (*_delete, *_change, *_changelist), but only *_change and *_changelist are tested. If *_delete is accidentally removed from the blocked set, this test suite would still pass.

Suggested test addition
         with self.subTest("change URL returns False"):
             obj = self._create_radius_accounting(username="test", session_id="1")
             request = self.client.get(
                 reverse("admin:test_project_radiusaccounting_change", args=[obj.pk])
             ).wsgi_request
             self.assertFalse(modeladmin.has_delete_permission(request))
+
+        with self.subTest("delete URL returns False"):
+            obj = self._create_radius_accounting(username="delete-test", session_id="2")
+            request = self.client.get(
+                reverse("admin:test_project_radiusaccounting_delete", args=[obj.pk])
+            ).wsgi_request
+            self.assertFalse(modeladmin.has_delete_permission(request))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_project/tests/test_admin.py` around lines 125 - 136, Add a subTest
that asserts the admin direct delete URL is blocked: create a RadiusAccounting
instance via _create_radius_accounting, build a request using
reverse("admin:test_project_radiusaccounting_delete", args=[obj.pk]) and take
.wsgi_request, then assertFalse(modeladmin.has_delete_permission(request)); this
mirrors the existing change and changelist checks and ensures the "*_delete"
route is covered.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@tests/test_project/tests/test_admin.py`:
- Around line 125-136: Add a subTest that asserts the admin direct delete URL is
blocked: create a RadiusAccounting instance via _create_radius_accounting, build
a request using reverse("admin:test_project_radiusaccounting_delete",
args=[obj.pk]) and take .wsgi_request, then
assertFalse(modeladmin.has_delete_permission(request)); this mirrors the
existing change and changelist checks and ensures the "*_delete" route is
covered.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9f316520-5c48-48fa-af9f-f8db7889f8d8

📥 Commits

Reviewing files that changed from the base of the PR and between 979e5b6 and a6dd1fd.

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

122-129: LGTM!

Test setup and changelist URL subtest are correct. The assertion properly validates that direct access via the changelist view is blocked.


131-136: LGTM!

Change URL subtest correctly validates that the *_change route is blocked.


138-147: LGTM!

Good simulation of cascade delete context by mocking resolver_match.url_name to a value outside the blocked set. The test correctly validates that such requests delegate to super().has_delete_permission().


149-154: LGTM!

This subtest properly covers the edge case where resolver_match is None, ensuring the getattr guards in the implementation work correctly.


156-173: LGTM!

This subtest validates the important case where the fallback to super().has_delete_permission() correctly denies access for users without the delete permission. Good use of mocking to isolate the permission check behavior.

@openwisp-companion
Copy link

Black and Flake8 Failures

Hello @UltraBot05,
(Analysis for commit a6dd1fd)

The CI pipeline failed due to code style violations detected by Black and Flake8.

Failures & Remediation:

  1. Black Check Failed: The Black code formatter found style issues.

    • Explanation: Black enforces a consistent code style, and it detected deviations from its standards in the codebase. The hint "did you forget to run openwisp-qa-format?" suggests that the automated formatting script was not run.
    • Remediation: Run the openwisp-qa-format script to automatically fix the Black formatting issues. This script is designed to apply Black and other linters to the project.
  2. Flake8 Check Failed: The Flake8 linter found style issues.

    • Explanation: Flake8 checks for PEP 8 compliance and other code quality issues. The specific errors W293 blank line contains whitespace indicate that there are lines in the file that contain only whitespace, which is not allowed by Flake8.
    • Remediation: Run the openwisp-qa-format script. This script will also address the Flake8 violations by removing the whitespace from blank lines.

Specifically, the errors point to blank lines with whitespace in tests/test_project/tests/test_admin.py at lines 155, 160, 164, and 171.

To fix these issues, please run the following command in your project's root directory:

openwisp-qa-format

@UltraBot05 UltraBot05 force-pushed the fix/readonlyadmin-cascade-delete branch from a6dd1fd to 7005ab9 Compare March 9, 2026 04:50
Added a subTest to verify that cascade deletes still respect child-model permissions.

Fixes openwisp#256
@UltraBot05
Copy link
Author

Hey @nemesifier , just a quick ping on this one as well. I just updated the companion PR #391 over in openwisp-firmware-upgrader to handle the specific UpgradeOperation blockers for #256.

This PR fixes the generic ReadOnlyAdmin side, and I've also implemented the final test coverage nitpicks requested by the bots. Together with #391, they fully resolve the organization deletion bug. Both are completely green and ready for review whenever you have a moment!

@nemesifier
Copy link
Member

Hey @nemesifier , just a quick ping on this one as well. I just updated the companion PR #391 over in openwisp-firmware-upgrader to handle the specific UpgradeOperation blockers for #256.

This PR fixes the generic ReadOnlyAdmin side, and I've also implemented the final test coverage nitpicks requested by the bots. Together with #391, they fully resolve the organization deletion bug. Both are completely green and ready for review whenever you have a moment!

Looks good, I will test asap. I am a bit flooded with PRs now but will do my best.

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