11from unittest .mock import MagicMock , patch
22
3+ from django .contrib import admin
34from django .contrib .admin .sites import AdminSite
4- from django .contrib .auth .models import Permission , User
5+ from django .contrib .auth import get_user_model
6+ from django .contrib .auth .models import Permission
57from django .core .exceptions import ImproperlyConfigured
68from django .test import TestCase
79from django .urls import reverse
2123)
2224from . import AdminTestMixin , CreateMixin
2325
26+ User = get_user_model ()
27+
2428
2529class TestAdmin (AdminTestMixin , CreateMixin , TestCase ):
2630 TEST_KEY = "w1gwJxKaHcamUw62TQIPgYchwLKn3AA0"
@@ -118,6 +122,8 @@ class TestReadOnlyAdmin(ReadOnlyAdmin):
118122
119123 def test_readonlyadmin_has_delete_permission (self ):
120124 modeladmin = ReadOnlyAdmin (RadiusAccounting , AdminSite ())
125+ # The Django test client keeps the resolved request on the response;
126+ # these assertions call the admin permission method directly.
121127
122128 with self .subTest ("changelist URL returns False" ):
123129 request = self .client .get (
@@ -139,23 +145,32 @@ def test_readonlyadmin_has_delete_permission(self):
139145 ).wsgi_request
140146 self .assertFalse (modeladmin .has_delete_permission (request ))
141147
142- with self .subTest ("cascade delete from unrelated URL returns True" ):
143- # Simulate being called from a parent model's delete
144- # confirmation (cascade), not from the model's own views.
148+ with self .subTest ("cascade delete from parent delete URL returns True" ):
145149 request = self .client .get (
146150 reverse ("admin:test_project_radiusaccounting_changelist" )
147151 ).wsgi_request
148152 mock_resolver = MagicMock ()
149- mock_resolver .url_name = "index "
153+ mock_resolver .url_name = "test_project_project_delete "
150154 request .resolver_match = mock_resolver
151155 self .assertTrue (modeladmin .has_delete_permission (request ))
152156
153- with self .subTest ("no resolver_match returns True" ):
157+ with self .subTest ("parent bulk delete returns True" ):
158+ request = self .client .post (
159+ reverse ("admin:test_project_project_changelist" ),
160+ data = {"action" : "delete_selected" },
161+ ).wsgi_request
162+ self .assertTrue (modeladmin .has_delete_permission (request ))
163+
164+ with self .subTest ("unrelated admin URL returns False" ):
165+ request = self .client .get (reverse ("admin:index" )).wsgi_request
166+ self .assertFalse (modeladmin .has_delete_permission (request ))
167+
168+ with self .subTest ("no resolver_match returns False" ):
154169 request = self .client .get (
155170 reverse ("admin:test_project_radiusaccounting_changelist" )
156171 ).wsgi_request
157172 request .resolver_match = None
158- self .assertTrue (modeladmin .has_delete_permission (request ))
173+ self .assertFalse (modeladmin .has_delete_permission (request ))
159174
160175 with self .subTest ("cascade delete without child permission returns False" ):
161176 user = User .objects .create (
@@ -166,12 +181,34 @@ def test_readonlyadmin_has_delete_permission(self):
166181 )
167182 self .client .force_login (user )
168183 request = self .client .get (reverse ("admin:index" )).wsgi_request
169-
170184 mock_resolver = MagicMock ()
171- mock_resolver .url_name = "index "
185+ mock_resolver .url_name = "test_project_project_delete "
172186 request .resolver_match = mock_resolver
173187 self .assertFalse (modeladmin .has_delete_permission (request ))
174188
189+ def test_readonlyadmin_allows_parent_cascade_delete (self ):
190+ original_admin = admin .site ._registry [Operator ].__class__
191+ admin .site .unregister (Operator )
192+ admin .site .register (Operator , ReadOnlyAdmin )
193+ try :
194+ project = Project .objects .create (name = "test-parent-delete" )
195+ operator = Operator .objects .create (
196+ first_name = "Jane" , last_name = "Doe" , project = project
197+ )
198+ path = reverse ("admin:test_project_project_delete" , args = [project .pk ])
199+ response = self .client .get (path )
200+ self .assertEqual (response .status_code , 200 )
201+ self .assertNotContains (
202+ response , "your account doesn't have permission to delete"
203+ )
204+ response = self .client .post (path , data = {"post" : "yes" }, follow = True )
205+ self .assertEqual (response .status_code , 200 )
206+ self .assertFalse (Project .objects .filter (pk = project .pk ).exists ())
207+ self .assertFalse (Operator .objects .filter (pk = operator .pk ).exists ())
208+ finally :
209+ admin .site .unregister (Operator )
210+ admin .site .register (Operator , original_admin )
211+
175212 def test_context_processor (self ):
176213 url = reverse ("admin:index" )
177214 response = self .client .get (url )
0 commit comments