from django.contrib.admin import ModelAdmin, TabularInline from django.contrib.admin.helpers import InlineAdminForm from django.contrib.admin.tests import AdminSeleniumTestCase from django.contrib.auth.models import Permission, User from django.contrib.contenttypes.models import ContentType from django.test import RequestFactory, TestCase, override_settings from django.urls import reverse from .admin import InnerInline from .admin import site as admin_site from .models import ( Author, BinaryTree, Book, BothVerboseNameProfile, Chapter, Child, ChildModel1, ChildModel2, Fashionista, FootNote, Holder, Holder2, Holder3, Holder4, Inner, Inner2, Inner3, Inner4Stacked, Inner4Tabular, Novel, OutfitItem, Parent, ParentModelWithCustomPk, Person, Poll, Profile, ProfileCollection, Question, ShowInlineParent, Sighting, SomeChildModel, SomeParentModel, Teacher, VerboseNamePluralProfile, VerboseNameProfile, ) INLINE_CHANGELINK_HTML = 'class="inlinechangelink">Change' class TestDataMixin: @classmethod def setUpTestData(cls): cls.superuser = User.objects.create_superuser( username="super", email="super@example.com", password="secret" ) @override_settings(ROOT_URLCONF="admin_inlines.urls") class TestInline(TestDataMixin, TestCase): factory = RequestFactory() @classmethod def setUpTestData(cls): super().setUpTestData() cls.holder = Holder.objects.create(dummy=13) Inner.objects.create(dummy=42, holder=cls.holder) cls.parent = SomeParentModel.objects.create(name="a") SomeChildModel.objects.create(name="b", position="0", parent=cls.parent) SomeChildModel.objects.create(name="c", position="1", parent=cls.parent) cls.view_only_user = User.objects.create_user( username="user", password="pwd", is_staff=True, ) parent_ct = ContentType.objects.get_for_model(SomeParentModel) child_ct = ContentType.objects.get_for_model(SomeChildModel) permission = Permission.objects.get( codename="view_someparentmodel", content_type=parent_ct, ) cls.view_only_user.user_permissions.add(permission) permission = Permission.objects.get( codename="view_somechildmodel", content_type=child_ct, ) cls.view_only_user.user_permissions.add(permission) def setUp(self): self.client.force_login(self.superuser) def test_can_delete(self): """ can_delete should be passed to inlineformset factory. """ response = self.client.get( reverse("admin:admin_inlines_holder_change", args=(self.holder.id,)) ) inner_formset = response.context["inline_admin_formsets"][0].formset expected = InnerInline.can_delete actual = inner_formset.can_delete self.assertEqual(expected, actual, "can_delete must be equal") def test_readonly_stacked_inline_label(self): """Bug #13174.""" holder = Holder.objects.create(dummy=42) Inner.objects.create(holder=holder, dummy=42, readonly="") response = self.client.get( reverse("admin:admin_inlines_holder_change", args=(holder.id,)) ) self.assertContains(response, "") def test_many_to_many_inlines(self): "Autogenerated many-to-many inlines are displayed correctly (#13407)" response = self.client.get(reverse("admin:admin_inlines_author_add")) # The heading for the m2m inline block uses the right text self.assertContains(response, "

Author-book relationships

") # The "add another" label is correct self.assertContains(response, "Add another Author-book relationship") # The '+' is dropped from the autogenerated form prefix (Author_books+) self.assertContains(response, 'id="id_Author_books-TOTAL_FORMS"') def test_inline_primary(self): person = Person.objects.create(firstname="Imelda") item = OutfitItem.objects.create(name="Shoes") # Imelda likes shoes, but can't carry her own bags. data = { "shoppingweakness_set-TOTAL_FORMS": 1, "shoppingweakness_set-INITIAL_FORMS": 0, "shoppingweakness_set-MAX_NUM_FORMS": 0, "_save": "Save", "person": person.id, "max_weight": 0, "shoppingweakness_set-0-item": item.id, } response = self.client.post( reverse("admin:admin_inlines_fashionista_add"), data ) self.assertEqual(response.status_code, 302) self.assertEqual(len(Fashionista.objects.filter(person__firstname="Imelda")), 1) def test_tabular_inline_column_css_class(self): """ Field names are included in the context to output a field-specific CSS class name in the column headers. """ response = self.client.get(reverse("admin:admin_inlines_poll_add")) text_field, call_me_field = list( response.context["inline_admin_formset"].fields() ) # Editable field. self.assertEqual(text_field["name"], "text") self.assertContains(response, '') # Read-only field. self.assertEqual(call_me_field["name"], "call_me") self.assertContains(response, '') def test_custom_form_tabular_inline_label(self): """ A model form with a form field specified (TitleForm.title1) should have its label rendered in the tabular inline. """ response = self.client.get(reverse("admin:admin_inlines_titlecollection_add")) self.assertContains( response, 'Title1', html=True ) def test_custom_form_tabular_inline_extra_field_label(self): response = self.client.get(reverse("admin:admin_inlines_outfititem_add")) _, extra_field = list(response.context["inline_admin_formset"].fields()) self.assertEqual(extra_field["label"], "Extra field") def test_non_editable_custom_form_tabular_inline_extra_field_label(self): response = self.client.get(reverse("admin:admin_inlines_chapter_add")) _, extra_field = list(response.context["inline_admin_formset"].fields()) self.assertEqual(extra_field["label"], "Extra field") def test_custom_form_tabular_inline_overridden_label(self): """ SomeChildModelForm.__init__() overrides the label of a form field. That label is displayed in the TabularInline. """ response = self.client.get(reverse("admin:admin_inlines_someparentmodel_add")) field = list(response.context["inline_admin_formset"].fields())[0] self.assertEqual(field["label"], "new label") self.assertContains( response, 'New label', html=True ) def test_tabular_non_field_errors(self): """ non_field_errors are displayed correctly, including the correct value for colspan. """ data = { "title_set-TOTAL_FORMS": 1, "title_set-INITIAL_FORMS": 0, "title_set-MAX_NUM_FORMS": 0, "_save": "Save", "title_set-0-title1": "a title", "title_set-0-title2": "a different title", } response = self.client.post( reverse("admin:admin_inlines_titlecollection_add"), data ) # Here colspan is "4": two fields (title1 and title2), one hidden field # and the delete checkbox. self.assertContains( response, '' '", ) def test_no_parent_callable_lookup(self): """Admin inline `readonly_field` shouldn't invoke parent ModelAdmin callable""" # Identically named callable isn't present in the parent ModelAdmin, # rendering of the add view shouldn't explode response = self.client.get(reverse("admin:admin_inlines_novel_add")) # View should have the child inlines section self.assertContains( response, '
Callable in QuestionInline

") def test_help_text(self): """ The inlines' model field help texts are displayed when using both the stacked and tabular layouts. """ response = self.client.get(reverse("admin:admin_inlines_holder4_add")) self.assertContains( response, '
Awesome stacked help text is awesome.
', 4 ) self.assertContains( response, '', 1, ) # ReadOnly fields response = self.client.get(reverse("admin:admin_inlines_capofamiglia_add")) self.assertContains( response, '', 1, ) def test_tabular_model_form_meta_readonly_field(self): """ Tabular inlines use ModelForm.Meta.help_texts and labels for read-only fields. """ response = self.client.get(reverse("admin:admin_inlines_someparentmodel_add")) self.assertContains( response, '', ) self.assertContains(response, "Label from ModelForm.Meta") def test_inline_hidden_field_no_column(self): """#18263 -- Make sure hidden fields don't get a column in tabular inlines""" parent = SomeParentModel.objects.create(name="a") SomeChildModel.objects.create(name="b", position="0", parent=parent) SomeChildModel.objects.create(name="c", position="1", parent=parent) response = self.client.get( reverse("admin:admin_inlines_someparentmodel_change", args=(parent.pk,)) ) self.assertNotContains(response, '') self.assertInHTML( '', response.rendered_content, ) def test_tabular_inline_hidden_field_with_view_only_permissions(self): """ Content of hidden field is not visible in tabular inline when user has view-only permission. """ self.client.force_login(self.view_only_user) url = reverse( "tabular_inline_hidden_field_admin:admin_inlines_someparentmodel_change", args=(self.parent.pk,), ) response = self.client.get(url) self.assertInHTML( 'Position', response.rendered_content, ) self.assertInHTML( '

0

', response.rendered_content ) self.assertInHTML( '

1

', response.rendered_content ) def test_stacked_inline_hidden_field_with_view_only_permissions(self): """ Content of hidden field is not visible in stacked inline when user has view-only permission. """ self.client.force_login(self.view_only_user) url = reverse( "stacked_inline_hidden_field_in_group_admin:" "admin_inlines_someparentmodel_change", args=(self.parent.pk,), ) response = self.client.get(url) # The whole line containing name + position fields is not hidden. self.assertContains( response, '
' ) # The div containing the position field is hidden. self.assertInHTML( '', response.rendered_content, ) self.assertInHTML( '', response.rendered_content, ) def test_stacked_inline_single_hidden_field_in_line_with_view_only_permissions( self, ): """ Content of hidden field is not visible in stacked inline when user has view-only permission and the field is grouped on a separate line. """ self.client.force_login(self.view_only_user) url = reverse( "stacked_inline_hidden_field_on_single_line_admin:" "admin_inlines_someparentmodel_change", args=(self.parent.pk,), ) response = self.client.get(url) # The whole line containing position field is hidden. self.assertInHTML( '', response.rendered_content, ) self.assertInHTML( '', response.rendered_content, ) def test_tabular_inline_with_hidden_field_non_field_errors_has_correct_colspan( self, ): """ In tabular inlines, when a form has non-field errors, those errors are rendered in a table line with a single cell spanning the whole table width. Colspan must be equal to the number of visible columns. """ parent = SomeParentModel.objects.create(name="a") child = SomeChildModel.objects.create(name="b", position="0", parent=parent) url = reverse( "tabular_inline_hidden_field_admin:admin_inlines_someparentmodel_change", args=(parent.id,), ) data = { "name": parent.name, "somechildmodel_set-TOTAL_FORMS": 1, "somechildmodel_set-INITIAL_FORMS": 1, "somechildmodel_set-MIN_NUM_FORMS": 0, "somechildmodel_set-MAX_NUM_FORMS": 1000, "_save": "Save", "somechildmodel_set-0-id": child.id, "somechildmodel_set-0-parent": parent.id, "somechildmodel_set-0-name": child.name, "somechildmodel_set-0-position": 1, } response = self.client.post(url, data) # Form has 3 visible columns and 1 hidden column. self.assertInHTML( '' 'Name' 'Position' "Delete?", response.rendered_content, ) # The non-field error must be spanned on 3 (visible) columns. self.assertInHTML( '' '', response.rendered_content, ) def test_non_related_name_inline(self): """ Multiple inlines with related_name='+' have correct form prefixes. """ response = self.client.get(reverse("admin:admin_inlines_capofamiglia_add")) self.assertContains( response, '', html=True ) self.assertContains( response, '', html=True, ) self.assertContains( response, '', html=True, ) self.assertContains( response, '', html=True ) self.assertContains( response, '', html=True, ) self.assertContains( response, '', html=True, ) @override_settings(USE_THOUSAND_SEPARATOR=True) def test_localize_pk_shortcut(self): """ The "View on Site" link is correct for locales that use thousand separators. """ holder = Holder.objects.create(pk=123456789, dummy=42) inner = Inner.objects.create(pk=987654321, holder=holder, dummy=42, readonly="") response = self.client.get( reverse("admin:admin_inlines_holder_change", args=(holder.id,)) ) inner_shortcut = "r/%s/%s/" % ( ContentType.objects.get_for_model(inner).pk, inner.pk, ) self.assertContains(response, inner_shortcut) def test_custom_pk_shortcut(self): """ The "View on Site" link is correct for models with a custom primary key field. """ parent = ParentModelWithCustomPk.objects.create(my_own_pk="foo", name="Foo") child1 = ChildModel1.objects.create(my_own_pk="bar", name="Bar", parent=parent) child2 = ChildModel2.objects.create(my_own_pk="baz", name="Baz", parent=parent) response = self.client.get( reverse("admin:admin_inlines_parentmodelwithcustompk_change", args=("foo",)) ) child1_shortcut = "r/%s/%s/" % ( ContentType.objects.get_for_model(child1).pk, child1.pk, ) child2_shortcut = "r/%s/%s/" % ( ContentType.objects.get_for_model(child2).pk, child2.pk, ) self.assertContains(response, child1_shortcut) self.assertContains(response, child2_shortcut) def test_create_inlines_on_inherited_model(self): """ An object can be created with inlines when it inherits another class. """ data = { "name": "Martian", "sighting_set-TOTAL_FORMS": 1, "sighting_set-INITIAL_FORMS": 0, "sighting_set-MAX_NUM_FORMS": 0, "sighting_set-0-place": "Zone 51", "_save": "Save", } response = self.client.post( reverse("admin:admin_inlines_extraterrestrial_add"), data ) self.assertEqual(response.status_code, 302) self.assertEqual(Sighting.objects.filter(et__name="Martian").count(), 1) def test_custom_get_extra_form(self): bt_head = BinaryTree.objects.create(name="Tree Head") BinaryTree.objects.create(name="First Child", parent=bt_head) # The maximum number of forms should respect 'get_max_num' on the # ModelAdmin max_forms_input = ( '' ) # The total number of forms will remain the same in either case total_forms_hidden = ( '' ) response = self.client.get(reverse("admin:admin_inlines_binarytree_add")) self.assertInHTML(max_forms_input % 3, response.rendered_content) self.assertInHTML(total_forms_hidden, response.rendered_content) response = self.client.get( reverse("admin:admin_inlines_binarytree_change", args=(bt_head.id,)) ) self.assertInHTML(max_forms_input % 2, response.rendered_content) self.assertInHTML(total_forms_hidden, response.rendered_content) def test_min_num(self): """ min_num and extra determine number of forms. """ class MinNumInline(TabularInline): model = BinaryTree min_num = 2 extra = 3 modeladmin = ModelAdmin(BinaryTree, admin_site) modeladmin.inlines = [MinNumInline] min_forms = ( '' ) total_forms = ( '' ) request = self.factory.get(reverse("admin:admin_inlines_binarytree_add")) request.user = User(username="super", is_superuser=True) response = modeladmin.changeform_view(request) self.assertInHTML(min_forms, response.rendered_content) self.assertInHTML(total_forms, response.rendered_content) def test_custom_min_num(self): bt_head = BinaryTree.objects.create(name="Tree Head") BinaryTree.objects.create(name="First Child", parent=bt_head) class MinNumInline(TabularInline): model = BinaryTree extra = 3 def get_min_num(self, request, obj=None, **kwargs): if obj: return 5 return 2 modeladmin = ModelAdmin(BinaryTree, admin_site) modeladmin.inlines = [MinNumInline] min_forms = ( '' ) total_forms = ( '' ) request = self.factory.get(reverse("admin:admin_inlines_binarytree_add")) request.user = User(username="super", is_superuser=True) response = modeladmin.changeform_view(request) self.assertInHTML(min_forms % 2, response.rendered_content) self.assertInHTML(total_forms % 5, response.rendered_content) request = self.factory.get( reverse("admin:admin_inlines_binarytree_change", args=(bt_head.id,)) ) request.user = User(username="super", is_superuser=True) response = modeladmin.changeform_view(request, object_id=str(bt_head.id)) self.assertInHTML(min_forms % 5, response.rendered_content) self.assertInHTML(total_forms % 8, response.rendered_content) def test_inline_nonauto_noneditable_pk(self): response = self.client.get(reverse("admin:admin_inlines_author_add")) self.assertContains( response, '', html=True, ) self.assertContains( response, '', html=True, ) def test_inline_nonauto_noneditable_inherited_pk(self): response = self.client.get(reverse("admin:admin_inlines_author_add")) self.assertContains( response, '', html=True, ) self.assertContains( response, '', html=True, ) def test_inline_editable_pk(self): response = self.client.get(reverse("admin:admin_inlines_author_add")) self.assertContains( response, '', html=True, count=1, ) self.assertContains( response, '', html=True, count=1, ) def test_stacked_inline_edit_form_contains_has_original_class(self): holder = Holder.objects.create(dummy=1) holder.inner_set.create(dummy=1) response = self.client.get( reverse("admin:admin_inlines_holder_change", args=(holder.pk,)) ) self.assertContains( response, '