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, 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, '
')
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,
'
'
'
The two titles must be the same
'
)
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,
)
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,
)
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(
'
'
'
A non-field error
',
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,
'
', html=True)
self.assertContains(
response,
'' % self.inner2.dummy,
html=True,
)
def test_inline_change_fk_add_change_perm(self):
permission = Permission.objects.get(codename='add_inner2', content_type=self.inner_ct)
self.user.user_permissions.add(permission)
permission = Permission.objects.get(codename='change_inner2', content_type=self.inner_ct)
self.user.user_permissions.add(permission)
response = self.client.get(self.holder_change_url)
# Add/change perm, so we can add new and change existing
self.assertContains(response, '
Inner2s
')
# One form for existing instance and three extra for new
self.assertContains(
response, '',
html=True
)
self.assertContains(
response,
'' % self.inner2.id,
html=True
)
def test_inline_change_fk_change_del_perm(self):
permission = Permission.objects.get(codename='change_inner2', content_type=self.inner_ct)
self.user.user_permissions.add(permission)
permission = Permission.objects.get(codename='delete_inner2', content_type=self.inner_ct)
self.user.user_permissions.add(permission)
response = self.client.get(self.holder_change_url)
# Change/delete perm on inner2s, so we can change/delete existing
self.assertContains(response, '
Inner2s
')
# One form for existing instance only, no new
self.assertContains(
response,
'',
html=True
)
self.assertContains(
response,
'' % self.inner2.id,
html=True
)
self.assertContains(response, 'id="id_inner2_set-0-DELETE"')
def test_inline_change_fk_all_perms(self):
permission = Permission.objects.get(codename='add_inner2', content_type=self.inner_ct)
self.user.user_permissions.add(permission)
permission = Permission.objects.get(codename='change_inner2', content_type=self.inner_ct)
self.user.user_permissions.add(permission)
permission = Permission.objects.get(codename='delete_inner2', content_type=self.inner_ct)
self.user.user_permissions.add(permission)
response = self.client.get(self.holder_change_url)
# All perms on inner2s, so we can add/change/delete
self.assertContains(response, '
Inner2s
', count=2)
# One form for existing instance only, three for new
self.assertContains(
response,
'',
html=True
)
self.assertContains(
response,
'' % self.inner2.id,
html=True
)
self.assertContains(response, 'id="id_inner2_set-0-DELETE"')
# TabularInline
self.assertContains(response, '
' # noqa
self.assertNotContains(
response,
delete_link % self.poll.id,
html=True
)
self.assertNotContains(response, '')
self.assertNotContains(response, '')
def test_inline_delete_buttons_are_not_shown(self):
Question.objects.create(text="How will this be rendered?", poll=self.poll)
response = self.client.get(self.change_url)
self.assertNotContains(
response,
'',
html=True
)
def test_extra_inlines_are_not_shown(self):
response = self.client.get(self.change_url)
self.assertNotContains(response, 'id="id_question_set-0-text"')
@override_settings(ROOT_URLCONF='admin_inlines.urls')
class TestVerboseNameInlineForms(TestDataMixin, TestCase):
factory = RequestFactory()
def test_verbose_name_inline(self):
class NonVerboseProfileInline(TabularInline):
model = Profile
verbose_name = 'Non-verbose childs'
class VerboseNameProfileInline(TabularInline):
model = VerboseNameProfile
verbose_name = 'Childs with verbose name'
class VerboseNamePluralProfileInline(TabularInline):
model = VerboseNamePluralProfile
verbose_name = 'Childs with verbose name plural'
class BothVerboseNameProfileInline(TabularInline):
model = BothVerboseNameProfile
verbose_name = 'Childs with both verbose names'
modeladmin = ModelAdmin(ProfileCollection, admin_site)
modeladmin.inlines = [
NonVerboseProfileInline,
VerboseNameProfileInline,
VerboseNamePluralProfileInline,
BothVerboseNameProfileInline,
]
obj = ProfileCollection.objects.create()
url = reverse('admin:admin_inlines_profilecollection_change', args=(obj.pk,))
request = self.factory.get(url)
request.user = self.superuser
response = modeladmin.changeform_view(request)
self.assertNotContains(response, 'Add another Profile')
# Non-verbose model.
self.assertContains(response, '
Non-verbose childss
')
self.assertContains(response, 'Add another Non-verbose child')
self.assertNotContains(response, '
Profiles
')
# Model with verbose name.
self.assertContains(response, '
Childs with verbose names
')
self.assertContains(response, 'Add another Childs with verbose name')
self.assertNotContains(response, '
Model with verbose name onlys
')
self.assertNotContains(response, 'Add another Model with verbose name only')
# Model with verbose name plural.
self.assertContains(response, '
Childs with verbose name plurals
')
self.assertContains(response, 'Add another Childs with verbose name plural')
self.assertNotContains(response, '
Model with verbose name plural only
')
# Model with both verbose names.
self.assertContains(response, '
Childs with both verbose namess
')
self.assertContains(response, 'Add another Childs with both verbose names')
self.assertNotContains(response, '
Model with both - plural name
')
self.assertNotContains(response, 'Add another Model with both - name')
def test_verbose_name_plural_inline(self):
class NonVerboseProfileInline(TabularInline):
model = Profile
verbose_name_plural = 'Non-verbose childs'
class VerboseNameProfileInline(TabularInline):
model = VerboseNameProfile
verbose_name_plural = 'Childs with verbose name'
class VerboseNamePluralProfileInline(TabularInline):
model = VerboseNamePluralProfile
verbose_name_plural = 'Childs with verbose name plural'
class BothVerboseNameProfileInline(TabularInline):
model = BothVerboseNameProfile
verbose_name_plural = 'Childs with both verbose names'
modeladmin = ModelAdmin(ProfileCollection, admin_site)
modeladmin.inlines = [
NonVerboseProfileInline,
VerboseNameProfileInline,
VerboseNamePluralProfileInline,
BothVerboseNameProfileInline,
]
obj = ProfileCollection.objects.create()
url = reverse('admin:admin_inlines_profilecollection_change', args=(obj.pk,))
request = self.factory.get(url)
request.user = self.superuser
response = modeladmin.changeform_view(request)
# Non-verbose model.
self.assertContains(response, '
Non-verbose childs
')
self.assertContains(response, 'Add another Profile')
self.assertNotContains(response, '
Profiles
')
# Model with verbose name.
self.assertContains(response, '
Childs with verbose name
')
self.assertContains(response, 'Add another Model with verbose name only')
self.assertNotContains(response, '
Model with verbose name onlys
')
# Model with verbose name plural.
self.assertContains(response, '
Childs with verbose name plural
')
self.assertContains(response, 'Add another Profile')
self.assertNotContains(response, '
Model with verbose name plural only
')
# Model with both verbose names.
self.assertContains(response, '
Childs with both verbose names
')
self.assertContains(response, 'Add another Model with both - name')
self.assertNotContains(response, '
Model with both - plural name
')
def test_both_verbose_names_inline(self):
class NonVerboseProfileInline(TabularInline):
model = Profile
verbose_name = 'Non-verbose childs - name'
verbose_name_plural = 'Non-verbose childs - plural name'
class VerboseNameProfileInline(TabularInline):
model = VerboseNameProfile
verbose_name = 'Childs with verbose name - name'
verbose_name_plural = 'Childs with verbose name - plural name'
class VerboseNamePluralProfileInline(TabularInline):
model = VerboseNamePluralProfile
verbose_name = 'Childs with verbose name plural - name'
verbose_name_plural = 'Childs with verbose name plural - plural name'
class BothVerboseNameProfileInline(TabularInline):
model = BothVerboseNameProfile
verbose_name = 'Childs with both - name'
verbose_name_plural = 'Childs with both - plural name'
modeladmin = ModelAdmin(ProfileCollection, admin_site)
modeladmin.inlines = [
NonVerboseProfileInline,
VerboseNameProfileInline,
VerboseNamePluralProfileInline,
BothVerboseNameProfileInline,
]
obj = ProfileCollection.objects.create()
url = reverse('admin:admin_inlines_profilecollection_change', args=(obj.pk,))
request = self.factory.get(url)
request.user = self.superuser
response = modeladmin.changeform_view(request)
self.assertNotContains(response, 'Add another Profile')
# Non-verbose model.
self.assertContains(response, '
Non-verbose childs - plural name
')
self.assertContains(response, 'Add another Non-verbose childs - name')
self.assertNotContains(response, '
Profiles
')
# Model with verbose name.
self.assertContains(response, '
Childs with verbose name - plural name
')
self.assertContains(response, 'Add another Childs with verbose name - name')
self.assertNotContains(response, '
Model with verbose name onlys
')
# Model with verbose name plural.
self.assertContains(
response,
'
Childs with verbose name plural - plural name
',
)
self.assertContains(
response,
'Add another Childs with verbose name plural - name',
)
self.assertNotContains(response, '
Model with verbose name plural only
')
# Model with both verbose names.
self.assertContains(response, '
Childs with both - plural name
')
self.assertContains(response, 'Add another Childs with both - name')
self.assertNotContains(response, '
Model with both - plural name
')
self.assertNotContains(response, 'Add another Model with both - name')
@override_settings(ROOT_URLCONF='admin_inlines.urls')
class SeleniumTests(AdminSeleniumTestCase):
available_apps = ['admin_inlines'] + AdminSeleniumTestCase.available_apps
def setUp(self):
User.objects.create_superuser(username='super', password='secret', email='super@example.com')
def test_add_stackeds(self):
"""
The "Add another XXX" link correctly adds items to the stacked formset.
"""
from selenium.webdriver.common.by import By
self.admin_login(username='super', password='secret')
self.selenium.get(self.live_server_url + reverse('admin:admin_inlines_holder4_add'))
inline_id = '#inner4stacked_set-group'
rows_selector = '%s .dynamic-inner4stacked_set' % inline_id
self.assertCountSeleniumElements(rows_selector, 3)
add_button = self.selenium.find_element(By.LINK_TEXT, 'Add another Inner4 stacked')
add_button.click()
self.assertCountSeleniumElements(rows_selector, 4)
def test_delete_stackeds(self):
from selenium.webdriver.common.by import By
self.admin_login(username='super', password='secret')
self.selenium.get(self.live_server_url + reverse('admin:admin_inlines_holder4_add'))
inline_id = '#inner4stacked_set-group'
rows_selector = '%s .dynamic-inner4stacked_set' % inline_id
self.assertCountSeleniumElements(rows_selector, 3)
add_button = self.selenium.find_element(By.LINK_TEXT, 'Add another Inner4 stacked')
add_button.click()
add_button.click()
self.assertCountSeleniumElements(rows_selector, 5)
for delete_link in self.selenium.find_elements(By.CSS_SELECTOR, '%s .inline-deletelink' % inline_id):
delete_link.click()
with self.disable_implicit_wait():
self.assertCountSeleniumElements(rows_selector, 0)
def test_delete_invalid_stacked_inlines(self):
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.common.by import By
self.admin_login(username='super', password='secret')
self.selenium.get(self.live_server_url + reverse('admin:admin_inlines_holder4_add'))
inline_id = '#inner4stacked_set-group'
rows_selector = '%s .dynamic-inner4stacked_set' % inline_id
self.assertCountSeleniumElements(rows_selector, 3)
add_button = self.selenium.find_element(
By.LINK_TEXT,
'Add another Inner4 stacked',
)
add_button.click()
add_button.click()
self.assertCountSeleniumElements('#id_inner4stacked_set-4-dummy', 1)
# Enter some data and click 'Save'.
self.selenium.find_element(By.NAME, 'dummy').send_keys('1')
self.selenium.find_element(By.NAME, 'inner4stacked_set-0-dummy').send_keys('100')
self.selenium.find_element(By.NAME, 'inner4stacked_set-1-dummy').send_keys('101')
self.selenium.find_element(By.NAME, 'inner4stacked_set-2-dummy').send_keys('222')
self.selenium.find_element(By.NAME, 'inner4stacked_set-3-dummy').send_keys('103')
self.selenium.find_element(By.NAME, 'inner4stacked_set-4-dummy').send_keys('222')
with self.wait_page_loaded():
self.selenium.find_element(By.XPATH, '//input[@value="Save"]').click()
# Sanity check.
self.assertCountSeleniumElements(rows_selector, 5)
errorlist = self.selenium.find_element(
By.CSS_SELECTOR,
'%s .dynamic-inner4stacked_set .errorlist li' % inline_id,
)
self.assertEqual('Please correct the duplicate values below.', errorlist.text)
delete_link = self.selenium.find_element(By.CSS_SELECTOR, '#inner4stacked_set-4 .inline-deletelink')
delete_link.click()
self.assertCountSeleniumElements(rows_selector, 4)
with self.disable_implicit_wait(), self.assertRaises(NoSuchElementException):
self.selenium.find_element(By.CSS_SELECTOR, '%s .dynamic-inner4stacked_set .errorlist li' % inline_id)
with self.wait_page_loaded():
self.selenium.find_element(By.XPATH, '//input[@value="Save"]').click()
# The objects have been created in the database.
self.assertEqual(Inner4Stacked.objects.all().count(), 4)
def test_delete_invalid_tabular_inlines(self):
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.common.by import By
self.admin_login(username='super', password='secret')
self.selenium.get(self.live_server_url + reverse('admin:admin_inlines_holder4_add'))
inline_id = '#inner4tabular_set-group'
rows_selector = '%s .dynamic-inner4tabular_set' % inline_id
self.assertCountSeleniumElements(rows_selector, 3)
add_button = self.selenium.find_element(
By.LINK_TEXT,
'Add another Inner4 tabular'
)
add_button.click()
add_button.click()
self.assertCountSeleniumElements('#id_inner4tabular_set-4-dummy', 1)
# Enter some data and click 'Save'.
self.selenium.find_element(By.NAME, 'dummy').send_keys('1')
self.selenium.find_element(By.NAME, 'inner4tabular_set-0-dummy').send_keys('100')
self.selenium.find_element(By.NAME, 'inner4tabular_set-1-dummy').send_keys('101')
self.selenium.find_element(By.NAME, 'inner4tabular_set-2-dummy').send_keys('222')
self.selenium.find_element(By.NAME, 'inner4tabular_set-3-dummy').send_keys('103')
self.selenium.find_element(By.NAME, 'inner4tabular_set-4-dummy').send_keys('222')
with self.wait_page_loaded():
self.selenium.find_element(By.XPATH, '//input[@value="Save"]').click()
# Sanity Check.
self.assertCountSeleniumElements(rows_selector, 5)
# Non-field errorlist is in its own
just before
# tr#inner4tabular_set-3:
errorlist = self.selenium.find_element(
By.CSS_SELECTOR,
'%s #inner4tabular_set-3 + .row-form-errors .errorlist li' % inline_id
)
self.assertEqual('Please correct the duplicate values below.', errorlist.text)
delete_link = self.selenium.find_element(By.CSS_SELECTOR, '#inner4tabular_set-4 .inline-deletelink')
delete_link.click()
self.assertCountSeleniumElements(rows_selector, 4)
with self.disable_implicit_wait(), self.assertRaises(NoSuchElementException):
self.selenium.find_element(By.CSS_SELECTOR, '%s .dynamic-inner4tabular_set .errorlist li' % inline_id)
with self.wait_page_loaded():
self.selenium.find_element(By.XPATH, '//input[@value="Save"]').click()
# The objects have been created in the database.
self.assertEqual(Inner4Tabular.objects.all().count(), 4)
def test_add_inlines(self):
"""
The "Add another XXX" link correctly adds items to the inline form.
"""
from selenium.webdriver.common.by import By
self.admin_login(username='super', password='secret')
self.selenium.get(self.live_server_url + reverse('admin:admin_inlines_profilecollection_add'))
# There's only one inline to start with and it has the correct ID.
self.assertCountSeleniumElements('.dynamic-profile_set', 1)
self.assertEqual(
self.selenium.find_elements(By.CSS_SELECTOR, '.dynamic-profile_set')[0].get_attribute('id'),
'profile_set-0',
)
self.assertCountSeleniumElements('.dynamic-profile_set#profile_set-0 input[name=profile_set-0-first_name]', 1)
self.assertCountSeleniumElements('.dynamic-profile_set#profile_set-0 input[name=profile_set-0-last_name]', 1)
# Add an inline
self.selenium.find_element(By.LINK_TEXT, 'Add another Profile').click()
# The inline has been added, it has the right id, and it contains the
# correct fields.
self.assertCountSeleniumElements('.dynamic-profile_set', 2)
self.assertEqual(
self.selenium.find_elements(By.CSS_SELECTOR, '.dynamic-profile_set')[1].get_attribute('id'),
'profile_set-1',
)
self.assertCountSeleniumElements('.dynamic-profile_set#profile_set-1 input[name=profile_set-1-first_name]', 1)
self.assertCountSeleniumElements('.dynamic-profile_set#profile_set-1 input[name=profile_set-1-last_name]', 1)
# Let's add another one to be sure
self.selenium.find_element(By.LINK_TEXT, 'Add another Profile').click()
self.assertCountSeleniumElements('.dynamic-profile_set', 3)
self.assertEqual(
self.selenium.find_elements(By.CSS_SELECTOR, '.dynamic-profile_set')[2].get_attribute('id'),
'profile_set-2',
)
self.assertCountSeleniumElements('.dynamic-profile_set#profile_set-2 input[name=profile_set-2-first_name]', 1)
self.assertCountSeleniumElements('.dynamic-profile_set#profile_set-2 input[name=profile_set-2-last_name]', 1)
# Enter some data and click 'Save'
self.selenium.find_element(By.NAME, 'profile_set-0-first_name').send_keys('0 first name 1')
self.selenium.find_element(By.NAME, 'profile_set-0-last_name').send_keys('0 last name 2')
self.selenium.find_element(By.NAME, 'profile_set-1-first_name').send_keys('1 first name 1')
self.selenium.find_element(By.NAME, 'profile_set-1-last_name').send_keys('1 last name 2')
self.selenium.find_element(By.NAME, 'profile_set-2-first_name').send_keys('2 first name 1')
self.selenium.find_element(By.NAME, 'profile_set-2-last_name').send_keys('2 last name 2')
with self.wait_page_loaded():
self.selenium.find_element(By.XPATH, '//input[@value="Save"]').click()
# The objects have been created in the database
self.assertEqual(ProfileCollection.objects.all().count(), 1)
self.assertEqual(Profile.objects.all().count(), 3)
def test_add_inline_link_absent_for_view_only_parent_model(self):
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.common.by import By
user = User.objects.create_user('testing', password='password', is_staff=True)
user.user_permissions.add(
Permission.objects.get(codename='view_poll', content_type=ContentType.objects.get_for_model(Poll))
)
user.user_permissions.add(
*Permission.objects.filter(
codename__endswith="question", content_type=ContentType.objects.get_for_model(Question)
).values_list('pk', flat=True)
)
self.admin_login(username='testing', password='password')
poll = Poll.objects.create(name="Survey")
change_url = reverse('admin:admin_inlines_poll_change', args=(poll.id,))
self.selenium.get(self.live_server_url + change_url)
with self.disable_implicit_wait():
with self.assertRaises(NoSuchElementException):
self.selenium.find_element(By.LINK_TEXT, 'Add another Question')
def test_delete_inlines(self):
from selenium.webdriver.common.by import By
self.admin_login(username='super', password='secret')
self.selenium.get(self.live_server_url + reverse('admin:admin_inlines_profilecollection_add'))
# Add a few inlines
self.selenium.find_element(By.LINK_TEXT, 'Add another Profile').click()
self.selenium.find_element(By.LINK_TEXT, 'Add another Profile').click()
self.selenium.find_element(By.LINK_TEXT, 'Add another Profile').click()
self.selenium.find_element(By.LINK_TEXT, 'Add another Profile').click()
self.assertCountSeleniumElements('#profile_set-group table tr.dynamic-profile_set', 5)
self.assertCountSeleniumElements('form#profilecollection_form tr.dynamic-profile_set#profile_set-0', 1)
self.assertCountSeleniumElements('form#profilecollection_form tr.dynamic-profile_set#profile_set-1', 1)
self.assertCountSeleniumElements('form#profilecollection_form tr.dynamic-profile_set#profile_set-2', 1)
self.assertCountSeleniumElements('form#profilecollection_form tr.dynamic-profile_set#profile_set-3', 1)
self.assertCountSeleniumElements('form#profilecollection_form tr.dynamic-profile_set#profile_set-4', 1)
# Click on a few delete buttons
self.selenium.find_element(
By.CSS_SELECTOR,
'form#profilecollection_form tr.dynamic-profile_set#profile_set-1 td.delete a',
).click()
self.selenium.find_element(
By.CSS_SELECTOR,
'form#profilecollection_form tr.dynamic-profile_set#profile_set-2 td.delete a',
).click()
# The rows are gone and the IDs have been re-sequenced
self.assertCountSeleniumElements('#profile_set-group table tr.dynamic-profile_set', 3)
self.assertCountSeleniumElements('form#profilecollection_form tr.dynamic-profile_set#profile_set-0', 1)
self.assertCountSeleniumElements('form#profilecollection_form tr.dynamic-profile_set#profile_set-1', 1)
self.assertCountSeleniumElements('form#profilecollection_form tr.dynamic-profile_set#profile_set-2', 1)
def test_collapsed_inlines(self):
from selenium.webdriver.common.by import By
# Collapsed inlines have SHOW/HIDE links.
self.admin_login(username='super', password='secret')
self.selenium.get(self.live_server_url + reverse('admin:admin_inlines_author_add'))
# One field is in a stacked inline, other in a tabular one.
test_fields = ['#id_nonautopkbook_set-0-title', '#id_nonautopkbook_set-2-0-title']
show_links = self.selenium.find_elements(By.LINK_TEXT, 'SHOW')
self.assertEqual(len(show_links), 3)
for show_index, field_name in enumerate(test_fields, 0):
self.wait_until_invisible(field_name)
show_links[show_index].click()
self.wait_until_visible(field_name)
hide_links = self.selenium.find_elements(By.LINK_TEXT, 'HIDE')
self.assertEqual(len(hide_links), 2)
for hide_index, field_name in enumerate(test_fields, 0):
self.wait_until_visible(field_name)
hide_links[hide_index].click()
self.wait_until_invisible(field_name)
def test_added_stacked_inline_with_collapsed_fields(self):
from selenium.webdriver.common.by import By
self.admin_login(username='super', password='secret')
self.selenium.get(self.live_server_url + reverse('admin:admin_inlines_teacher_add'))
self.selenium.find_element(By.LINK_TEXT, 'Add another Child').click()
test_fields = ['#id_child_set-0-name', '#id_child_set-1-name']
show_links = self.selenium.find_elements(By.LINK_TEXT, 'SHOW')
self.assertEqual(len(show_links), 2)
for show_index, field_name in enumerate(test_fields, 0):
self.wait_until_invisible(field_name)
show_links[show_index].click()
self.wait_until_visible(field_name)
hide_links = self.selenium.find_elements(By.LINK_TEXT, 'HIDE')
self.assertEqual(len(hide_links), 2)
for hide_index, field_name in enumerate(test_fields, 0):
self.wait_until_visible(field_name)
hide_links[hide_index].click()
self.wait_until_invisible(field_name)
def assertBorder(self, element, border):
width, style, color = border.split(' ')
border_properties = [
'border-bottom-%s',
'border-left-%s',
'border-right-%s',
'border-top-%s',
]
for prop in border_properties:
prop = prop % 'width'
self.assertEqual(element.value_of_css_property(prop), width)
for prop in border_properties:
prop = prop % 'style'
self.assertEqual(element.value_of_css_property(prop), style)
# Convert hex color to rgb.
self.assertRegex(color, '#[0-9a-f]{6}')
r, g, b = int(color[1:3], 16), int(color[3:5], 16), int(color[5:], 16)
# The value may be expressed as either rgb() or rgba() depending on the
# browser.
colors = [
'rgb(%d, %d, %d)' % (r, g, b),
'rgba(%d, %d, %d, 1)' % (r, g, b),
]
for prop in border_properties:
prop = prop % 'color'
self.assertIn(element.value_of_css_property(prop), colors)
def test_inline_formset_error_input_border(self):
from selenium.webdriver.common.by import By
self.admin_login(username='super', password='secret')
self.selenium.get(self.live_server_url + reverse('admin:admin_inlines_holder5_add'))
self.wait_until_visible('#id_dummy')
self.selenium.find_element(By.ID, 'id_dummy').send_keys(1)
fields = ['id_inner5stacked_set-0-dummy', 'id_inner5tabular_set-0-dummy']
show_links = self.selenium.find_elements(By.LINK_TEXT, 'SHOW')
for show_index, field_name in enumerate(fields):
show_links[show_index].click()
self.wait_until_visible('#' + field_name)
self.selenium.find_element(By.ID, field_name).send_keys(1)
# Before save all inputs have default border
for inline in ('stacked', 'tabular'):
for field_name in ('name', 'select', 'text'):
element_id = 'id_inner5%s_set-0-%s' % (inline, field_name)
self.assertBorder(
self.selenium.find_element(By.ID, element_id),
'1px solid #cccccc',
)
self.selenium.find_element(By.XPATH, '//input[@value="Save"]').click()
# Test the red border around inputs by css selectors
stacked_selectors = ['.errors input', '.errors select', '.errors textarea']
for selector in stacked_selectors:
self.assertBorder(
self.selenium.find_element(By.CSS_SELECTOR, selector),
'1px solid #ba2121',
)
tabular_selectors = [
'td ul.errorlist + input', 'td ul.errorlist + select', 'td ul.errorlist + textarea'
]
for selector in tabular_selectors:
self.assertBorder(
self.selenium.find_element(By.CSS_SELECTOR, selector),
'1px solid #ba2121',
)
def test_inline_formset_error(self):
from selenium.webdriver.common.by import By
self.admin_login(username='super', password='secret')
self.selenium.get(self.live_server_url + reverse('admin:admin_inlines_holder5_add'))
stacked_inline_formset_selector = 'div#inner5stacked_set-group fieldset.module.collapse'
tabular_inline_formset_selector = 'div#inner5tabular_set-group fieldset.module.collapse'
# Inlines without errors, both inlines collapsed
self.selenium.find_element(By.XPATH, '//input[@value="Save"]').click()
self.assertCountSeleniumElements(stacked_inline_formset_selector + '.collapsed', 1)
self.assertCountSeleniumElements(tabular_inline_formset_selector + '.collapsed', 1)
show_links = self.selenium.find_elements(By.LINK_TEXT, 'SHOW')
self.assertEqual(len(show_links), 2)
# Inlines with errors, both inlines expanded
test_fields = ['#id_inner5stacked_set-0-dummy', '#id_inner5tabular_set-0-dummy']
for show_index, field_name in enumerate(test_fields):
show_links[show_index].click()
self.wait_until_visible(field_name)
self.selenium.find_element(By.ID, field_name[1:]).send_keys(1)
hide_links = self.selenium.find_elements(By.LINK_TEXT, 'HIDE')
self.assertEqual(len(hide_links), 2)
for hide_index, field_name in enumerate(test_fields):
hide_link = hide_links[hide_index]
self.selenium.execute_script('window.scrollTo(0, %s);' % hide_link.location['y'])
hide_link.click()
self.wait_until_invisible(field_name)
with self.wait_page_loaded():
self.selenium.find_element(By.XPATH, '//input[@value="Save"]').click()
with self.disable_implicit_wait():
self.assertCountSeleniumElements(stacked_inline_formset_selector + '.collapsed', 0)
self.assertCountSeleniumElements(tabular_inline_formset_selector + '.collapsed', 0)
self.assertCountSeleniumElements(stacked_inline_formset_selector, 1)
self.assertCountSeleniumElements(tabular_inline_formset_selector, 1)
def test_inlines_verbose_name(self):
"""
The item added by the "Add another XXX" link must use the correct
verbose_name in the inline form.
"""
from selenium.webdriver.common.by import By
self.admin_login(username='super', password='secret')
# Hide sidebar.
self.selenium.get(self.live_server_url + reverse('admin:admin_inlines_course_add'))
toggle_button = self.selenium.find_element(By.CSS_SELECTOR, '#toggle-nav-sidebar')
toggle_button.click()
# Each combination of horizontal/vertical filter with stacked/tabular
# inlines.
tests = [
'admin:admin_inlines_course_add',
'admin:admin_inlines_courseproxy_add',
'admin:admin_inlines_courseproxy1_add',
'admin:admin_inlines_courseproxy2_add',
]
css_selector = '.dynamic-class_set#class_set-%s h2'
for url_name in tests:
with self.subTest(url=url_name):
self.selenium.get(self.live_server_url + reverse(url_name))
# First inline shows the verbose_name.
available, chosen = self.selenium.find_elements(By.CSS_SELECTOR, css_selector % 0)
self.assertEqual(available.text, 'AVAILABLE ATTENDANT')
self.assertEqual(chosen.text, 'CHOSEN ATTENDANT')
# Added inline should also have the correct verbose_name.
self.selenium.find_element(By.LINK_TEXT, 'Add another Class').click()
available, chosen = self.selenium.find_elements(By.CSS_SELECTOR, css_selector % 1)
self.assertEqual(available.text, 'AVAILABLE ATTENDANT')
self.assertEqual(chosen.text, 'CHOSEN ATTENDANT')
# Third inline should also have the correct verbose_name.
self.selenium.find_element(By.LINK_TEXT, 'Add another Class').click()
available, chosen = self.selenium.find_elements(By.CSS_SELECTOR, css_selector % 2)
self.assertEqual(available.text, 'AVAILABLE ATTENDANT')
self.assertEqual(chosen.text, 'CHOSEN ATTENDANT')