Skip to content

Commit da00fb6

Browse files
committed
[admin] Allow cascade deletions in ReadOnlyAdmin
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.
1 parent 782c8d9 commit da00fb6

File tree

2 files changed

+29
-1
lines changed

2 files changed

+29
-1
lines changed

openwisp_utils/admin.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,14 @@ def has_add_permission(self, request):
3434
return False
3535

3636
def has_delete_permission(self, request, obj=None):
37-
return False
37+
opts = self.model._meta
38+
url_name = request.resolver_match.url_name
39+
if url_name in (
40+
f"{opts.app_label}_{opts.model_name}_delete",
41+
f"{opts.app_label}_{opts.model_name}_changelist",
42+
):
43+
return False
44+
return super().has_delete_permission(request, obj)
3845

3946
def save_model(self, request, obj, form, change): # pragma: nocover
4047
pass

tests/test_project/tests/test_admin.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,27 @@ class TestReadOnlyAdmin(ReadOnlyAdmin):
119119
["id", "session_id", "username", "start_time", "stop_time"],
120120
)
121121

122+
def test_readonlyadmin_has_delete_permission(self):
123+
modeladmin = ReadOnlyAdmin(RadiusAccounting, AdminSite())
124+
125+
with self.subTest("direct delete URL returns False"):
126+
request = self.client.get(
127+
reverse("admin:test_project_radiusaccounting_changelist")
128+
).wsgi_request
129+
self.assertFalse(modeladmin.has_delete_permission(request))
130+
131+
with self.subTest("cascade delete from unrelated URL returns True"):
132+
# Reuse the authenticated request but simulate being called from
133+
# a parent model's delete confirmation (cascade), not from the
134+
# model's own delete/changelist URL.
135+
request = self.client.get(
136+
reverse("admin:test_project_radiusaccounting_changelist")
137+
).wsgi_request
138+
mock_resolver = MagicMock()
139+
mock_resolver.url_name = "index"
140+
request.resolver_match = mock_resolver
141+
self.assertTrue(modeladmin.has_delete_permission(request))
142+
122143
def test_context_processor(self):
123144
url = reverse("admin:index")
124145
response = self.client.get(url)

0 commit comments

Comments
 (0)