',
)
self.assertNotEqual(
table_output.find(row_html),
-1,
"Failed to find expected row element: %s" % table_output,
)
def test_result_list_editable_html(self):
"""
Regression tests for #11791: Inclusion tag result_list generates a
table and this checks that the items are nested within the table
element tags.
Also a regression test for #13599, verifies that hidden fields
when list_editable is enabled are rendered in a div outside the
table.
"""
new_parent = Parent.objects.create(name="parent")
new_child = Child.objects.create(name="name", parent=new_parent)
request = self.factory.get("/child/")
request.user = self.superuser
m = ChildAdmin(Child, custom_site)
# Test with list_editable fields
m.list_display = ["id", "name", "parent"]
m.list_display_links = ["id"]
m.list_editable = ["name"]
cl = m.get_changelist_instance(request)
FormSet = m.get_changelist_formset(request)
cl.formset = FormSet(queryset=cl.result_list)
template = Template(
"{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}"
)
context = Context({"cl": cl, "opts": Child._meta})
table_output = template.render(context)
# make sure that hidden fields are in the correct place
hiddenfields_div = (
'
'
''
"
"
) % new_child.id
self.assertInHTML(
hiddenfields_div, table_output, msg_prefix="Failed to find hidden fields"
)
# make sure that list editable fields are rendered in divs correctly
editable_name_field = (
''
)
self.assertInHTML(
'
%s
' % editable_name_field,
table_output,
msg_prefix='Failed to find "name" list_editable field',
)
def test_result_list_editable(self):
"""
Regression test for #14312: list_editable with pagination
"""
new_parent = Parent.objects.create(name="parent")
for i in range(1, 201):
Child.objects.create(name="name %s" % i, parent=new_parent)
request = self.factory.get("/child/", data={"p": -1}) # Anything outside range
request.user = self.superuser
m = ChildAdmin(Child, custom_site)
# Test with list_editable fields
m.list_display = ["id", "name", "parent"]
m.list_display_links = ["id"]
m.list_editable = ["name"]
with self.assertRaises(IncorrectLookupParameters):
m.get_changelist_instance(request)
@skipUnlessDBFeature("supports_transactions")
def test_list_editable_atomicity(self):
a = Swallow.objects.create(origin="Swallow A", load=4, speed=1)
b = Swallow.objects.create(origin="Swallow B", load=2, speed=2)
self.client.force_login(self.superuser)
changelist_url = reverse("admin:admin_changelist_swallow_changelist")
data = {
"form-TOTAL_FORMS": "2",
"form-INITIAL_FORMS": "2",
"form-MIN_NUM_FORMS": "0",
"form-MAX_NUM_FORMS": "1000",
"form-0-uuid": str(a.pk),
"form-1-uuid": str(b.pk),
"form-0-load": "9.0",
"form-0-speed": "3.0",
"form-1-load": "5.0",
"form-1-speed": "1.0",
"_save": "Save",
}
with mock.patch(
"django.contrib.admin.ModelAdmin.log_change", side_effect=DatabaseError
):
with self.assertRaises(DatabaseError):
self.client.post(changelist_url, data)
# Original values are preserved.
a.refresh_from_db()
self.assertEqual(a.load, 4)
self.assertEqual(a.speed, 1)
b.refresh_from_db()
self.assertEqual(b.load, 2)
self.assertEqual(b.speed, 2)
with mock.patch(
"django.contrib.admin.ModelAdmin.log_change",
side_effect=[None, DatabaseError],
):
with self.assertRaises(DatabaseError):
self.client.post(changelist_url, data)
# Original values are preserved.
a.refresh_from_db()
self.assertEqual(a.load, 4)
self.assertEqual(a.speed, 1)
b.refresh_from_db()
self.assertEqual(b.load, 2)
self.assertEqual(b.speed, 2)
def test_custom_paginator(self):
new_parent = Parent.objects.create(name="parent")
for i in range(1, 201):
Child.objects.create(name="name %s" % i, parent=new_parent)
request = self.factory.get("/child/")
request.user = self.superuser
m = CustomPaginationAdmin(Child, custom_site)
cl = m.get_changelist_instance(request)
cl.get_results(request)
self.assertIsInstance(cl.paginator, CustomPaginator)
def test_distinct_for_m2m_in_list_filter(self):
"""
Regression test for #13902: When using a ManyToMany in list_filter,
results shouldn't appear more than once. Basic ManyToMany.
"""
blues = Genre.objects.create(name="Blues")
band = Band.objects.create(name="B.B. King Review", nr_of_members=11)
band.genres.add(blues)
band.genres.add(blues)
m = BandAdmin(Band, custom_site)
request = self.factory.get("/band/", data={"genres": blues.pk})
request.user = self.superuser
cl = m.get_changelist_instance(request)
cl.get_results(request)
# There's only one Group instance
self.assertEqual(cl.result_count, 1)
# Queryset must be deletable.
cl.queryset.delete()
self.assertEqual(cl.queryset.count(), 0)
def test_distinct_for_through_m2m_in_list_filter(self):
"""
Regression test for #13902: When using a ManyToMany in list_filter,
results shouldn't appear more than once. With an intermediate model.
"""
lead = Musician.objects.create(name="Vox")
band = Group.objects.create(name="The Hype")
Membership.objects.create(group=band, music=lead, role="lead voice")
Membership.objects.create(group=band, music=lead, role="bass player")
m = GroupAdmin(Group, custom_site)
request = self.factory.get("/group/", data={"members": lead.pk})
request.user = self.superuser
cl = m.get_changelist_instance(request)
cl.get_results(request)
# There's only one Group instance
self.assertEqual(cl.result_count, 1)
# Queryset must be deletable.
cl.queryset.delete()
self.assertEqual(cl.queryset.count(), 0)
def test_distinct_for_through_m2m_at_second_level_in_list_filter(self):
"""
When using a ManyToMany in list_filter at the second level behind a
ForeignKey, distinct() must be called and results shouldn't appear more
than once.
"""
lead = Musician.objects.create(name="Vox")
band = Group.objects.create(name="The Hype")
Concert.objects.create(name="Woodstock", group=band)
Membership.objects.create(group=band, music=lead, role="lead voice")
Membership.objects.create(group=band, music=lead, role="bass player")
m = ConcertAdmin(Concert, custom_site)
request = self.factory.get("/concert/", data={"group__members": lead.pk})
request.user = self.superuser
cl = m.get_changelist_instance(request)
cl.get_results(request)
# There's only one Concert instance
self.assertEqual(cl.result_count, 1)
# Queryset must be deletable.
cl.queryset.delete()
self.assertEqual(cl.queryset.count(), 0)
def test_distinct_for_inherited_m2m_in_list_filter(self):
"""
Regression test for #13902: When using a ManyToMany in list_filter,
results shouldn't appear more than once. Model managed in the
admin inherits from the one that defines the relationship.
"""
lead = Musician.objects.create(name="John")
four = Quartet.objects.create(name="The Beatles")
Membership.objects.create(group=four, music=lead, role="lead voice")
Membership.objects.create(group=four, music=lead, role="guitar player")
m = QuartetAdmin(Quartet, custom_site)
request = self.factory.get("/quartet/", data={"members": lead.pk})
request.user = self.superuser
cl = m.get_changelist_instance(request)
cl.get_results(request)
# There's only one Quartet instance
self.assertEqual(cl.result_count, 1)
# Queryset must be deletable.
cl.queryset.delete()
self.assertEqual(cl.queryset.count(), 0)
def test_distinct_for_m2m_to_inherited_in_list_filter(self):
"""
Regression test for #13902: When using a ManyToMany in list_filter,
results shouldn't appear more than once. Target of the relationship
inherits from another.
"""
lead = ChordsMusician.objects.create(name="Player A")
three = ChordsBand.objects.create(name="The Chords Trio")
Invitation.objects.create(band=three, player=lead, instrument="guitar")
Invitation.objects.create(band=three, player=lead, instrument="bass")
m = ChordsBandAdmin(ChordsBand, custom_site)
request = self.factory.get("/chordsband/", data={"members": lead.pk})
request.user = self.superuser
cl = m.get_changelist_instance(request)
cl.get_results(request)
# There's only one ChordsBand instance
self.assertEqual(cl.result_count, 1)
def test_distinct_for_non_unique_related_object_in_list_filter(self):
"""
Regressions tests for #15819: If a field listed in list_filters
is a non-unique related object, distinct() must be called.
"""
parent = Parent.objects.create(name="Mary")
# Two children with the same name
Child.objects.create(parent=parent, name="Daniel")
Child.objects.create(parent=parent, name="Daniel")
m = ParentAdmin(Parent, custom_site)
request = self.factory.get("/parent/", data={"child__name": "Daniel"})
request.user = self.superuser
cl = m.get_changelist_instance(request)
# Make sure distinct() was called
self.assertEqual(cl.queryset.count(), 1)
# Queryset must be deletable.
cl.queryset.delete()
self.assertEqual(cl.queryset.count(), 0)
def test_changelist_search_form_validation(self):
m = ConcertAdmin(Concert, custom_site)
tests = [
({SEARCH_VAR: "\x00"}, "Null characters are not allowed."),
({SEARCH_VAR: "some\x00thing"}, "Null characters are not allowed."),
]
for case, error in tests:
with self.subTest(case=case):
request = self.factory.get("/concert/", case)
request.user = self.superuser
request._messages = CookieStorage(request)
m.get_changelist_instance(request)
messages = [m.message for m in request._messages]
self.assertEqual(1, len(messages))
self.assertEqual(error, messages[0])
def test_distinct_for_non_unique_related_object_in_search_fields(self):
"""
Regressions tests for #15819: If a field listed in search_fields
is a non-unique related object, distinct() must be called.
"""
parent = Parent.objects.create(name="Mary")
Child.objects.create(parent=parent, name="Danielle")
Child.objects.create(parent=parent, name="Daniel")
m = ParentAdmin(Parent, custom_site)
request = self.factory.get("/parent/", data={SEARCH_VAR: "daniel"})
request.user = self.superuser
cl = m.get_changelist_instance(request)
# Make sure distinct() was called
self.assertEqual(cl.queryset.count(), 1)
# Queryset must be deletable.
cl.queryset.delete()
self.assertEqual(cl.queryset.count(), 0)
def test_distinct_for_many_to_many_at_second_level_in_search_fields(self):
"""
When using a ManyToMany in search_fields at the second level behind a
ForeignKey, distinct() must be called and results shouldn't appear more
than once.
"""
lead = Musician.objects.create(name="Vox")
band = Group.objects.create(name="The Hype")
Concert.objects.create(name="Woodstock", group=band)
Membership.objects.create(group=band, music=lead, role="lead voice")
Membership.objects.create(group=band, music=lead, role="bass player")
m = ConcertAdmin(Concert, custom_site)
request = self.factory.get("/concert/", data={SEARCH_VAR: "vox"})
request.user = self.superuser
cl = m.get_changelist_instance(request)
# There's only one Concert instance
self.assertEqual(cl.queryset.count(), 1)
# Queryset must be deletable.
cl.queryset.delete()
self.assertEqual(cl.queryset.count(), 0)
def test_multiple_search_fields(self):
"""
All rows containing each of the searched words are returned, where each
word must be in one of search_fields.
"""
band_duo = Group.objects.create(name="Duo")
band_hype = Group.objects.create(name="The Hype")
mary = Musician.objects.create(name="Mary Halvorson")
jonathan = Musician.objects.create(name="Jonathan Finlayson")
band_duo.members.set([mary, jonathan])
Concert.objects.create(name="Tiny desk concert", group=band_duo)
Concert.objects.create(name="Woodstock concert", group=band_hype)
# FK lookup.
concert_model_admin = ConcertAdmin(Concert, custom_site)
concert_model_admin.search_fields = ["group__name", "name"]
# Reverse FK lookup.
group_model_admin = GroupAdmin(Group, custom_site)
group_model_admin.search_fields = ["name", "concert__name", "members__name"]
for search_string, result_count in (
("Duo Concert", 1),
("Tiny Desk Concert", 1),
("Concert", 2),
("Other Concert", 0),
("Duo Woodstock", 0),
):
with self.subTest(search_string=search_string):
# FK lookup.
request = self.factory.get(
"/concert/", data={SEARCH_VAR: search_string}
)
request.user = self.superuser
concert_changelist = concert_model_admin.get_changelist_instance(
request
)
self.assertEqual(concert_changelist.queryset.count(), result_count)
# Reverse FK lookup.
request = self.factory.get("/group/", data={SEARCH_VAR: search_string})
request.user = self.superuser
group_changelist = group_model_admin.get_changelist_instance(request)
self.assertEqual(group_changelist.queryset.count(), result_count)
# Many-to-many lookup.
for search_string, result_count in (
("Finlayson Duo Tiny", 1),
("Finlayson", 1),
("Finlayson Hype", 0),
("Jonathan Finlayson Duo", 1),
("Mary Jonathan Duo", 0),
("Oscar Finlayson Duo", 0),
):
with self.subTest(search_string=search_string):
request = self.factory.get("/group/", data={SEARCH_VAR: search_string})
request.user = self.superuser
group_changelist = group_model_admin.get_changelist_instance(request)
self.assertEqual(group_changelist.queryset.count(), result_count)
def test_pk_in_search_fields(self):
band = Group.objects.create(name="The Hype")
Concert.objects.create(name="Woodstock", group=band)
m = ConcertAdmin(Concert, custom_site)
m.search_fields = ["group__pk"]
request = self.factory.get("/concert/", data={SEARCH_VAR: band.pk})
request.user = self.superuser
cl = m.get_changelist_instance(request)
self.assertEqual(cl.queryset.count(), 1)
request = self.factory.get("/concert/", data={SEARCH_VAR: band.pk + 5})
request.user = self.superuser
cl = m.get_changelist_instance(request)
self.assertEqual(cl.queryset.count(), 0)
def test_builtin_lookup_in_search_fields(self):
band = Group.objects.create(name="The Hype")
concert = Concert.objects.create(name="Woodstock", group=band)
m = ConcertAdmin(Concert, custom_site)
m.search_fields = ["name__iexact"]
request = self.factory.get("/", data={SEARCH_VAR: "woodstock"})
request.user = self.superuser
cl = m.get_changelist_instance(request)
self.assertCountEqual(cl.queryset, [concert])
request = self.factory.get("/", data={SEARCH_VAR: "wood"})
request.user = self.superuser
cl = m.get_changelist_instance(request)
self.assertCountEqual(cl.queryset, [])
def test_custom_lookup_in_search_fields(self):
band = Group.objects.create(name="The Hype")
concert = Concert.objects.create(name="Woodstock", group=band)
m = ConcertAdmin(Concert, custom_site)
m.search_fields = ["group__name__cc"]
with register_lookup(Field, Contains, lookup_name="cc"):
request = self.factory.get("/", data={SEARCH_VAR: "Hype"})
request.user = self.superuser
cl = m.get_changelist_instance(request)
self.assertCountEqual(cl.queryset, [concert])
request = self.factory.get("/", data={SEARCH_VAR: "Woodstock"})
request.user = self.superuser
cl = m.get_changelist_instance(request)
self.assertCountEqual(cl.queryset, [])
def test_spanning_relations_with_custom_lookup_in_search_fields(self):
hype = Group.objects.create(name="The Hype")
concert = Concert.objects.create(name="Woodstock", group=hype)
vox = Musician.objects.create(name="Vox", age=20)
Membership.objects.create(music=vox, group=hype)
# Register a custom lookup on IntegerField to ensure that field
# traversing logic in ModelAdmin.get_search_results() works.
with register_lookup(IntegerField, Exact, lookup_name="exactly"):
m = ConcertAdmin(Concert, custom_site)
m.search_fields = ["group__members__age__exactly"]
request = self.factory.get("/", data={SEARCH_VAR: "20"})
request.user = self.superuser
cl = m.get_changelist_instance(request)
self.assertCountEqual(cl.queryset, [concert])
request = self.factory.get("/", data={SEARCH_VAR: "21"})
request.user = self.superuser
cl = m.get_changelist_instance(request)
self.assertCountEqual(cl.queryset, [])
def test_custom_lookup_with_pk_shortcut(self):
self.assertEqual(CharPK._meta.pk.name, "char_pk") # Not equal to 'pk'.
m = admin.ModelAdmin(CustomIdUser, custom_site)
abc = CharPK.objects.create(char_pk="abc")
abcd = CharPK.objects.create(char_pk="abcd")
m = admin.ModelAdmin(CharPK, custom_site)
m.search_fields = ["pk__exact"]
request = self.factory.get("/", data={SEARCH_VAR: "abc"})
request.user = self.superuser
cl = m.get_changelist_instance(request)
self.assertCountEqual(cl.queryset, [abc])
request = self.factory.get("/", data={SEARCH_VAR: "abcd"})
request.user = self.superuser
cl = m.get_changelist_instance(request)
self.assertCountEqual(cl.queryset, [abcd])
def test_search_with_exact_lookup_for_non_string_field(self):
child = Child.objects.create(name="Asher", age=11)
model_admin = ChildAdmin(Child, custom_site)
for search_term, expected_result in [
("11", [child]),
("Asher", [child]),
("1", []),
("A", []),
("random", []),
]:
request = self.factory.get("/", data={SEARCH_VAR: search_term})
request.user = self.superuser
with self.subTest(search_term=search_term):
# 1 query for filtered result, 1 for filtered count, 1 for total count.
with self.assertNumQueries(3):
cl = model_admin.get_changelist_instance(request)
self.assertCountEqual(cl.queryset, expected_result)
def test_search_with_exact_lookup_relationship_field(self):
child = Child.objects.create(name="I am a child", age=11)
grandchild = GrandChild.objects.create(name="I am a grandchild", parent=child)
model_admin = GrandChildAdmin(GrandChild, custom_site)
request = self.factory.get("/", data={SEARCH_VAR: "'I am a child'"})
request.user = self.superuser
cl = model_admin.get_changelist_instance(request)
self.assertCountEqual(cl.queryset, [grandchild])
for search_term, expected_result in [
("11", [grandchild]),
("'I am a child'", [grandchild]),
("1", []),
("A", []),
("random", []),
]:
request = self.factory.get("/", data={SEARCH_VAR: search_term})
request.user = self.superuser
with self.subTest(search_term=search_term):
cl = model_admin.get_changelist_instance(request)
self.assertCountEqual(cl.queryset, expected_result)
def test_no_distinct_for_m2m_in_list_filter_without_params(self):
"""
If a ManyToManyField is in list_filter but isn't in any lookup params,
the changelist's query shouldn't have distinct.
"""
m = BandAdmin(Band, custom_site)
for lookup_params in ({}, {"name": "test"}):
request = self.factory.get("/band/", lookup_params)
request.user = self.superuser
cl = m.get_changelist_instance(request)
self.assertIs(cl.queryset.query.distinct, False)
# A ManyToManyField in params does have distinct applied.
request = self.factory.get("/band/", {"genres": "0"})
request.user = self.superuser
cl = m.get_changelist_instance(request)
self.assertIs(cl.queryset.query.distinct, True)
def test_pagination(self):
"""
Regression tests for #12893: Pagination in admins changelist doesn't
use queryset set by modeladmin.
"""
parent = Parent.objects.create(name="anything")
for i in range(1, 31):
Child.objects.create(name="name %s" % i, parent=parent)
Child.objects.create(name="filtered %s" % i, parent=parent)
request = self.factory.get("/child/")
request.user = self.superuser
# Test default queryset
m = ChildAdmin(Child, custom_site)
cl = m.get_changelist_instance(request)
self.assertEqual(cl.queryset.count(), 60)
self.assertEqual(cl.paginator.count, 60)
self.assertEqual(list(cl.paginator.page_range), [1, 2, 3, 4, 5, 6])
# Test custom queryset
m = FilteredChildAdmin(Child, custom_site)
cl = m.get_changelist_instance(request)
self.assertEqual(cl.queryset.count(), 30)
self.assertEqual(cl.paginator.count, 30)
self.assertEqual(list(cl.paginator.page_range), [1, 2, 3])
def test_pagination_render(self):
objs = [Swallow(origin=f"Swallow {i}", load=i, speed=i) for i in range(1, 5)]
Swallow.objects.bulk_create(objs)
request = self.factory.get("/child/")
request.user = self.superuser
admin = SwallowAdmin(Swallow, custom_site)
cl = admin.get_changelist_instance(request)
template = Template(
"{% load admin_list %}{% spaceless %}{% pagination cl %}{% endspaceless %}"
)
context = Context({"cl": cl, "opts": cl.opts})
pagination_output = template.render(context)
self.assertTrue(
pagination_output.startswith(
'"))
self.assertInHTML(
'