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
77from django .forms .models import ModelForm
88from django .test import RequestFactory , SimpleTestCase , TestCase , override_settings
99from django .urls import reverse
1010
11- from .admin import MediaInline , MediaPermanentInline
11+ from .admin import MediaInline , MediaPermanentInline , PhoneNumberInline
1212from .admin import site as admin_site
13- from .models import Category , Episode , EpisodePermanent , Media , PhoneNumber
13+ from .models import Category , Contact , Episode , EpisodePermanent , Media , PhoneNumber
1414
1515
1616class TestDataMixin :
@@ -295,12 +295,102 @@ def test_delete(self):
295295
296296
297297@override_settings (ROOT_URLCONF = "generic_inline_admin.urls" )
298- class NoInlineDeletionTest (SimpleTestCase ):
299- def test_no_deletion (self ):
300- inline = MediaPermanentInline (EpisodePermanent , admin_site )
301- fake_request = object ()
302- formset = inline .get_formset (fake_request )
303- self .assertFalse (formset .can_delete )
298+ class GenericInlineAdminPermissionsTest (TestCase ):
299+ factory = RequestFactory ()
300+
301+ @classmethod
302+ def setUpTestData (cls ):
303+ cls .user = User (username = "admin" , is_staff = True , is_active = True )
304+ cls .user .set_password ("secret" )
305+ cls .user .save ()
306+
307+ # User always has all permissions on Contact (parent) model.
308+ # Permissions on the inlines vary per test.
309+ cls .contact_type = ContentType .objects .get_for_model (Contact )
310+ cls .user .user_permissions .add (
311+ * Permission .objects .filter (content_type = cls .contact_type )
312+ )
313+
314+ def test_add_inline_without_add_permission (self ):
315+ self .client .force_login (self .user )
316+ inline_view_perm = Permission .objects .get (codename = "view_phonenumber" )
317+ self .user .user_permissions .add (inline_view_perm )
318+
319+ category_id = Category .objects .create (name = "test" ).pk
320+ prefix = "generic_inline_admin-phonenumber-content_type-object_id"
321+ post_data = {
322+ "name" : "Barbara" ,
323+ # inline data
324+ f"{ prefix } -TOTAL_FORMS" : "1" ,
325+ f"{ prefix } -INITIAL_FORMS" : "0" ,
326+ f"{ prefix } -MIN_NUM_FORMS" : "0" ,
327+ f"{ prefix } -MAX_NUM_FORMS" : "0" ,
328+ f"{ prefix } -0-id" : "" ,
329+ f"{ prefix } -0-phone_number" : "555-555-5555" ,
330+ f"{ prefix } -0-category" : str (category_id ),
331+ }
332+ request = self .factory .get (reverse ("admin:generic_inline_admin_contact_add" ))
333+ request .user = self .user
334+ inline = PhoneNumberInline (Contact , AdminSite ())
335+ FormSet = inline .get_formset (request )
336+ formset = FormSet (
337+ data = post_data , prefix = prefix , instance = Contact (name = "Barbara" )
338+ )
339+
340+ self .assertIs (formset .is_valid (), True )
341+ self .assertIs (formset .has_changed (), False )
342+ self .assertEqual (formset .save (commit = False ), [])
343+
344+ def test_add_inline_with_change_permission_only (self ):
345+ """
346+ Forged new inline instances are ignored without add permissions, but
347+ but edits still work with edit permissions.
348+ """
349+ self .client .force_login (self .user )
350+ inline_perms = Permission .objects .filter (
351+ codename__in = ("view_phonenumber" , "change_phonenumber" )
352+ )
353+ self .user .user_permissions .add (* inline_perms )
354+
355+ category_id = Category .objects .create (name = "test" ).pk
356+ contact = Contact .objects .create (name = "Barbara" )
357+ existing_number = PhoneNumber .objects .create (
358+ category_id = category_id ,
359+ content_type = self .contact_type ,
360+ object_id = contact .pk ,
361+ phone_number = "555-555-5555" ,
362+ )
363+ prefix = "generic_inline_admin-phonenumber-content_type-object_id"
364+ post_data = {
365+ "id" : str (contact .pk ),
366+ "name" : "Barbara" ,
367+ # inline data
368+ f"{ prefix } -TOTAL_FORMS" : "2" ,
369+ f"{ prefix } -INITIAL_FORMS" : "1" ,
370+ f"{ prefix } -MIN_NUM_FORMS" : "0" ,
371+ f"{ prefix } -MAX_NUM_FORMS" : "0" ,
372+ # Attempt to edit the existing phone number value.
373+ f"{ prefix } -0-id" : str (existing_number .id ),
374+ f"{ prefix } -0-phone_number" : "111-111-1111" ,
375+ f"{ prefix } -0-category" : str (category_id ),
376+ # Attempt to forge a new phone number.
377+ f"{ prefix } -1-id" : "" ,
378+ f"{ prefix } -1-phone_number" : "666-666-6666" ,
379+ f"{ prefix } -1-category" : str (category_id ),
380+ "_save" : "Save" ,
381+ }
382+ request = self .factory .get (
383+ reverse ("admin:generic_inline_admin_contact_change" , args = [contact .pk ])
384+ )
385+ request .user = self .user
386+ inline = PhoneNumberInline (Contact , AdminSite ())
387+ FormSet = inline .get_formset (request )
388+ formset = FormSet (data = post_data , prefix = prefix , instance = contact )
389+
390+ self .assertIs (formset .is_valid (), True )
391+ self .assertIs (formset .has_changed (), True )
392+ # The edit succeeds; the add is ignored.
393+ self .assertEqual (formset .save (commit = False ), [existing_number ])
304394
305395
306396class MockRequest :
@@ -316,6 +406,14 @@ def has_perm(self, perm, obj=None):
316406request .user = MockSuperUser ()
317407
318408
409+ @override_settings (ROOT_URLCONF = "generic_inline_admin.urls" )
410+ class NoInlineDeletionTest (SimpleTestCase ):
411+ def test_no_deletion (self ):
412+ inline = MediaPermanentInline (EpisodePermanent , admin_site )
413+ formset = inline .get_formset (request )
414+ self .assertFalse (formset .can_delete )
415+
416+
319417@override_settings (ROOT_URLCONF = "generic_inline_admin.urls" )
320418class GenericInlineModelAdminTest (SimpleTestCase ):
321419 def setUp (self ):
0 commit comments