11from django .contrib import admin
22from django .contrib .admin .sites import AdminSite
3- from django .contrib .auth .models import User
3+ from django .contrib .auth .models import Permission , User
44from django .contrib .contenttypes .admin import GenericTabularInline
55from django .contrib .contenttypes .models import ContentType
66from django .forms .formsets import DEFAULT_MAX_NUM
1515from django .urls import reverse
1616from django .utils .deprecation import RemovedInDjango60Warning
1717
18- from .admin import MediaInline , MediaPermanentInline
18+ from .admin import MediaInline , MediaPermanentInline , PhoneNumberInline
1919from .admin import site as admin_site
20- from .models import Category , Episode , EpisodePermanent , Media , PhoneNumber
20+ from .models import Category , Contact , Episode , EpisodePermanent , Media , PhoneNumber
2121
2222
2323class TestDataMixin :
@@ -304,13 +304,102 @@ def test_delete(self):
304304
305305
306306@override_settings (ROOT_URLCONF = "generic_inline_admin.urls" )
307- class NoInlineDeletionTest (SimpleTestCase ):
308- @ignore_warnings (category = RemovedInDjango60Warning )
309- def test_no_deletion (self ):
310- inline = MediaPermanentInline (EpisodePermanent , admin_site )
311- fake_request = object ()
312- formset = inline .get_formset (fake_request )
313- self .assertFalse (formset .can_delete )
307+ class GenericInlineAdminPermissionsTest (TestCase ):
308+ factory = RequestFactory ()
309+
310+ @classmethod
311+ def setUpTestData (cls ):
312+ cls .user = User (username = "admin" , is_staff = True , is_active = True )
313+ cls .user .set_password ("secret" )
314+ cls .user .save ()
315+
316+ # User always has all permissions on Contact (parent) model.
317+ # Permissions on the inlines vary per test.
318+ cls .contact_type = ContentType .objects .get_for_model (Contact )
319+ cls .user .user_permissions .add (
320+ * Permission .objects .filter (content_type = cls .contact_type )
321+ )
322+
323+ def test_add_inline_without_add_permission (self ):
324+ self .client .force_login (self .user )
325+ inline_view_perm = Permission .objects .get (codename = "view_phonenumber" )
326+ self .user .user_permissions .add (inline_view_perm )
327+
328+ category_id = Category .objects .create (name = "test" ).pk
329+ prefix = "generic_inline_admin-phonenumber-content_type-object_id"
330+ post_data = {
331+ "name" : "Barbara" ,
332+ # inline data
333+ f"{ prefix } -TOTAL_FORMS" : "1" ,
334+ f"{ prefix } -INITIAL_FORMS" : "0" ,
335+ f"{ prefix } -MIN_NUM_FORMS" : "0" ,
336+ f"{ prefix } -MAX_NUM_FORMS" : "0" ,
337+ f"{ prefix } -0-id" : "" ,
338+ f"{ prefix } -0-phone_number" : "555-555-5555" ,
339+ f"{ prefix } -0-category" : str (category_id ),
340+ }
341+ request = self .factory .get (reverse ("admin:generic_inline_admin_contact_add" ))
342+ request .user = self .user
343+ inline = PhoneNumberInline (Contact , AdminSite ())
344+ FormSet = inline .get_formset (request )
345+ formset = FormSet (
346+ data = post_data , prefix = prefix , instance = Contact (name = "Barbara" )
347+ )
348+
349+ self .assertIs (formset .is_valid (), True )
350+ self .assertIs (formset .has_changed (), False )
351+ self .assertEqual (formset .save (commit = False ), [])
352+
353+ def test_add_inline_with_change_permission_only (self ):
354+ """
355+ Forged new inline instances are ignored without add permissions, but
356+ but edits still work with edit permissions.
357+ """
358+ self .client .force_login (self .user )
359+ inline_perms = Permission .objects .filter (
360+ codename__in = ("view_phonenumber" , "change_phonenumber" )
361+ )
362+ self .user .user_permissions .add (* inline_perms )
363+
364+ category_id = Category .objects .create (name = "test" ).pk
365+ contact = Contact .objects .create (name = "Barbara" )
366+ existing_number = PhoneNumber .objects .create (
367+ category_id = category_id ,
368+ content_type = self .contact_type ,
369+ object_id = contact .pk ,
370+ phone_number = "555-555-5555" ,
371+ )
372+ prefix = "generic_inline_admin-phonenumber-content_type-object_id"
373+ post_data = {
374+ "id" : str (contact .pk ),
375+ "name" : "Barbara" ,
376+ # inline data
377+ f"{ prefix } -TOTAL_FORMS" : "2" ,
378+ f"{ prefix } -INITIAL_FORMS" : "1" ,
379+ f"{ prefix } -MIN_NUM_FORMS" : "0" ,
380+ f"{ prefix } -MAX_NUM_FORMS" : "0" ,
381+ # Attempt to edit the existing phone number value.
382+ f"{ prefix } -0-id" : str (existing_number .id ),
383+ f"{ prefix } -0-phone_number" : "111-111-1111" ,
384+ f"{ prefix } -0-category" : str (category_id ),
385+ # Attempt to forge a new phone number.
386+ f"{ prefix } -1-id" : "" ,
387+ f"{ prefix } -1-phone_number" : "666-666-6666" ,
388+ f"{ prefix } -1-category" : str (category_id ),
389+ "_save" : "Save" ,
390+ }
391+ request = self .factory .get (
392+ reverse ("admin:generic_inline_admin_contact_change" , args = [contact .pk ])
393+ )
394+ request .user = self .user
395+ inline = PhoneNumberInline (Contact , AdminSite ())
396+ FormSet = inline .get_formset (request )
397+ formset = FormSet (data = post_data , prefix = prefix , instance = contact )
398+
399+ self .assertIs (formset .is_valid (), True )
400+ self .assertIs (formset .has_changed (), True )
401+ # The edit succeeds; the add is ignored.
402+ self .assertEqual (formset .save (commit = False ), [existing_number ])
314403
315404
316405class MockRequest :
@@ -326,6 +415,15 @@ def has_perm(self, perm, obj=None):
326415request .user = MockSuperUser ()
327416
328417
418+ @override_settings (ROOT_URLCONF = "generic_inline_admin.urls" )
419+ class NoInlineDeletionTest (SimpleTestCase ):
420+ @ignore_warnings (category = RemovedInDjango60Warning )
421+ def test_no_deletion (self ):
422+ inline = MediaPermanentInline (EpisodePermanent , admin_site )
423+ formset = inline .get_formset (request )
424+ self .assertFalse (formset .can_delete )
425+
426+
329427@override_settings (ROOT_URLCONF = "generic_inline_admin.urls" )
330428class GenericInlineModelAdminTest (SimpleTestCase ):
331429 def setUp (self ):
0 commit comments