1. import datetime
    
  2. 
    
  3. from django.contrib import admin
    
  4. from django.contrib.admin.models import LogEntry
    
  5. from django.contrib.admin.options import IncorrectLookupParameters
    
  6. from django.contrib.admin.templatetags.admin_list import pagination
    
  7. from django.contrib.admin.tests import AdminSeleniumTestCase
    
  8. from django.contrib.admin.views.main import (
    
  9.     ALL_VAR,
    
  10.     IS_POPUP_VAR,
    
  11.     ORDER_VAR,
    
  12.     PAGE_VAR,
    
  13.     SEARCH_VAR,
    
  14.     TO_FIELD_VAR,
    
  15. )
    
  16. from django.contrib.auth.models import User
    
  17. from django.contrib.contenttypes.models import ContentType
    
  18. from django.contrib.messages.storage.cookie import CookieStorage
    
  19. from django.db import connection, models
    
  20. from django.db.models import F, Field, IntegerField
    
  21. from django.db.models.functions import Upper
    
  22. from django.db.models.lookups import Contains, Exact
    
  23. from django.template import Context, Template, TemplateSyntaxError
    
  24. from django.test import TestCase, override_settings
    
  25. from django.test.client import RequestFactory
    
  26. from django.test.utils import CaptureQueriesContext, isolate_apps, register_lookup
    
  27. from django.urls import reverse
    
  28. from django.utils import formats
    
  29. 
    
  30. from .admin import (
    
  31.     BandAdmin,
    
  32.     ChildAdmin,
    
  33.     ChordsBandAdmin,
    
  34.     ConcertAdmin,
    
  35.     CustomPaginationAdmin,
    
  36.     CustomPaginator,
    
  37.     DynamicListDisplayChildAdmin,
    
  38.     DynamicListDisplayLinksChildAdmin,
    
  39.     DynamicListFilterChildAdmin,
    
  40.     DynamicSearchFieldsChildAdmin,
    
  41.     EmptyValueChildAdmin,
    
  42.     EventAdmin,
    
  43.     FilteredChildAdmin,
    
  44.     GroupAdmin,
    
  45.     InvitationAdmin,
    
  46.     NoListDisplayLinksParentAdmin,
    
  47.     ParentAdmin,
    
  48.     ParentAdminTwoSearchFields,
    
  49.     QuartetAdmin,
    
  50.     SwallowAdmin,
    
  51. )
    
  52. from .admin import site as custom_site
    
  53. from .models import (
    
  54.     Band,
    
  55.     CharPK,
    
  56.     Child,
    
  57.     ChordsBand,
    
  58.     ChordsMusician,
    
  59.     Concert,
    
  60.     CustomIdUser,
    
  61.     Event,
    
  62.     Genre,
    
  63.     Group,
    
  64.     Invitation,
    
  65.     Membership,
    
  66.     Musician,
    
  67.     OrderedObject,
    
  68.     Parent,
    
  69.     Quartet,
    
  70.     Swallow,
    
  71.     SwallowOneToOne,
    
  72.     UnorderedObject,
    
  73. )
    
  74. 
    
  75. 
    
  76. def build_tbody_html(pk, href, extra_fields):
    
  77.     return (
    
  78.         "<tbody><tr>"
    
  79.         '<td class="action-checkbox">'
    
  80.         '<input type="checkbox" name="_selected_action" value="{}" '
    
  81.         'class="action-select"></td>'
    
  82.         '<th class="field-name"><a href="{}">name</a></th>'
    
  83.         "{}</tr></tbody>"
    
  84.     ).format(pk, href, extra_fields)
    
  85. 
    
  86. 
    
  87. @override_settings(ROOT_URLCONF="admin_changelist.urls")
    
  88. class ChangeListTests(TestCase):
    
  89.     factory = RequestFactory()
    
  90. 
    
  91.     @classmethod
    
  92.     def setUpTestData(cls):
    
  93.         cls.superuser = User.objects.create_superuser(
    
  94.             username="super", email="[email protected]", password="xxx"
    
  95.         )
    
  96. 
    
  97.     def _create_superuser(self, username):
    
  98.         return User.objects.create_superuser(
    
  99.             username=username, email="[email protected]", password="xxx"
    
  100.         )
    
  101. 
    
  102.     def _mocked_authenticated_request(self, url, user):
    
  103.         request = self.factory.get(url)
    
  104.         request.user = user
    
  105.         return request
    
  106. 
    
  107.     def test_repr(self):
    
  108.         m = ChildAdmin(Child, custom_site)
    
  109.         request = self.factory.get("/child/")
    
  110.         request.user = self.superuser
    
  111.         cl = m.get_changelist_instance(request)
    
  112.         self.assertEqual(repr(cl), "<ChangeList: model=Child model_admin=ChildAdmin>")
    
  113. 
    
  114.     def test_specified_ordering_by_f_expression(self):
    
  115.         class OrderedByFBandAdmin(admin.ModelAdmin):
    
  116.             list_display = ["name", "genres", "nr_of_members"]
    
  117.             ordering = (
    
  118.                 F("nr_of_members").desc(nulls_last=True),
    
  119.                 Upper(F("name")).asc(),
    
  120.                 F("genres").asc(),
    
  121.             )
    
  122. 
    
  123.         m = OrderedByFBandAdmin(Band, custom_site)
    
  124.         request = self.factory.get("/band/")
    
  125.         request.user = self.superuser
    
  126.         cl = m.get_changelist_instance(request)
    
  127.         self.assertEqual(cl.get_ordering_field_columns(), {3: "desc", 2: "asc"})
    
  128. 
    
  129.     def test_specified_ordering_by_f_expression_without_asc_desc(self):
    
  130.         class OrderedByFBandAdmin(admin.ModelAdmin):
    
  131.             list_display = ["name", "genres", "nr_of_members"]
    
  132.             ordering = (F("nr_of_members"), Upper("name"), F("genres"))
    
  133. 
    
  134.         m = OrderedByFBandAdmin(Band, custom_site)
    
  135.         request = self.factory.get("/band/")
    
  136.         request.user = self.superuser
    
  137.         cl = m.get_changelist_instance(request)
    
  138.         self.assertEqual(cl.get_ordering_field_columns(), {3: "asc", 2: "asc"})
    
  139. 
    
  140.     def test_select_related_preserved(self):
    
  141.         """
    
  142.         Regression test for #10348: ChangeList.get_queryset() shouldn't
    
  143.         overwrite a custom select_related provided by ModelAdmin.get_queryset().
    
  144.         """
    
  145.         m = ChildAdmin(Child, custom_site)
    
  146.         request = self.factory.get("/child/")
    
  147.         request.user = self.superuser
    
  148.         cl = m.get_changelist_instance(request)
    
  149.         self.assertEqual(cl.queryset.query.select_related, {"parent": {}})
    
  150. 
    
  151.     def test_select_related_preserved_when_multi_valued_in_search_fields(self):
    
  152.         parent = Parent.objects.create(name="Mary")
    
  153.         Child.objects.create(parent=parent, name="Danielle")
    
  154.         Child.objects.create(parent=parent, name="Daniel")
    
  155. 
    
  156.         m = ParentAdmin(Parent, custom_site)
    
  157.         request = self.factory.get("/parent/", data={SEARCH_VAR: "daniel"})
    
  158.         request.user = self.superuser
    
  159. 
    
  160.         cl = m.get_changelist_instance(request)
    
  161.         self.assertEqual(cl.queryset.count(), 1)
    
  162.         # select_related is preserved.
    
  163.         self.assertEqual(cl.queryset.query.select_related, {"child": {}})
    
  164. 
    
  165.     def test_select_related_as_tuple(self):
    
  166.         ia = InvitationAdmin(Invitation, custom_site)
    
  167.         request = self.factory.get("/invitation/")
    
  168.         request.user = self.superuser
    
  169.         cl = ia.get_changelist_instance(request)
    
  170.         self.assertEqual(cl.queryset.query.select_related, {"player": {}})
    
  171. 
    
  172.     def test_select_related_as_empty_tuple(self):
    
  173.         ia = InvitationAdmin(Invitation, custom_site)
    
  174.         ia.list_select_related = ()
    
  175.         request = self.factory.get("/invitation/")
    
  176.         request.user = self.superuser
    
  177.         cl = ia.get_changelist_instance(request)
    
  178.         self.assertIs(cl.queryset.query.select_related, False)
    
  179. 
    
  180.     def test_get_select_related_custom_method(self):
    
  181.         class GetListSelectRelatedAdmin(admin.ModelAdmin):
    
  182.             list_display = ("band", "player")
    
  183. 
    
  184.             def get_list_select_related(self, request):
    
  185.                 return ("band", "player")
    
  186. 
    
  187.         ia = GetListSelectRelatedAdmin(Invitation, custom_site)
    
  188.         request = self.factory.get("/invitation/")
    
  189.         request.user = self.superuser
    
  190.         cl = ia.get_changelist_instance(request)
    
  191.         self.assertEqual(cl.queryset.query.select_related, {"player": {}, "band": {}})
    
  192. 
    
  193.     def test_many_search_terms(self):
    
  194.         parent = Parent.objects.create(name="Mary")
    
  195.         Child.objects.create(parent=parent, name="Danielle")
    
  196.         Child.objects.create(parent=parent, name="Daniel")
    
  197. 
    
  198.         m = ParentAdmin(Parent, custom_site)
    
  199.         request = self.factory.get("/parent/", data={SEARCH_VAR: "daniel " * 80})
    
  200.         request.user = self.superuser
    
  201. 
    
  202.         cl = m.get_changelist_instance(request)
    
  203.         with CaptureQueriesContext(connection) as context:
    
  204.             object_count = cl.queryset.count()
    
  205.         self.assertEqual(object_count, 1)
    
  206.         self.assertEqual(context.captured_queries[0]["sql"].count("JOIN"), 1)
    
  207. 
    
  208.     def test_related_field_multiple_search_terms(self):
    
  209.         """
    
  210.         Searches over multi-valued relationships return rows from related
    
  211.         models only when all searched fields match that row.
    
  212.         """
    
  213.         parent = Parent.objects.create(name="Mary")
    
  214.         Child.objects.create(parent=parent, name="Danielle", age=18)
    
  215.         Child.objects.create(parent=parent, name="Daniel", age=19)
    
  216. 
    
  217.         m = ParentAdminTwoSearchFields(Parent, custom_site)
    
  218. 
    
  219.         request = self.factory.get("/parent/", data={SEARCH_VAR: "danielle 19"})
    
  220.         request.user = self.superuser
    
  221.         cl = m.get_changelist_instance(request)
    
  222.         self.assertEqual(cl.queryset.count(), 0)
    
  223. 
    
  224.         request = self.factory.get("/parent/", data={SEARCH_VAR: "daniel 19"})
    
  225.         request.user = self.superuser
    
  226.         cl = m.get_changelist_instance(request)
    
  227.         self.assertEqual(cl.queryset.count(), 1)
    
  228. 
    
  229.     def test_result_list_empty_changelist_value(self):
    
  230.         """
    
  231.         Regression test for #14982: EMPTY_CHANGELIST_VALUE should be honored
    
  232.         for relationship fields
    
  233.         """
    
  234.         new_child = Child.objects.create(name="name", parent=None)
    
  235.         request = self.factory.get("/child/")
    
  236.         request.user = self.superuser
    
  237.         m = ChildAdmin(Child, custom_site)
    
  238.         cl = m.get_changelist_instance(request)
    
  239.         cl.formset = None
    
  240.         template = Template(
    
  241.             "{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}"
    
  242.         )
    
  243.         context = Context({"cl": cl, "opts": Child._meta})
    
  244.         table_output = template.render(context)
    
  245.         link = reverse("admin:admin_changelist_child_change", args=(new_child.id,))
    
  246.         row_html = build_tbody_html(
    
  247.             new_child.id, link, '<td class="field-parent nowrap">-</td>'
    
  248.         )
    
  249.         self.assertNotEqual(
    
  250.             table_output.find(row_html),
    
  251.             -1,
    
  252.             "Failed to find expected row element: %s" % table_output,
    
  253.         )
    
  254. 
    
  255.     def test_result_list_set_empty_value_display_on_admin_site(self):
    
  256.         """
    
  257.         Empty value display can be set on AdminSite.
    
  258.         """
    
  259.         new_child = Child.objects.create(name="name", parent=None)
    
  260.         request = self.factory.get("/child/")
    
  261.         request.user = self.superuser
    
  262.         # Set a new empty display value on AdminSite.
    
  263.         admin.site.empty_value_display = "???"
    
  264.         m = ChildAdmin(Child, admin.site)
    
  265.         cl = m.get_changelist_instance(request)
    
  266.         cl.formset = None
    
  267.         template = Template(
    
  268.             "{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}"
    
  269.         )
    
  270.         context = Context({"cl": cl, "opts": Child._meta})
    
  271.         table_output = template.render(context)
    
  272.         link = reverse("admin:admin_changelist_child_change", args=(new_child.id,))
    
  273.         row_html = build_tbody_html(
    
  274.             new_child.id, link, '<td class="field-parent nowrap">???</td>'
    
  275.         )
    
  276.         self.assertNotEqual(
    
  277.             table_output.find(row_html),
    
  278.             -1,
    
  279.             "Failed to find expected row element: %s" % table_output,
    
  280.         )
    
  281. 
    
  282.     def test_result_list_set_empty_value_display_in_model_admin(self):
    
  283.         """
    
  284.         Empty value display can be set in ModelAdmin or individual fields.
    
  285.         """
    
  286.         new_child = Child.objects.create(name="name", parent=None)
    
  287.         request = self.factory.get("/child/")
    
  288.         request.user = self.superuser
    
  289.         m = EmptyValueChildAdmin(Child, admin.site)
    
  290.         cl = m.get_changelist_instance(request)
    
  291.         cl.formset = None
    
  292.         template = Template(
    
  293.             "{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}"
    
  294.         )
    
  295.         context = Context({"cl": cl, "opts": Child._meta})
    
  296.         table_output = template.render(context)
    
  297.         link = reverse("admin:admin_changelist_child_change", args=(new_child.id,))
    
  298.         row_html = build_tbody_html(
    
  299.             new_child.id,
    
  300.             link,
    
  301.             '<td class="field-age_display">&amp;dagger;</td>'
    
  302.             '<td class="field-age">-empty-</td>',
    
  303.         )
    
  304.         self.assertNotEqual(
    
  305.             table_output.find(row_html),
    
  306.             -1,
    
  307.             "Failed to find expected row element: %s" % table_output,
    
  308.         )
    
  309. 
    
  310.     def test_result_list_html(self):
    
  311.         """
    
  312.         Inclusion tag result_list generates a table when with default
    
  313.         ModelAdmin settings.
    
  314.         """
    
  315.         new_parent = Parent.objects.create(name="parent")
    
  316.         new_child = Child.objects.create(name="name", parent=new_parent)
    
  317.         request = self.factory.get("/child/")
    
  318.         request.user = self.superuser
    
  319.         m = ChildAdmin(Child, custom_site)
    
  320.         cl = m.get_changelist_instance(request)
    
  321.         cl.formset = None
    
  322.         template = Template(
    
  323.             "{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}"
    
  324.         )
    
  325.         context = Context({"cl": cl, "opts": Child._meta})
    
  326.         table_output = template.render(context)
    
  327.         link = reverse("admin:admin_changelist_child_change", args=(new_child.id,))
    
  328.         row_html = build_tbody_html(
    
  329.             new_child.id, link, '<td class="field-parent nowrap">%s</td>' % new_parent
    
  330.         )
    
  331.         self.assertNotEqual(
    
  332.             table_output.find(row_html),
    
  333.             -1,
    
  334.             "Failed to find expected row element: %s" % table_output,
    
  335.         )
    
  336. 
    
  337.     def test_result_list_editable_html(self):
    
  338.         """
    
  339.         Regression tests for #11791: Inclusion tag result_list generates a
    
  340.         table and this checks that the items are nested within the table
    
  341.         element tags.
    
  342.         Also a regression test for #13599, verifies that hidden fields
    
  343.         when list_editable is enabled are rendered in a div outside the
    
  344.         table.
    
  345.         """
    
  346.         new_parent = Parent.objects.create(name="parent")
    
  347.         new_child = Child.objects.create(name="name", parent=new_parent)
    
  348.         request = self.factory.get("/child/")
    
  349.         request.user = self.superuser
    
  350.         m = ChildAdmin(Child, custom_site)
    
  351. 
    
  352.         # Test with list_editable fields
    
  353.         m.list_display = ["id", "name", "parent"]
    
  354.         m.list_display_links = ["id"]
    
  355.         m.list_editable = ["name"]
    
  356.         cl = m.get_changelist_instance(request)
    
  357.         FormSet = m.get_changelist_formset(request)
    
  358.         cl.formset = FormSet(queryset=cl.result_list)
    
  359.         template = Template(
    
  360.             "{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}"
    
  361.         )
    
  362.         context = Context({"cl": cl, "opts": Child._meta})
    
  363.         table_output = template.render(context)
    
  364.         # make sure that hidden fields are in the correct place
    
  365.         hiddenfields_div = (
    
  366.             '<div class="hiddenfields">'
    
  367.             '<input type="hidden" name="form-0-id" value="%d" id="id_form-0-id">'
    
  368.             "</div>"
    
  369.         ) % new_child.id
    
  370.         self.assertInHTML(
    
  371.             hiddenfields_div, table_output, msg_prefix="Failed to find hidden fields"
    
  372.         )
    
  373. 
    
  374.         # make sure that list editable fields are rendered in divs correctly
    
  375.         editable_name_field = (
    
  376.             '<input name="form-0-name" value="name" class="vTextField" '
    
  377.             'maxlength="30" type="text" id="id_form-0-name">'
    
  378.         )
    
  379.         self.assertInHTML(
    
  380.             '<td class="field-name">%s</td>' % editable_name_field,
    
  381.             table_output,
    
  382.             msg_prefix='Failed to find "name" list_editable field',
    
  383.         )
    
  384. 
    
  385.     def test_result_list_editable(self):
    
  386.         """
    
  387.         Regression test for #14312: list_editable with pagination
    
  388.         """
    
  389.         new_parent = Parent.objects.create(name="parent")
    
  390.         for i in range(1, 201):
    
  391.             Child.objects.create(name="name %s" % i, parent=new_parent)
    
  392.         request = self.factory.get("/child/", data={"p": -1})  # Anything outside range
    
  393.         request.user = self.superuser
    
  394.         m = ChildAdmin(Child, custom_site)
    
  395. 
    
  396.         # Test with list_editable fields
    
  397.         m.list_display = ["id", "name", "parent"]
    
  398.         m.list_display_links = ["id"]
    
  399.         m.list_editable = ["name"]
    
  400.         with self.assertRaises(IncorrectLookupParameters):
    
  401.             m.get_changelist_instance(request)
    
  402. 
    
  403.     def test_custom_paginator(self):
    
  404.         new_parent = Parent.objects.create(name="parent")
    
  405.         for i in range(1, 201):
    
  406.             Child.objects.create(name="name %s" % i, parent=new_parent)
    
  407. 
    
  408.         request = self.factory.get("/child/")
    
  409.         request.user = self.superuser
    
  410.         m = CustomPaginationAdmin(Child, custom_site)
    
  411. 
    
  412.         cl = m.get_changelist_instance(request)
    
  413.         cl.get_results(request)
    
  414.         self.assertIsInstance(cl.paginator, CustomPaginator)
    
  415. 
    
  416.     def test_no_duplicates_for_m2m_in_list_filter(self):
    
  417.         """
    
  418.         Regression test for #13902: When using a ManyToMany in list_filter,
    
  419.         results shouldn't appear more than once. Basic ManyToMany.
    
  420.         """
    
  421.         blues = Genre.objects.create(name="Blues")
    
  422.         band = Band.objects.create(name="B.B. King Review", nr_of_members=11)
    
  423. 
    
  424.         band.genres.add(blues)
    
  425.         band.genres.add(blues)
    
  426. 
    
  427.         m = BandAdmin(Band, custom_site)
    
  428.         request = self.factory.get("/band/", data={"genres": blues.pk})
    
  429.         request.user = self.superuser
    
  430. 
    
  431.         cl = m.get_changelist_instance(request)
    
  432.         cl.get_results(request)
    
  433. 
    
  434.         # There's only one Group instance
    
  435.         self.assertEqual(cl.result_count, 1)
    
  436.         # Queryset must be deletable.
    
  437.         self.assertIs(cl.queryset.query.distinct, False)
    
  438.         cl.queryset.delete()
    
  439.         self.assertEqual(cl.queryset.count(), 0)
    
  440. 
    
  441.     def test_no_duplicates_for_through_m2m_in_list_filter(self):
    
  442.         """
    
  443.         Regression test for #13902: When using a ManyToMany in list_filter,
    
  444.         results shouldn't appear more than once. With an intermediate model.
    
  445.         """
    
  446.         lead = Musician.objects.create(name="Vox")
    
  447.         band = Group.objects.create(name="The Hype")
    
  448.         Membership.objects.create(group=band, music=lead, role="lead voice")
    
  449.         Membership.objects.create(group=band, music=lead, role="bass player")
    
  450. 
    
  451.         m = GroupAdmin(Group, custom_site)
    
  452.         request = self.factory.get("/group/", data={"members": lead.pk})
    
  453.         request.user = self.superuser
    
  454. 
    
  455.         cl = m.get_changelist_instance(request)
    
  456.         cl.get_results(request)
    
  457. 
    
  458.         # There's only one Group instance
    
  459.         self.assertEqual(cl.result_count, 1)
    
  460.         # Queryset must be deletable.
    
  461.         self.assertIs(cl.queryset.query.distinct, False)
    
  462.         cl.queryset.delete()
    
  463.         self.assertEqual(cl.queryset.count(), 0)
    
  464. 
    
  465.     def test_no_duplicates_for_through_m2m_at_second_level_in_list_filter(self):
    
  466.         """
    
  467.         When using a ManyToMany in list_filter at the second level behind a
    
  468.         ForeignKey, results shouldn't appear more than once.
    
  469.         """
    
  470.         lead = Musician.objects.create(name="Vox")
    
  471.         band = Group.objects.create(name="The Hype")
    
  472.         Concert.objects.create(name="Woodstock", group=band)
    
  473.         Membership.objects.create(group=band, music=lead, role="lead voice")
    
  474.         Membership.objects.create(group=band, music=lead, role="bass player")
    
  475. 
    
  476.         m = ConcertAdmin(Concert, custom_site)
    
  477.         request = self.factory.get("/concert/", data={"group__members": lead.pk})
    
  478.         request.user = self.superuser
    
  479. 
    
  480.         cl = m.get_changelist_instance(request)
    
  481.         cl.get_results(request)
    
  482. 
    
  483.         # There's only one Concert instance
    
  484.         self.assertEqual(cl.result_count, 1)
    
  485.         # Queryset must be deletable.
    
  486.         self.assertIs(cl.queryset.query.distinct, False)
    
  487.         cl.queryset.delete()
    
  488.         self.assertEqual(cl.queryset.count(), 0)
    
  489. 
    
  490.     def test_no_duplicates_for_inherited_m2m_in_list_filter(self):
    
  491.         """
    
  492.         Regression test for #13902: When using a ManyToMany in list_filter,
    
  493.         results shouldn't appear more than once. Model managed in the
    
  494.         admin inherits from the one that defines the relationship.
    
  495.         """
    
  496.         lead = Musician.objects.create(name="John")
    
  497.         four = Quartet.objects.create(name="The Beatles")
    
  498.         Membership.objects.create(group=four, music=lead, role="lead voice")
    
  499.         Membership.objects.create(group=four, music=lead, role="guitar player")
    
  500. 
    
  501.         m = QuartetAdmin(Quartet, custom_site)
    
  502.         request = self.factory.get("/quartet/", data={"members": lead.pk})
    
  503.         request.user = self.superuser
    
  504. 
    
  505.         cl = m.get_changelist_instance(request)
    
  506.         cl.get_results(request)
    
  507. 
    
  508.         # There's only one Quartet instance
    
  509.         self.assertEqual(cl.result_count, 1)
    
  510.         # Queryset must be deletable.
    
  511.         self.assertIs(cl.queryset.query.distinct, False)
    
  512.         cl.queryset.delete()
    
  513.         self.assertEqual(cl.queryset.count(), 0)
    
  514. 
    
  515.     def test_no_duplicates_for_m2m_to_inherited_in_list_filter(self):
    
  516.         """
    
  517.         Regression test for #13902: When using a ManyToMany in list_filter,
    
  518.         results shouldn't appear more than once. Target of the relationship
    
  519.         inherits from another.
    
  520.         """
    
  521.         lead = ChordsMusician.objects.create(name="Player A")
    
  522.         three = ChordsBand.objects.create(name="The Chords Trio")
    
  523.         Invitation.objects.create(band=three, player=lead, instrument="guitar")
    
  524.         Invitation.objects.create(band=three, player=lead, instrument="bass")
    
  525. 
    
  526.         m = ChordsBandAdmin(ChordsBand, custom_site)
    
  527.         request = self.factory.get("/chordsband/", data={"members": lead.pk})
    
  528.         request.user = self.superuser
    
  529. 
    
  530.         cl = m.get_changelist_instance(request)
    
  531.         cl.get_results(request)
    
  532. 
    
  533.         # There's only one ChordsBand instance
    
  534.         self.assertEqual(cl.result_count, 1)
    
  535.         # Queryset must be deletable.
    
  536.         self.assertIs(cl.queryset.query.distinct, False)
    
  537.         cl.queryset.delete()
    
  538.         self.assertEqual(cl.queryset.count(), 0)
    
  539. 
    
  540.     def test_no_duplicates_for_non_unique_related_object_in_list_filter(self):
    
  541.         """
    
  542.         Regressions tests for #15819: If a field listed in list_filters is a
    
  543.         non-unique related object, results shouldn't appear more than once.
    
  544.         """
    
  545.         parent = Parent.objects.create(name="Mary")
    
  546.         # Two children with the same name
    
  547.         Child.objects.create(parent=parent, name="Daniel")
    
  548.         Child.objects.create(parent=parent, name="Daniel")
    
  549. 
    
  550.         m = ParentAdmin(Parent, custom_site)
    
  551.         request = self.factory.get("/parent/", data={"child__name": "Daniel"})
    
  552.         request.user = self.superuser
    
  553. 
    
  554.         cl = m.get_changelist_instance(request)
    
  555.         # Exists() is applied.
    
  556.         self.assertEqual(cl.queryset.count(), 1)
    
  557.         # Queryset must be deletable.
    
  558.         self.assertIs(cl.queryset.query.distinct, False)
    
  559.         cl.queryset.delete()
    
  560.         self.assertEqual(cl.queryset.count(), 0)
    
  561. 
    
  562.     def test_changelist_search_form_validation(self):
    
  563.         m = ConcertAdmin(Concert, custom_site)
    
  564.         tests = [
    
  565.             ({SEARCH_VAR: "\x00"}, "Null characters are not allowed."),
    
  566.             ({SEARCH_VAR: "some\x00thing"}, "Null characters are not allowed."),
    
  567.         ]
    
  568.         for case, error in tests:
    
  569.             with self.subTest(case=case):
    
  570.                 request = self.factory.get("/concert/", case)
    
  571.                 request.user = self.superuser
    
  572.                 request._messages = CookieStorage(request)
    
  573.                 m.get_changelist_instance(request)
    
  574.                 messages = [m.message for m in request._messages]
    
  575.                 self.assertEqual(1, len(messages))
    
  576.                 self.assertEqual(error, messages[0])
    
  577. 
    
  578.     def test_no_duplicates_for_non_unique_related_object_in_search_fields(self):
    
  579.         """
    
  580.         Regressions tests for #15819: If a field listed in search_fields
    
  581.         is a non-unique related object, Exists() must be applied.
    
  582.         """
    
  583.         parent = Parent.objects.create(name="Mary")
    
  584.         Child.objects.create(parent=parent, name="Danielle")
    
  585.         Child.objects.create(parent=parent, name="Daniel")
    
  586. 
    
  587.         m = ParentAdmin(Parent, custom_site)
    
  588.         request = self.factory.get("/parent/", data={SEARCH_VAR: "daniel"})
    
  589.         request.user = self.superuser
    
  590. 
    
  591.         cl = m.get_changelist_instance(request)
    
  592.         # Exists() is applied.
    
  593.         self.assertEqual(cl.queryset.count(), 1)
    
  594.         # Queryset must be deletable.
    
  595.         self.assertIs(cl.queryset.query.distinct, False)
    
  596.         cl.queryset.delete()
    
  597.         self.assertEqual(cl.queryset.count(), 0)
    
  598. 
    
  599.     def test_no_duplicates_for_many_to_many_at_second_level_in_search_fields(self):
    
  600.         """
    
  601.         When using a ManyToMany in search_fields at the second level behind a
    
  602.         ForeignKey, Exists() must be applied and results shouldn't appear more
    
  603.         than once.
    
  604.         """
    
  605.         lead = Musician.objects.create(name="Vox")
    
  606.         band = Group.objects.create(name="The Hype")
    
  607.         Concert.objects.create(name="Woodstock", group=band)
    
  608.         Membership.objects.create(group=band, music=lead, role="lead voice")
    
  609.         Membership.objects.create(group=band, music=lead, role="bass player")
    
  610. 
    
  611.         m = ConcertAdmin(Concert, custom_site)
    
  612.         request = self.factory.get("/concert/", data={SEARCH_VAR: "vox"})
    
  613.         request.user = self.superuser
    
  614. 
    
  615.         cl = m.get_changelist_instance(request)
    
  616.         # There's only one Concert instance
    
  617.         self.assertEqual(cl.queryset.count(), 1)
    
  618.         # Queryset must be deletable.
    
  619.         self.assertIs(cl.queryset.query.distinct, False)
    
  620.         cl.queryset.delete()
    
  621.         self.assertEqual(cl.queryset.count(), 0)
    
  622. 
    
  623.     def test_multiple_search_fields(self):
    
  624.         """
    
  625.         All rows containing each of the searched words are returned, where each
    
  626.         word must be in one of search_fields.
    
  627.         """
    
  628.         band_duo = Group.objects.create(name="Duo")
    
  629.         band_hype = Group.objects.create(name="The Hype")
    
  630.         mary = Musician.objects.create(name="Mary Halvorson")
    
  631.         jonathan = Musician.objects.create(name="Jonathan Finlayson")
    
  632.         band_duo.members.set([mary, jonathan])
    
  633.         Concert.objects.create(name="Tiny desk concert", group=band_duo)
    
  634.         Concert.objects.create(name="Woodstock concert", group=band_hype)
    
  635.         # FK lookup.
    
  636.         concert_model_admin = ConcertAdmin(Concert, custom_site)
    
  637.         concert_model_admin.search_fields = ["group__name", "name"]
    
  638.         # Reverse FK lookup.
    
  639.         group_model_admin = GroupAdmin(Group, custom_site)
    
  640.         group_model_admin.search_fields = ["name", "concert__name", "members__name"]
    
  641.         for search_string, result_count in (
    
  642.             ("Duo Concert", 1),
    
  643.             ("Tiny Desk Concert", 1),
    
  644.             ("Concert", 2),
    
  645.             ("Other Concert", 0),
    
  646.             ("Duo Woodstock", 0),
    
  647.         ):
    
  648.             with self.subTest(search_string=search_string):
    
  649.                 # FK lookup.
    
  650.                 request = self.factory.get(
    
  651.                     "/concert/", data={SEARCH_VAR: search_string}
    
  652.                 )
    
  653.                 request.user = self.superuser
    
  654.                 concert_changelist = concert_model_admin.get_changelist_instance(
    
  655.                     request
    
  656.                 )
    
  657.                 self.assertEqual(concert_changelist.queryset.count(), result_count)
    
  658.                 # Reverse FK lookup.
    
  659.                 request = self.factory.get("/group/", data={SEARCH_VAR: search_string})
    
  660.                 request.user = self.superuser
    
  661.                 group_changelist = group_model_admin.get_changelist_instance(request)
    
  662.                 self.assertEqual(group_changelist.queryset.count(), result_count)
    
  663.         # Many-to-many lookup.
    
  664.         for search_string, result_count in (
    
  665.             ("Finlayson Duo Tiny", 1),
    
  666.             ("Finlayson", 1),
    
  667.             ("Finlayson Hype", 0),
    
  668.             ("Jonathan Finlayson Duo", 1),
    
  669.             ("Mary Jonathan Duo", 0),
    
  670.             ("Oscar Finlayson Duo", 0),
    
  671.         ):
    
  672.             with self.subTest(search_string=search_string):
    
  673.                 request = self.factory.get("/group/", data={SEARCH_VAR: search_string})
    
  674.                 request.user = self.superuser
    
  675.                 group_changelist = group_model_admin.get_changelist_instance(request)
    
  676.                 self.assertEqual(group_changelist.queryset.count(), result_count)
    
  677. 
    
  678.     def test_pk_in_search_fields(self):
    
  679.         band = Group.objects.create(name="The Hype")
    
  680.         Concert.objects.create(name="Woodstock", group=band)
    
  681. 
    
  682.         m = ConcertAdmin(Concert, custom_site)
    
  683.         m.search_fields = ["group__pk"]
    
  684. 
    
  685.         request = self.factory.get("/concert/", data={SEARCH_VAR: band.pk})
    
  686.         request.user = self.superuser
    
  687.         cl = m.get_changelist_instance(request)
    
  688.         self.assertEqual(cl.queryset.count(), 1)
    
  689. 
    
  690.         request = self.factory.get("/concert/", data={SEARCH_VAR: band.pk + 5})
    
  691.         request.user = self.superuser
    
  692.         cl = m.get_changelist_instance(request)
    
  693.         self.assertEqual(cl.queryset.count(), 0)
    
  694. 
    
  695.     def test_builtin_lookup_in_search_fields(self):
    
  696.         band = Group.objects.create(name="The Hype")
    
  697.         concert = Concert.objects.create(name="Woodstock", group=band)
    
  698. 
    
  699.         m = ConcertAdmin(Concert, custom_site)
    
  700.         m.search_fields = ["name__iexact"]
    
  701. 
    
  702.         request = self.factory.get("/", data={SEARCH_VAR: "woodstock"})
    
  703.         request.user = self.superuser
    
  704.         cl = m.get_changelist_instance(request)
    
  705.         self.assertCountEqual(cl.queryset, [concert])
    
  706. 
    
  707.         request = self.factory.get("/", data={SEARCH_VAR: "wood"})
    
  708.         request.user = self.superuser
    
  709.         cl = m.get_changelist_instance(request)
    
  710.         self.assertCountEqual(cl.queryset, [])
    
  711. 
    
  712.     def test_custom_lookup_in_search_fields(self):
    
  713.         band = Group.objects.create(name="The Hype")
    
  714.         concert = Concert.objects.create(name="Woodstock", group=band)
    
  715. 
    
  716.         m = ConcertAdmin(Concert, custom_site)
    
  717.         m.search_fields = ["group__name__cc"]
    
  718.         with register_lookup(Field, Contains, lookup_name="cc"):
    
  719.             request = self.factory.get("/", data={SEARCH_VAR: "Hype"})
    
  720.             request.user = self.superuser
    
  721.             cl = m.get_changelist_instance(request)
    
  722.             self.assertCountEqual(cl.queryset, [concert])
    
  723. 
    
  724.             request = self.factory.get("/", data={SEARCH_VAR: "Woodstock"})
    
  725.             request.user = self.superuser
    
  726.             cl = m.get_changelist_instance(request)
    
  727.             self.assertCountEqual(cl.queryset, [])
    
  728. 
    
  729.     def test_spanning_relations_with_custom_lookup_in_search_fields(self):
    
  730.         hype = Group.objects.create(name="The Hype")
    
  731.         concert = Concert.objects.create(name="Woodstock", group=hype)
    
  732.         vox = Musician.objects.create(name="Vox", age=20)
    
  733.         Membership.objects.create(music=vox, group=hype)
    
  734.         # Register a custom lookup on IntegerField to ensure that field
    
  735.         # traversing logic in ModelAdmin.get_search_results() works.
    
  736.         with register_lookup(IntegerField, Exact, lookup_name="exactly"):
    
  737.             m = ConcertAdmin(Concert, custom_site)
    
  738.             m.search_fields = ["group__members__age__exactly"]
    
  739. 
    
  740.             request = self.factory.get("/", data={SEARCH_VAR: "20"})
    
  741.             request.user = self.superuser
    
  742.             cl = m.get_changelist_instance(request)
    
  743.             self.assertCountEqual(cl.queryset, [concert])
    
  744. 
    
  745.             request = self.factory.get("/", data={SEARCH_VAR: "21"})
    
  746.             request.user = self.superuser
    
  747.             cl = m.get_changelist_instance(request)
    
  748.             self.assertCountEqual(cl.queryset, [])
    
  749. 
    
  750.     def test_custom_lookup_with_pk_shortcut(self):
    
  751.         self.assertEqual(CharPK._meta.pk.name, "char_pk")  # Not equal to 'pk'.
    
  752.         m = admin.ModelAdmin(CustomIdUser, custom_site)
    
  753. 
    
  754.         abc = CharPK.objects.create(char_pk="abc")
    
  755.         abcd = CharPK.objects.create(char_pk="abcd")
    
  756.         m = admin.ModelAdmin(CharPK, custom_site)
    
  757.         m.search_fields = ["pk__exact"]
    
  758. 
    
  759.         request = self.factory.get("/", data={SEARCH_VAR: "abc"})
    
  760.         request.user = self.superuser
    
  761.         cl = m.get_changelist_instance(request)
    
  762.         self.assertCountEqual(cl.queryset, [abc])
    
  763. 
    
  764.         request = self.factory.get("/", data={SEARCH_VAR: "abcd"})
    
  765.         request.user = self.superuser
    
  766.         cl = m.get_changelist_instance(request)
    
  767.         self.assertCountEqual(cl.queryset, [abcd])
    
  768. 
    
  769.     def test_no_exists_for_m2m_in_list_filter_without_params(self):
    
  770.         """
    
  771.         If a ManyToManyField is in list_filter but isn't in any lookup params,
    
  772.         the changelist's query shouldn't have Exists().
    
  773.         """
    
  774.         m = BandAdmin(Band, custom_site)
    
  775.         for lookup_params in ({}, {"name": "test"}):
    
  776.             request = self.factory.get("/band/", lookup_params)
    
  777.             request.user = self.superuser
    
  778.             cl = m.get_changelist_instance(request)
    
  779.             self.assertNotIn(" EXISTS", str(cl.queryset.query))
    
  780. 
    
  781.         # A ManyToManyField in params does have Exists() applied.
    
  782.         request = self.factory.get("/band/", {"genres": "0"})
    
  783.         request.user = self.superuser
    
  784.         cl = m.get_changelist_instance(request)
    
  785.         self.assertIn(" EXISTS", str(cl.queryset.query))
    
  786. 
    
  787.     def test_pagination(self):
    
  788.         """
    
  789.         Regression tests for #12893: Pagination in admins changelist doesn't
    
  790.         use queryset set by modeladmin.
    
  791.         """
    
  792.         parent = Parent.objects.create(name="anything")
    
  793.         for i in range(1, 31):
    
  794.             Child.objects.create(name="name %s" % i, parent=parent)
    
  795.             Child.objects.create(name="filtered %s" % i, parent=parent)
    
  796. 
    
  797.         request = self.factory.get("/child/")
    
  798.         request.user = self.superuser
    
  799. 
    
  800.         # Test default queryset
    
  801.         m = ChildAdmin(Child, custom_site)
    
  802.         cl = m.get_changelist_instance(request)
    
  803.         self.assertEqual(cl.queryset.count(), 60)
    
  804.         self.assertEqual(cl.paginator.count, 60)
    
  805.         self.assertEqual(list(cl.paginator.page_range), [1, 2, 3, 4, 5, 6])
    
  806. 
    
  807.         # Test custom queryset
    
  808.         m = FilteredChildAdmin(Child, custom_site)
    
  809.         cl = m.get_changelist_instance(request)
    
  810.         self.assertEqual(cl.queryset.count(), 30)
    
  811.         self.assertEqual(cl.paginator.count, 30)
    
  812.         self.assertEqual(list(cl.paginator.page_range), [1, 2, 3])
    
  813. 
    
  814.     def test_computed_list_display_localization(self):
    
  815.         """
    
  816.         Regression test for #13196: output of functions should be  localized
    
  817.         in the changelist.
    
  818.         """
    
  819.         self.client.force_login(self.superuser)
    
  820.         event = Event.objects.create(date=datetime.date.today())
    
  821.         response = self.client.get(reverse("admin:admin_changelist_event_changelist"))
    
  822.         self.assertContains(response, formats.localize(event.date))
    
  823.         self.assertNotContains(response, str(event.date))
    
  824. 
    
  825.     def test_dynamic_list_display(self):
    
  826.         """
    
  827.         Regression tests for #14206: dynamic list_display support.
    
  828.         """
    
  829.         parent = Parent.objects.create(name="parent")
    
  830.         for i in range(10):
    
  831.             Child.objects.create(name="child %s" % i, parent=parent)
    
  832. 
    
  833.         user_noparents = self._create_superuser("noparents")
    
  834.         user_parents = self._create_superuser("parents")
    
  835. 
    
  836.         # Test with user 'noparents'
    
  837.         m = custom_site._registry[Child]
    
  838.         request = self._mocked_authenticated_request("/child/", user_noparents)
    
  839.         response = m.changelist_view(request)
    
  840.         self.assertNotContains(response, "Parent object")
    
  841. 
    
  842.         list_display = m.get_list_display(request)
    
  843.         list_display_links = m.get_list_display_links(request, list_display)
    
  844.         self.assertEqual(list_display, ["name", "age"])
    
  845.         self.assertEqual(list_display_links, ["name"])
    
  846. 
    
  847.         # Test with user 'parents'
    
  848.         m = DynamicListDisplayChildAdmin(Child, custom_site)
    
  849.         request = self._mocked_authenticated_request("/child/", user_parents)
    
  850.         response = m.changelist_view(request)
    
  851.         self.assertContains(response, "Parent object")
    
  852. 
    
  853.         custom_site.unregister(Child)
    
  854. 
    
  855.         list_display = m.get_list_display(request)
    
  856.         list_display_links = m.get_list_display_links(request, list_display)
    
  857.         self.assertEqual(list_display, ("parent", "name", "age"))
    
  858.         self.assertEqual(list_display_links, ["parent"])
    
  859. 
    
  860.         # Test default implementation
    
  861.         custom_site.register(Child, ChildAdmin)
    
  862.         m = custom_site._registry[Child]
    
  863.         request = self._mocked_authenticated_request("/child/", user_noparents)
    
  864.         response = m.changelist_view(request)
    
  865.         self.assertContains(response, "Parent object")
    
  866. 
    
  867.     def test_show_all(self):
    
  868.         parent = Parent.objects.create(name="anything")
    
  869.         for i in range(1, 31):
    
  870.             Child.objects.create(name="name %s" % i, parent=parent)
    
  871.             Child.objects.create(name="filtered %s" % i, parent=parent)
    
  872. 
    
  873.         # Add "show all" parameter to request
    
  874.         request = self.factory.get("/child/", data={ALL_VAR: ""})
    
  875.         request.user = self.superuser
    
  876. 
    
  877.         # Test valid "show all" request (number of total objects is under max)
    
  878.         m = ChildAdmin(Child, custom_site)
    
  879.         m.list_max_show_all = 200
    
  880.         # 200 is the max we'll pass to ChangeList
    
  881.         cl = m.get_changelist_instance(request)
    
  882.         cl.get_results(request)
    
  883.         self.assertEqual(len(cl.result_list), 60)
    
  884. 
    
  885.         # Test invalid "show all" request (number of total objects over max)
    
  886.         # falls back to paginated pages
    
  887.         m = ChildAdmin(Child, custom_site)
    
  888.         m.list_max_show_all = 30
    
  889.         # 30 is the max we'll pass to ChangeList for this test
    
  890.         cl = m.get_changelist_instance(request)
    
  891.         cl.get_results(request)
    
  892.         self.assertEqual(len(cl.result_list), 10)
    
  893. 
    
  894.     def test_dynamic_list_display_links(self):
    
  895.         """
    
  896.         Regression tests for #16257: dynamic list_display_links support.
    
  897.         """
    
  898.         parent = Parent.objects.create(name="parent")
    
  899.         for i in range(1, 10):
    
  900.             Child.objects.create(id=i, name="child %s" % i, parent=parent, age=i)
    
  901. 
    
  902.         m = DynamicListDisplayLinksChildAdmin(Child, custom_site)
    
  903.         superuser = self._create_superuser("superuser")
    
  904.         request = self._mocked_authenticated_request("/child/", superuser)
    
  905.         response = m.changelist_view(request)
    
  906.         for i in range(1, 10):
    
  907.             link = reverse("admin:admin_changelist_child_change", args=(i,))
    
  908.             self.assertContains(response, '<a href="%s">%s</a>' % (link, i))
    
  909. 
    
  910.         list_display = m.get_list_display(request)
    
  911.         list_display_links = m.get_list_display_links(request, list_display)
    
  912.         self.assertEqual(list_display, ("parent", "name", "age"))
    
  913.         self.assertEqual(list_display_links, ["age"])
    
  914. 
    
  915.     def test_no_list_display_links(self):
    
  916.         """#15185 -- Allow no links from the 'change list' view grid."""
    
  917.         p = Parent.objects.create(name="parent")
    
  918.         m = NoListDisplayLinksParentAdmin(Parent, custom_site)
    
  919.         superuser = self._create_superuser("superuser")
    
  920.         request = self._mocked_authenticated_request("/parent/", superuser)
    
  921.         response = m.changelist_view(request)
    
  922.         link = reverse("admin:admin_changelist_parent_change", args=(p.pk,))
    
  923.         self.assertNotContains(response, '<a href="%s">' % link)
    
  924. 
    
  925.     def test_clear_all_filters_link(self):
    
  926.         self.client.force_login(self.superuser)
    
  927.         url = reverse("admin:auth_user_changelist")
    
  928.         response = self.client.get(url)
    
  929.         self.assertNotContains(response, "&#10006; Clear all filters")
    
  930.         link = '<a href="%s">&#10006; Clear all filters</a>'
    
  931.         for data, href in (
    
  932.             ({"is_staff__exact": "0"}, "?"),
    
  933.             (
    
  934.                 {"is_staff__exact": "0", "username__startswith": "test"},
    
  935.                 "?username__startswith=test",
    
  936.             ),
    
  937.             (
    
  938.                 {"is_staff__exact": "0", SEARCH_VAR: "test"},
    
  939.                 "?%s=test" % SEARCH_VAR,
    
  940.             ),
    
  941.             (
    
  942.                 {"is_staff__exact": "0", IS_POPUP_VAR: "id"},
    
  943.                 "?%s=id" % IS_POPUP_VAR,
    
  944.             ),
    
  945.         ):
    
  946.             with self.subTest(data=data):
    
  947.                 response = self.client.get(url, data=data)
    
  948.                 self.assertContains(response, link % href)
    
  949. 
    
  950.     def test_clear_all_filters_link_callable_filter(self):
    
  951.         self.client.force_login(self.superuser)
    
  952.         url = reverse("admin:admin_changelist_band_changelist")
    
  953.         response = self.client.get(url)
    
  954.         self.assertNotContains(response, "&#10006; Clear all filters")
    
  955.         link = '<a href="%s">&#10006; Clear all filters</a>'
    
  956.         for data, href in (
    
  957.             ({"nr_of_members_partition": "5"}, "?"),
    
  958.             (
    
  959.                 {"nr_of_members_partition": "more", "name__startswith": "test"},
    
  960.                 "?name__startswith=test",
    
  961.             ),
    
  962.             (
    
  963.                 {"nr_of_members_partition": "5", IS_POPUP_VAR: "id"},
    
  964.                 "?%s=id" % IS_POPUP_VAR,
    
  965.             ),
    
  966.         ):
    
  967.             with self.subTest(data=data):
    
  968.                 response = self.client.get(url, data=data)
    
  969.                 self.assertContains(response, link % href)
    
  970. 
    
  971.     def test_no_clear_all_filters_link(self):
    
  972.         self.client.force_login(self.superuser)
    
  973.         url = reverse("admin:auth_user_changelist")
    
  974.         link = ">&#10006; Clear all filters</a>"
    
  975.         for data in (
    
  976.             {SEARCH_VAR: "test"},
    
  977.             {ORDER_VAR: "-1"},
    
  978.             {TO_FIELD_VAR: "id"},
    
  979.             {PAGE_VAR: "1"},
    
  980.             {IS_POPUP_VAR: "1"},
    
  981.             {"username__startswith": "test"},
    
  982.         ):
    
  983.             with self.subTest(data=data):
    
  984.                 response = self.client.get(url, data=data)
    
  985.                 self.assertNotContains(response, link)
    
  986. 
    
  987.     def test_tuple_list_display(self):
    
  988.         swallow = Swallow.objects.create(origin="Africa", load="12.34", speed="22.2")
    
  989.         swallow2 = Swallow.objects.create(origin="Africa", load="12.34", speed="22.2")
    
  990.         swallow_o2o = SwallowOneToOne.objects.create(swallow=swallow2)
    
  991. 
    
  992.         model_admin = SwallowAdmin(Swallow, custom_site)
    
  993.         superuser = self._create_superuser("superuser")
    
  994.         request = self._mocked_authenticated_request("/swallow/", superuser)
    
  995.         response = model_admin.changelist_view(request)
    
  996.         # just want to ensure it doesn't blow up during rendering
    
  997.         self.assertContains(response, str(swallow.origin))
    
  998.         self.assertContains(response, str(swallow.load))
    
  999.         self.assertContains(response, str(swallow.speed))
    
  1000.         # Reverse one-to-one relations should work.
    
  1001.         self.assertContains(response, '<td class="field-swallowonetoone">-</td>')
    
  1002.         self.assertContains(
    
  1003.             response, '<td class="field-swallowonetoone">%s</td>' % swallow_o2o
    
  1004.         )
    
  1005. 
    
  1006.     def test_multiuser_edit(self):
    
  1007.         """
    
  1008.         Simultaneous edits of list_editable fields on the changelist by
    
  1009.         different users must not result in one user's edits creating a new
    
  1010.         object instead of modifying the correct existing object (#11313).
    
  1011.         """
    
  1012.         # To replicate this issue, simulate the following steps:
    
  1013.         # 1. User1 opens an admin changelist with list_editable fields.
    
  1014.         # 2. User2 edits object "Foo" such that it moves to another page in
    
  1015.         #    the pagination order and saves.
    
  1016.         # 3. User1 edits object "Foo" and saves.
    
  1017.         # 4. The edit made by User1 does not get applied to object "Foo" but
    
  1018.         #    instead is used to create a new object (bug).
    
  1019. 
    
  1020.         # For this test, order the changelist by the 'speed' attribute and
    
  1021.         # display 3 objects per page (SwallowAdmin.list_per_page = 3).
    
  1022. 
    
  1023.         # Setup the test to reflect the DB state after step 2 where User2 has
    
  1024.         # edited the first swallow object's speed from '4' to '1'.
    
  1025.         a = Swallow.objects.create(origin="Swallow A", load=4, speed=1)
    
  1026.         b = Swallow.objects.create(origin="Swallow B", load=2, speed=2)
    
  1027.         c = Swallow.objects.create(origin="Swallow C", load=5, speed=5)
    
  1028.         d = Swallow.objects.create(origin="Swallow D", load=9, speed=9)
    
  1029. 
    
  1030.         superuser = self._create_superuser("superuser")
    
  1031.         self.client.force_login(superuser)
    
  1032.         changelist_url = reverse("admin:admin_changelist_swallow_changelist")
    
  1033. 
    
  1034.         # Send the POST from User1 for step 3. It's still using the changelist
    
  1035.         # ordering from before User2's edits in step 2.
    
  1036.         data = {
    
  1037.             "form-TOTAL_FORMS": "3",
    
  1038.             "form-INITIAL_FORMS": "3",
    
  1039.             "form-MIN_NUM_FORMS": "0",
    
  1040.             "form-MAX_NUM_FORMS": "1000",
    
  1041.             "form-0-uuid": str(d.pk),
    
  1042.             "form-1-uuid": str(c.pk),
    
  1043.             "form-2-uuid": str(a.pk),
    
  1044.             "form-0-load": "9.0",
    
  1045.             "form-0-speed": "9.0",
    
  1046.             "form-1-load": "5.0",
    
  1047.             "form-1-speed": "5.0",
    
  1048.             "form-2-load": "5.0",
    
  1049.             "form-2-speed": "4.0",
    
  1050.             "_save": "Save",
    
  1051.         }
    
  1052.         response = self.client.post(
    
  1053.             changelist_url, data, follow=True, extra={"o": "-2"}
    
  1054.         )
    
  1055. 
    
  1056.         # The object User1 edited in step 3 is displayed on the changelist and
    
  1057.         # has the correct edits applied.
    
  1058.         self.assertContains(response, "1 swallow was changed successfully.")
    
  1059.         self.assertContains(response, a.origin)
    
  1060.         a.refresh_from_db()
    
  1061.         self.assertEqual(a.load, float(data["form-2-load"]))
    
  1062.         self.assertEqual(a.speed, float(data["form-2-speed"]))
    
  1063.         b.refresh_from_db()
    
  1064.         self.assertEqual(b.load, 2)
    
  1065.         self.assertEqual(b.speed, 2)
    
  1066.         c.refresh_from_db()
    
  1067.         self.assertEqual(c.load, float(data["form-1-load"]))
    
  1068.         self.assertEqual(c.speed, float(data["form-1-speed"]))
    
  1069.         d.refresh_from_db()
    
  1070.         self.assertEqual(d.load, float(data["form-0-load"]))
    
  1071.         self.assertEqual(d.speed, float(data["form-0-speed"]))
    
  1072.         # No new swallows were created.
    
  1073.         self.assertEqual(len(Swallow.objects.all()), 4)
    
  1074. 
    
  1075.     def test_get_edited_object_ids(self):
    
  1076.         a = Swallow.objects.create(origin="Swallow A", load=4, speed=1)
    
  1077.         b = Swallow.objects.create(origin="Swallow B", load=2, speed=2)
    
  1078.         c = Swallow.objects.create(origin="Swallow C", load=5, speed=5)
    
  1079.         superuser = self._create_superuser("superuser")
    
  1080.         self.client.force_login(superuser)
    
  1081.         changelist_url = reverse("admin:admin_changelist_swallow_changelist")
    
  1082.         m = SwallowAdmin(Swallow, custom_site)
    
  1083.         data = {
    
  1084.             "form-TOTAL_FORMS": "3",
    
  1085.             "form-INITIAL_FORMS": "3",
    
  1086.             "form-MIN_NUM_FORMS": "0",
    
  1087.             "form-MAX_NUM_FORMS": "1000",
    
  1088.             "form-0-uuid": str(a.pk),
    
  1089.             "form-1-uuid": str(b.pk),
    
  1090.             "form-2-uuid": str(c.pk),
    
  1091.             "form-0-load": "9.0",
    
  1092.             "form-0-speed": "9.0",
    
  1093.             "form-1-load": "5.0",
    
  1094.             "form-1-speed": "5.0",
    
  1095.             "form-2-load": "5.0",
    
  1096.             "form-2-speed": "4.0",
    
  1097.             "_save": "Save",
    
  1098.         }
    
  1099.         request = self.factory.post(changelist_url, data=data)
    
  1100.         pks = m._get_edited_object_pks(request, prefix="form")
    
  1101.         self.assertEqual(sorted(pks), sorted([str(a.pk), str(b.pk), str(c.pk)]))
    
  1102. 
    
  1103.     def test_get_list_editable_queryset(self):
    
  1104.         a = Swallow.objects.create(origin="Swallow A", load=4, speed=1)
    
  1105.         Swallow.objects.create(origin="Swallow B", load=2, speed=2)
    
  1106.         data = {
    
  1107.             "form-TOTAL_FORMS": "2",
    
  1108.             "form-INITIAL_FORMS": "2",
    
  1109.             "form-MIN_NUM_FORMS": "0",
    
  1110.             "form-MAX_NUM_FORMS": "1000",
    
  1111.             "form-0-uuid": str(a.pk),
    
  1112.             "form-0-load": "10",
    
  1113.             "_save": "Save",
    
  1114.         }
    
  1115.         superuser = self._create_superuser("superuser")
    
  1116.         self.client.force_login(superuser)
    
  1117.         changelist_url = reverse("admin:admin_changelist_swallow_changelist")
    
  1118.         m = SwallowAdmin(Swallow, custom_site)
    
  1119.         request = self.factory.post(changelist_url, data=data)
    
  1120.         queryset = m._get_list_editable_queryset(request, prefix="form")
    
  1121.         self.assertEqual(queryset.count(), 1)
    
  1122.         data["form-0-uuid"] = "INVALD_PRIMARY_KEY"
    
  1123.         # The unfiltered queryset is returned if there's invalid data.
    
  1124.         request = self.factory.post(changelist_url, data=data)
    
  1125.         queryset = m._get_list_editable_queryset(request, prefix="form")
    
  1126.         self.assertEqual(queryset.count(), 2)
    
  1127. 
    
  1128.     def test_get_list_editable_queryset_with_regex_chars_in_prefix(self):
    
  1129.         a = Swallow.objects.create(origin="Swallow A", load=4, speed=1)
    
  1130.         Swallow.objects.create(origin="Swallow B", load=2, speed=2)
    
  1131.         data = {
    
  1132.             "form$-TOTAL_FORMS": "2",
    
  1133.             "form$-INITIAL_FORMS": "2",
    
  1134.             "form$-MIN_NUM_FORMS": "0",
    
  1135.             "form$-MAX_NUM_FORMS": "1000",
    
  1136.             "form$-0-uuid": str(a.pk),
    
  1137.             "form$-0-load": "10",
    
  1138.             "_save": "Save",
    
  1139.         }
    
  1140.         superuser = self._create_superuser("superuser")
    
  1141.         self.client.force_login(superuser)
    
  1142.         changelist_url = reverse("admin:admin_changelist_swallow_changelist")
    
  1143.         m = SwallowAdmin(Swallow, custom_site)
    
  1144.         request = self.factory.post(changelist_url, data=data)
    
  1145.         queryset = m._get_list_editable_queryset(request, prefix="form$")
    
  1146.         self.assertEqual(queryset.count(), 1)
    
  1147. 
    
  1148.     def test_changelist_view_list_editable_changed_objects_uses_filter(self):
    
  1149.         """list_editable edits use a filtered queryset to limit memory usage."""
    
  1150.         a = Swallow.objects.create(origin="Swallow A", load=4, speed=1)
    
  1151.         Swallow.objects.create(origin="Swallow B", load=2, speed=2)
    
  1152.         data = {
    
  1153.             "form-TOTAL_FORMS": "2",
    
  1154.             "form-INITIAL_FORMS": "2",
    
  1155.             "form-MIN_NUM_FORMS": "0",
    
  1156.             "form-MAX_NUM_FORMS": "1000",
    
  1157.             "form-0-uuid": str(a.pk),
    
  1158.             "form-0-load": "10",
    
  1159.             "_save": "Save",
    
  1160.         }
    
  1161.         superuser = self._create_superuser("superuser")
    
  1162.         self.client.force_login(superuser)
    
  1163.         changelist_url = reverse("admin:admin_changelist_swallow_changelist")
    
  1164.         with CaptureQueriesContext(connection) as context:
    
  1165.             response = self.client.post(changelist_url, data=data)
    
  1166.             self.assertEqual(response.status_code, 200)
    
  1167.             self.assertIn("WHERE", context.captured_queries[4]["sql"])
    
  1168.             self.assertIn("IN", context.captured_queries[4]["sql"])
    
  1169.             # Check only the first few characters since the UUID may have dashes.
    
  1170.             self.assertIn(str(a.pk)[:8], context.captured_queries[4]["sql"])
    
  1171. 
    
  1172.     def test_deterministic_order_for_unordered_model(self):
    
  1173.         """
    
  1174.         The primary key is used in the ordering of the changelist's results to
    
  1175.         guarantee a deterministic order, even when the model doesn't have any
    
  1176.         default ordering defined (#17198).
    
  1177.         """
    
  1178.         superuser = self._create_superuser("superuser")
    
  1179. 
    
  1180.         for counter in range(1, 51):
    
  1181.             UnorderedObject.objects.create(id=counter, bool=True)
    
  1182. 
    
  1183.         class UnorderedObjectAdmin(admin.ModelAdmin):
    
  1184.             list_per_page = 10
    
  1185. 
    
  1186.         def check_results_order(ascending=False):
    
  1187.             custom_site.register(UnorderedObject, UnorderedObjectAdmin)
    
  1188.             model_admin = UnorderedObjectAdmin(UnorderedObject, custom_site)
    
  1189.             counter = 0 if ascending else 51
    
  1190.             for page in range(1, 6):
    
  1191.                 request = self._mocked_authenticated_request(
    
  1192.                     "/unorderedobject/?p=%s" % page, superuser
    
  1193.                 )
    
  1194.                 response = model_admin.changelist_view(request)
    
  1195.                 for result in response.context_data["cl"].result_list:
    
  1196.                     counter += 1 if ascending else -1
    
  1197.                     self.assertEqual(result.id, counter)
    
  1198.             custom_site.unregister(UnorderedObject)
    
  1199. 
    
  1200.         # When no order is defined at all, everything is ordered by '-pk'.
    
  1201.         check_results_order()
    
  1202. 
    
  1203.         # When an order field is defined but multiple records have the same
    
  1204.         # value for that field, make sure everything gets ordered by -pk as well.
    
  1205.         UnorderedObjectAdmin.ordering = ["bool"]
    
  1206.         check_results_order()
    
  1207. 
    
  1208.         # When order fields are defined, including the pk itself, use them.
    
  1209.         UnorderedObjectAdmin.ordering = ["bool", "-pk"]
    
  1210.         check_results_order()
    
  1211.         UnorderedObjectAdmin.ordering = ["bool", "pk"]
    
  1212.         check_results_order(ascending=True)
    
  1213.         UnorderedObjectAdmin.ordering = ["-id", "bool"]
    
  1214.         check_results_order()
    
  1215.         UnorderedObjectAdmin.ordering = ["id", "bool"]
    
  1216.         check_results_order(ascending=True)
    
  1217. 
    
  1218.     def test_deterministic_order_for_model_ordered_by_its_manager(self):
    
  1219.         """
    
  1220.         The primary key is used in the ordering of the changelist's results to
    
  1221.         guarantee a deterministic order, even when the model has a manager that
    
  1222.         defines a default ordering (#17198).
    
  1223.         """
    
  1224.         superuser = self._create_superuser("superuser")
    
  1225. 
    
  1226.         for counter in range(1, 51):
    
  1227.             OrderedObject.objects.create(id=counter, bool=True, number=counter)
    
  1228. 
    
  1229.         class OrderedObjectAdmin(admin.ModelAdmin):
    
  1230.             list_per_page = 10
    
  1231. 
    
  1232.         def check_results_order(ascending=False):
    
  1233.             custom_site.register(OrderedObject, OrderedObjectAdmin)
    
  1234.             model_admin = OrderedObjectAdmin(OrderedObject, custom_site)
    
  1235.             counter = 0 if ascending else 51
    
  1236.             for page in range(1, 6):
    
  1237.                 request = self._mocked_authenticated_request(
    
  1238.                     "/orderedobject/?p=%s" % page, superuser
    
  1239.                 )
    
  1240.                 response = model_admin.changelist_view(request)
    
  1241.                 for result in response.context_data["cl"].result_list:
    
  1242.                     counter += 1 if ascending else -1
    
  1243.                     self.assertEqual(result.id, counter)
    
  1244.             custom_site.unregister(OrderedObject)
    
  1245. 
    
  1246.         # When no order is defined at all, use the model's default ordering
    
  1247.         # (i.e. 'number').
    
  1248.         check_results_order(ascending=True)
    
  1249. 
    
  1250.         # When an order field is defined but multiple records have the same
    
  1251.         # value for that field, make sure everything gets ordered by -pk as well.
    
  1252.         OrderedObjectAdmin.ordering = ["bool"]
    
  1253.         check_results_order()
    
  1254. 
    
  1255.         # When order fields are defined, including the pk itself, use them.
    
  1256.         OrderedObjectAdmin.ordering = ["bool", "-pk"]
    
  1257.         check_results_order()
    
  1258.         OrderedObjectAdmin.ordering = ["bool", "pk"]
    
  1259.         check_results_order(ascending=True)
    
  1260.         OrderedObjectAdmin.ordering = ["-id", "bool"]
    
  1261.         check_results_order()
    
  1262.         OrderedObjectAdmin.ordering = ["id", "bool"]
    
  1263.         check_results_order(ascending=True)
    
  1264. 
    
  1265.     @isolate_apps("admin_changelist")
    
  1266.     def test_total_ordering_optimization(self):
    
  1267.         class Related(models.Model):
    
  1268.             unique_field = models.BooleanField(unique=True)
    
  1269. 
    
  1270.             class Meta:
    
  1271.                 ordering = ("unique_field",)
    
  1272. 
    
  1273.         class Model(models.Model):
    
  1274.             unique_field = models.BooleanField(unique=True)
    
  1275.             unique_nullable_field = models.BooleanField(unique=True, null=True)
    
  1276.             related = models.ForeignKey(Related, models.CASCADE)
    
  1277.             other_related = models.ForeignKey(Related, models.CASCADE)
    
  1278.             related_unique = models.OneToOneField(Related, models.CASCADE)
    
  1279.             field = models.BooleanField()
    
  1280.             other_field = models.BooleanField()
    
  1281.             null_field = models.BooleanField(null=True)
    
  1282. 
    
  1283.             class Meta:
    
  1284.                 unique_together = {
    
  1285.                     ("field", "other_field"),
    
  1286.                     ("field", "null_field"),
    
  1287.                     ("related", "other_related_id"),
    
  1288.                 }
    
  1289. 
    
  1290.         class ModelAdmin(admin.ModelAdmin):
    
  1291.             def get_queryset(self, request):
    
  1292.                 return Model.objects.none()
    
  1293. 
    
  1294.         request = self._mocked_authenticated_request("/", self.superuser)
    
  1295.         site = admin.AdminSite(name="admin")
    
  1296.         model_admin = ModelAdmin(Model, site)
    
  1297.         change_list = model_admin.get_changelist_instance(request)
    
  1298.         tests = (
    
  1299.             ([], ["-pk"]),
    
  1300.             # Unique non-nullable field.
    
  1301.             (["unique_field"], ["unique_field"]),
    
  1302.             (["-unique_field"], ["-unique_field"]),
    
  1303.             # Unique nullable field.
    
  1304.             (["unique_nullable_field"], ["unique_nullable_field", "-pk"]),
    
  1305.             # Field.
    
  1306.             (["field"], ["field", "-pk"]),
    
  1307.             # Related field introspection is not implemented.
    
  1308.             (["related__unique_field"], ["related__unique_field", "-pk"]),
    
  1309.             # Related attname unique.
    
  1310.             (["related_unique_id"], ["related_unique_id"]),
    
  1311.             # Related ordering introspection is not implemented.
    
  1312.             (["related_unique"], ["related_unique", "-pk"]),
    
  1313.             # Composite unique.
    
  1314.             (["field", "-other_field"], ["field", "-other_field"]),
    
  1315.             # Composite unique nullable.
    
  1316.             (["-field", "null_field"], ["-field", "null_field", "-pk"]),
    
  1317.             # Composite unique and nullable.
    
  1318.             (
    
  1319.                 ["-field", "null_field", "other_field"],
    
  1320.                 ["-field", "null_field", "other_field"],
    
  1321.             ),
    
  1322.             # Composite unique attnames.
    
  1323.             (["related_id", "-other_related_id"], ["related_id", "-other_related_id"]),
    
  1324.             # Composite unique names.
    
  1325.             (["related", "-other_related_id"], ["related", "-other_related_id", "-pk"]),
    
  1326.         )
    
  1327.         # F() objects composite unique.
    
  1328.         total_ordering = [F("field"), F("other_field").desc(nulls_last=True)]
    
  1329.         # F() objects composite unique nullable.
    
  1330.         non_total_ordering = [F("field"), F("null_field").desc(nulls_last=True)]
    
  1331.         tests += (
    
  1332.             (total_ordering, total_ordering),
    
  1333.             (non_total_ordering, non_total_ordering + ["-pk"]),
    
  1334.         )
    
  1335.         for ordering, expected in tests:
    
  1336.             with self.subTest(ordering=ordering):
    
  1337.                 self.assertEqual(
    
  1338.                     change_list._get_deterministic_ordering(ordering), expected
    
  1339.                 )
    
  1340. 
    
  1341.     @isolate_apps("admin_changelist")
    
  1342.     def test_total_ordering_optimization_meta_constraints(self):
    
  1343.         class Related(models.Model):
    
  1344.             unique_field = models.BooleanField(unique=True)
    
  1345. 
    
  1346.             class Meta:
    
  1347.                 ordering = ("unique_field",)
    
  1348. 
    
  1349.         class Model(models.Model):
    
  1350.             field_1 = models.BooleanField()
    
  1351.             field_2 = models.BooleanField()
    
  1352.             field_3 = models.BooleanField()
    
  1353.             field_4 = models.BooleanField()
    
  1354.             field_5 = models.BooleanField()
    
  1355.             field_6 = models.BooleanField()
    
  1356.             nullable_1 = models.BooleanField(null=True)
    
  1357.             nullable_2 = models.BooleanField(null=True)
    
  1358.             related_1 = models.ForeignKey(Related, models.CASCADE)
    
  1359.             related_2 = models.ForeignKey(Related, models.CASCADE)
    
  1360.             related_3 = models.ForeignKey(Related, models.CASCADE)
    
  1361.             related_4 = models.ForeignKey(Related, models.CASCADE)
    
  1362. 
    
  1363.             class Meta:
    
  1364.                 constraints = [
    
  1365.                     *[
    
  1366.                         models.UniqueConstraint(fields=fields, name="".join(fields))
    
  1367.                         for fields in (
    
  1368.                             ["field_1"],
    
  1369.                             ["nullable_1"],
    
  1370.                             ["related_1"],
    
  1371.                             ["related_2_id"],
    
  1372.                             ["field_2", "field_3"],
    
  1373.                             ["field_2", "nullable_2"],
    
  1374.                             ["field_2", "related_3"],
    
  1375.                             ["field_3", "related_4_id"],
    
  1376.                         )
    
  1377.                     ],
    
  1378.                     models.CheckConstraint(check=models.Q(id__gt=0), name="foo"),
    
  1379.                     models.UniqueConstraint(
    
  1380.                         fields=["field_5"],
    
  1381.                         condition=models.Q(id__gt=10),
    
  1382.                         name="total_ordering_1",
    
  1383.                     ),
    
  1384.                     models.UniqueConstraint(
    
  1385.                         fields=["field_6"],
    
  1386.                         condition=models.Q(),
    
  1387.                         name="total_ordering",
    
  1388.                     ),
    
  1389.                 ]
    
  1390. 
    
  1391.         class ModelAdmin(admin.ModelAdmin):
    
  1392.             def get_queryset(self, request):
    
  1393.                 return Model.objects.none()
    
  1394. 
    
  1395.         request = self._mocked_authenticated_request("/", self.superuser)
    
  1396.         site = admin.AdminSite(name="admin")
    
  1397.         model_admin = ModelAdmin(Model, site)
    
  1398.         change_list = model_admin.get_changelist_instance(request)
    
  1399.         tests = (
    
  1400.             # Unique non-nullable field.
    
  1401.             (["field_1"], ["field_1"]),
    
  1402.             # Unique nullable field.
    
  1403.             (["nullable_1"], ["nullable_1", "-pk"]),
    
  1404.             # Related attname unique.
    
  1405.             (["related_1_id"], ["related_1_id"]),
    
  1406.             (["related_2_id"], ["related_2_id"]),
    
  1407.             # Related ordering introspection is not implemented.
    
  1408.             (["related_1"], ["related_1", "-pk"]),
    
  1409.             # Composite unique.
    
  1410.             (["-field_2", "field_3"], ["-field_2", "field_3"]),
    
  1411.             # Composite unique nullable.
    
  1412.             (["field_2", "-nullable_2"], ["field_2", "-nullable_2", "-pk"]),
    
  1413.             # Composite unique and nullable.
    
  1414.             (
    
  1415.                 ["field_2", "-nullable_2", "field_3"],
    
  1416.                 ["field_2", "-nullable_2", "field_3"],
    
  1417.             ),
    
  1418.             # Composite field and related field name.
    
  1419.             (["field_2", "-related_3"], ["field_2", "-related_3", "-pk"]),
    
  1420.             (["field_3", "related_4"], ["field_3", "related_4", "-pk"]),
    
  1421.             # Composite field and related field attname.
    
  1422.             (["field_2", "related_3_id"], ["field_2", "related_3_id"]),
    
  1423.             (["field_3", "-related_4_id"], ["field_3", "-related_4_id"]),
    
  1424.             # Partial unique constraint is ignored.
    
  1425.             (["field_5"], ["field_5", "-pk"]),
    
  1426.             # Unique constraint with an empty condition.
    
  1427.             (["field_6"], ["field_6"]),
    
  1428.         )
    
  1429.         for ordering, expected in tests:
    
  1430.             with self.subTest(ordering=ordering):
    
  1431.                 self.assertEqual(
    
  1432.                     change_list._get_deterministic_ordering(ordering), expected
    
  1433.                 )
    
  1434. 
    
  1435.     def test_dynamic_list_filter(self):
    
  1436.         """
    
  1437.         Regression tests for ticket #17646: dynamic list_filter support.
    
  1438.         """
    
  1439.         parent = Parent.objects.create(name="parent")
    
  1440.         for i in range(10):
    
  1441.             Child.objects.create(name="child %s" % i, parent=parent)
    
  1442. 
    
  1443.         user_noparents = self._create_superuser("noparents")
    
  1444.         user_parents = self._create_superuser("parents")
    
  1445. 
    
  1446.         # Test with user 'noparents'
    
  1447.         m = DynamicListFilterChildAdmin(Child, custom_site)
    
  1448.         request = self._mocked_authenticated_request("/child/", user_noparents)
    
  1449.         response = m.changelist_view(request)
    
  1450.         self.assertEqual(response.context_data["cl"].list_filter, ["name", "age"])
    
  1451. 
    
  1452.         # Test with user 'parents'
    
  1453.         m = DynamicListFilterChildAdmin(Child, custom_site)
    
  1454.         request = self._mocked_authenticated_request("/child/", user_parents)
    
  1455.         response = m.changelist_view(request)
    
  1456.         self.assertEqual(
    
  1457.             response.context_data["cl"].list_filter, ("parent", "name", "age")
    
  1458.         )
    
  1459. 
    
  1460.     def test_dynamic_search_fields(self):
    
  1461.         child = self._create_superuser("child")
    
  1462.         m = DynamicSearchFieldsChildAdmin(Child, custom_site)
    
  1463.         request = self._mocked_authenticated_request("/child/", child)
    
  1464.         response = m.changelist_view(request)
    
  1465.         self.assertEqual(response.context_data["cl"].search_fields, ("name", "age"))
    
  1466. 
    
  1467.     def test_pagination_page_range(self):
    
  1468.         """
    
  1469.         Regression tests for ticket #15653: ensure the number of pages
    
  1470.         generated for changelist views are correct.
    
  1471.         """
    
  1472.         # instantiating and setting up ChangeList object
    
  1473.         m = GroupAdmin(Group, custom_site)
    
  1474.         request = self.factory.get("/group/")
    
  1475.         request.user = self.superuser
    
  1476.         cl = m.get_changelist_instance(request)
    
  1477.         cl.list_per_page = 10
    
  1478. 
    
  1479.         ELLIPSIS = cl.paginator.ELLIPSIS
    
  1480.         for number, pages, expected in [
    
  1481.             (1, 1, []),
    
  1482.             (1, 2, [1, 2]),
    
  1483.             (6, 11, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]),
    
  1484.             (6, 12, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]),
    
  1485.             (6, 13, [1, 2, 3, 4, 5, 6, 7, 8, 9, ELLIPSIS, 12, 13]),
    
  1486.             (7, 12, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]),
    
  1487.             (7, 13, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]),
    
  1488.             (7, 14, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ELLIPSIS, 13, 14]),
    
  1489.             (8, 13, [1, 2, ELLIPSIS, 5, 6, 7, 8, 9, 10, 11, 12, 13]),
    
  1490.             (8, 14, [1, 2, ELLIPSIS, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]),
    
  1491.             (8, 15, [1, 2, ELLIPSIS, 5, 6, 7, 8, 9, 10, 11, ELLIPSIS, 14, 15]),
    
  1492.         ]:
    
  1493.             with self.subTest(number=number, pages=pages):
    
  1494.                 # assuming exactly `pages * cl.list_per_page` objects
    
  1495.                 Group.objects.all().delete()
    
  1496.                 for i in range(pages * cl.list_per_page):
    
  1497.                     Group.objects.create(name="test band")
    
  1498. 
    
  1499.                 # setting page number and calculating page range
    
  1500.                 cl.page_num = number
    
  1501.                 cl.get_results(request)
    
  1502.                 self.assertEqual(list(pagination(cl)["page_range"]), expected)
    
  1503. 
    
  1504.     def test_object_tools_displayed_no_add_permission(self):
    
  1505.         """
    
  1506.         When ModelAdmin.has_add_permission() returns False, the object-tools
    
  1507.         block is still shown.
    
  1508.         """
    
  1509.         superuser = self._create_superuser("superuser")
    
  1510.         m = EventAdmin(Event, custom_site)
    
  1511.         request = self._mocked_authenticated_request("/event/", superuser)
    
  1512.         self.assertFalse(m.has_add_permission(request))
    
  1513.         response = m.changelist_view(request)
    
  1514.         self.assertIn('<ul class="object-tools">', response.rendered_content)
    
  1515.         # The "Add" button inside the object-tools shouldn't appear.
    
  1516.         self.assertNotIn("Add ", response.rendered_content)
    
  1517. 
    
  1518.     def test_search_help_text(self):
    
  1519.         superuser = self._create_superuser("superuser")
    
  1520.         m = BandAdmin(Band, custom_site)
    
  1521.         # search_fields without search_help_text.
    
  1522.         m.search_fields = ["name"]
    
  1523.         request = self._mocked_authenticated_request("/band/", superuser)
    
  1524.         response = m.changelist_view(request)
    
  1525.         self.assertIsNone(response.context_data["cl"].search_help_text)
    
  1526.         self.assertNotContains(response, '<div class="help id="searchbar_helptext">')
    
  1527.         # search_fields with search_help_text.
    
  1528.         m.search_help_text = "Search help text"
    
  1529.         request = self._mocked_authenticated_request("/band/", superuser)
    
  1530.         response = m.changelist_view(request)
    
  1531.         self.assertEqual(
    
  1532.             response.context_data["cl"].search_help_text, "Search help text"
    
  1533.         )
    
  1534.         self.assertContains(
    
  1535.             response, '<div class="help" id="searchbar_helptext">Search help text</div>'
    
  1536.         )
    
  1537.         self.assertContains(
    
  1538.             response,
    
  1539.             '<input type="text" size="40" name="q" value="" id="searchbar" '
    
  1540.             'autofocus aria-describedby="searchbar_helptext">',
    
  1541.         )
    
  1542. 
    
  1543. 
    
  1544. class GetAdminLogTests(TestCase):
    
  1545.     def test_custom_user_pk_not_named_id(self):
    
  1546.         """
    
  1547.         {% get_admin_log %} works if the user model's primary key isn't named
    
  1548.         'id'.
    
  1549.         """
    
  1550.         context = Context({"user": CustomIdUser()})
    
  1551.         template = Template(
    
  1552.             "{% load log %}{% get_admin_log 10 as admin_log for_user user %}"
    
  1553.         )
    
  1554.         # This template tag just logs.
    
  1555.         self.assertEqual(template.render(context), "")
    
  1556. 
    
  1557.     def test_no_user(self):
    
  1558.         """{% get_admin_log %} works without specifying a user."""
    
  1559.         user = User(username="jondoe", password="secret", email="[email protected]")
    
  1560.         user.save()
    
  1561.         ct = ContentType.objects.get_for_model(User)
    
  1562.         LogEntry.objects.log_action(user.pk, ct.pk, user.pk, repr(user), 1)
    
  1563.         t = Template(
    
  1564.             "{% load log %}"
    
  1565.             "{% get_admin_log 100 as admin_log %}"
    
  1566.             "{% for entry in admin_log %}"
    
  1567.             "{{ entry|safe }}"
    
  1568.             "{% endfor %}"
    
  1569.         )
    
  1570.         self.assertEqual(t.render(Context({})), "Added “<User: jondoe>”.")
    
  1571. 
    
  1572.     def test_missing_args(self):
    
  1573.         msg = "'get_admin_log' statements require two arguments"
    
  1574.         with self.assertRaisesMessage(TemplateSyntaxError, msg):
    
  1575.             Template("{% load log %}{% get_admin_log 10 as %}")
    
  1576. 
    
  1577.     def test_non_integer_limit(self):
    
  1578.         msg = "First argument to 'get_admin_log' must be an integer"
    
  1579.         with self.assertRaisesMessage(TemplateSyntaxError, msg):
    
  1580.             Template(
    
  1581.                 '{% load log %}{% get_admin_log "10" as admin_log for_user user %}'
    
  1582.             )
    
  1583. 
    
  1584.     def test_without_as(self):
    
  1585.         msg = "Second argument to 'get_admin_log' must be 'as'"
    
  1586.         with self.assertRaisesMessage(TemplateSyntaxError, msg):
    
  1587.             Template("{% load log %}{% get_admin_log 10 ad admin_log for_user user %}")
    
  1588. 
    
  1589.     def test_without_for_user(self):
    
  1590.         msg = "Fourth argument to 'get_admin_log' must be 'for_user'"
    
  1591.         with self.assertRaisesMessage(TemplateSyntaxError, msg):
    
  1592.             Template("{% load log %}{% get_admin_log 10 as admin_log foruser user %}")
    
  1593. 
    
  1594. 
    
  1595. @override_settings(ROOT_URLCONF="admin_changelist.urls")
    
  1596. class SeleniumTests(AdminSeleniumTestCase):
    
  1597.     available_apps = ["admin_changelist"] + AdminSeleniumTestCase.available_apps
    
  1598. 
    
  1599.     def setUp(self):
    
  1600.         User.objects.create_superuser(username="super", password="secret", email=None)
    
  1601. 
    
  1602.     def test_add_row_selection(self):
    
  1603.         """
    
  1604.         The status line for selected rows gets updated correctly (#22038).
    
  1605.         """
    
  1606.         from selenium.webdriver.common.by import By
    
  1607. 
    
  1608.         self.admin_login(username="super", password="secret")
    
  1609.         self.selenium.get(self.live_server_url + reverse("admin:auth_user_changelist"))
    
  1610. 
    
  1611.         form_id = "#changelist-form"
    
  1612. 
    
  1613.         # Test amount of rows in the Changelist
    
  1614.         rows = self.selenium.find_elements(
    
  1615.             By.CSS_SELECTOR, "%s #result_list tbody tr" % form_id
    
  1616.         )
    
  1617.         self.assertEqual(len(rows), 1)
    
  1618.         row = rows[0]
    
  1619. 
    
  1620.         selection_indicator = self.selenium.find_element(
    
  1621.             By.CSS_SELECTOR, "%s .action-counter" % form_id
    
  1622.         )
    
  1623.         all_selector = self.selenium.find_element(By.ID, "action-toggle")
    
  1624.         row_selector = self.selenium.find_element(
    
  1625.             By.CSS_SELECTOR,
    
  1626.             "%s #result_list tbody tr:first-child .action-select" % form_id,
    
  1627.         )
    
  1628. 
    
  1629.         # Test current selection
    
  1630.         self.assertEqual(selection_indicator.text, "0 of 1 selected")
    
  1631.         self.assertIs(all_selector.get_property("checked"), False)
    
  1632.         self.assertEqual(row.get_attribute("class"), "")
    
  1633. 
    
  1634.         # Select a row and check again
    
  1635.         row_selector.click()
    
  1636.         self.assertEqual(selection_indicator.text, "1 of 1 selected")
    
  1637.         self.assertIs(all_selector.get_property("checked"), True)
    
  1638.         self.assertEqual(row.get_attribute("class"), "selected")
    
  1639. 
    
  1640.         # Deselect a row and check again
    
  1641.         row_selector.click()
    
  1642.         self.assertEqual(selection_indicator.text, "0 of 1 selected")
    
  1643.         self.assertIs(all_selector.get_property("checked"), False)
    
  1644.         self.assertEqual(row.get_attribute("class"), "")
    
  1645. 
    
  1646.     def test_modifier_allows_multiple_section(self):
    
  1647.         """
    
  1648.         Selecting a row and then selecting another row whilst holding shift
    
  1649.         should select all rows in-between.
    
  1650.         """
    
  1651.         from selenium.webdriver.common.action_chains import ActionChains
    
  1652.         from selenium.webdriver.common.by import By
    
  1653.         from selenium.webdriver.common.keys import Keys
    
  1654. 
    
  1655.         Parent.objects.bulk_create([Parent(name="parent%d" % i) for i in range(5)])
    
  1656.         self.admin_login(username="super", password="secret")
    
  1657.         self.selenium.get(
    
  1658.             self.live_server_url + reverse("admin:admin_changelist_parent_changelist")
    
  1659.         )
    
  1660.         checkboxes = self.selenium.find_elements(
    
  1661.             By.CSS_SELECTOR, "tr input.action-select"
    
  1662.         )
    
  1663.         self.assertEqual(len(checkboxes), 5)
    
  1664.         for c in checkboxes:
    
  1665.             self.assertIs(c.get_property("checked"), False)
    
  1666.         # Check first row. Hold-shift and check next-to-last row.
    
  1667.         checkboxes[0].click()
    
  1668.         ActionChains(self.selenium).key_down(Keys.SHIFT).click(checkboxes[-2]).key_up(
    
  1669.             Keys.SHIFT
    
  1670.         ).perform()
    
  1671.         for c in checkboxes[:-2]:
    
  1672.             self.assertIs(c.get_property("checked"), True)
    
  1673.         self.assertIs(checkboxes[-1].get_property("checked"), False)
    
  1674. 
    
  1675.     def test_select_all_across_pages(self):
    
  1676.         from selenium.webdriver.common.by import By
    
  1677. 
    
  1678.         Parent.objects.bulk_create([Parent(name="parent%d" % i) for i in range(101)])
    
  1679.         self.admin_login(username="super", password="secret")
    
  1680.         self.selenium.get(
    
  1681.             self.live_server_url + reverse("admin:admin_changelist_parent_changelist")
    
  1682.         )
    
  1683. 
    
  1684.         selection_indicator = self.selenium.find_element(
    
  1685.             By.CSS_SELECTOR, ".action-counter"
    
  1686.         )
    
  1687.         select_all_indicator = self.selenium.find_element(
    
  1688.             By.CSS_SELECTOR, ".actions .all"
    
  1689.         )
    
  1690.         question = self.selenium.find_element(By.CSS_SELECTOR, ".actions > .question")
    
  1691.         clear = self.selenium.find_element(By.CSS_SELECTOR, ".actions > .clear")
    
  1692.         select_all = self.selenium.find_element(By.ID, "action-toggle")
    
  1693.         select_across = self.selenium.find_elements(By.NAME, "select_across")
    
  1694. 
    
  1695.         self.assertIs(question.is_displayed(), False)
    
  1696.         self.assertIs(clear.is_displayed(), False)
    
  1697.         self.assertIs(select_all.get_property("checked"), False)
    
  1698.         for hidden_input in select_across:
    
  1699.             self.assertEqual(hidden_input.get_property("value"), "0")
    
  1700.         self.assertIs(selection_indicator.is_displayed(), True)
    
  1701.         self.assertEqual(selection_indicator.text, "0 of 100 selected")
    
  1702.         self.assertIs(select_all_indicator.is_displayed(), False)
    
  1703. 
    
  1704.         select_all.click()
    
  1705.         self.assertIs(question.is_displayed(), True)
    
  1706.         self.assertIs(clear.is_displayed(), False)
    
  1707.         self.assertIs(select_all.get_property("checked"), True)
    
  1708.         for hidden_input in select_across:
    
  1709.             self.assertEqual(hidden_input.get_property("value"), "0")
    
  1710.         self.assertIs(selection_indicator.is_displayed(), True)
    
  1711.         self.assertEqual(selection_indicator.text, "100 of 100 selected")
    
  1712.         self.assertIs(select_all_indicator.is_displayed(), False)
    
  1713. 
    
  1714.         question.click()
    
  1715.         self.assertIs(question.is_displayed(), False)
    
  1716.         self.assertIs(clear.is_displayed(), True)
    
  1717.         self.assertIs(select_all.get_property("checked"), True)
    
  1718.         for hidden_input in select_across:
    
  1719.             self.assertEqual(hidden_input.get_property("value"), "1")
    
  1720.         self.assertIs(selection_indicator.is_displayed(), False)
    
  1721.         self.assertIs(select_all_indicator.is_displayed(), True)
    
  1722. 
    
  1723.         clear.click()
    
  1724.         self.assertIs(question.is_displayed(), False)
    
  1725.         self.assertIs(clear.is_displayed(), False)
    
  1726.         self.assertIs(select_all.get_property("checked"), False)
    
  1727.         for hidden_input in select_across:
    
  1728.             self.assertEqual(hidden_input.get_property("value"), "0")
    
  1729.         self.assertIs(selection_indicator.is_displayed(), True)
    
  1730.         self.assertEqual(selection_indicator.text, "0 of 100 selected")
    
  1731.         self.assertIs(select_all_indicator.is_displayed(), False)
    
  1732. 
    
  1733.     def test_actions_warn_on_pending_edits(self):
    
  1734.         from selenium.webdriver.common.by import By
    
  1735. 
    
  1736.         Parent.objects.create(name="foo")
    
  1737. 
    
  1738.         self.admin_login(username="super", password="secret")
    
  1739.         self.selenium.get(
    
  1740.             self.live_server_url + reverse("admin:admin_changelist_parent_changelist")
    
  1741.         )
    
  1742. 
    
  1743.         name_input = self.selenium.find_element(By.ID, "id_form-0-name")
    
  1744.         name_input.clear()
    
  1745.         name_input.send_keys("bar")
    
  1746.         self.selenium.find_element(By.ID, "action-toggle").click()
    
  1747.         self.selenium.find_element(By.NAME, "index").click()  # Go
    
  1748.         alert = self.selenium.switch_to.alert
    
  1749.         try:
    
  1750.             self.assertEqual(
    
  1751.                 alert.text,
    
  1752.                 "You have unsaved changes on individual editable fields. If you "
    
  1753.                 "run an action, your unsaved changes will be lost.",
    
  1754.             )
    
  1755.         finally:
    
  1756.             alert.dismiss()
    
  1757. 
    
  1758.     def test_save_with_changes_warns_on_pending_action(self):
    
  1759.         from selenium.webdriver.common.by import By
    
  1760.         from selenium.webdriver.support.ui import Select
    
  1761. 
    
  1762.         Parent.objects.create(name="parent")
    
  1763. 
    
  1764.         self.admin_login(username="super", password="secret")
    
  1765.         self.selenium.get(
    
  1766.             self.live_server_url + reverse("admin:admin_changelist_parent_changelist")
    
  1767.         )
    
  1768. 
    
  1769.         name_input = self.selenium.find_element(By.ID, "id_form-0-name")
    
  1770.         name_input.clear()
    
  1771.         name_input.send_keys("other name")
    
  1772.         Select(self.selenium.find_element(By.NAME, "action")).select_by_value(
    
  1773.             "delete_selected"
    
  1774.         )
    
  1775.         self.selenium.find_element(By.NAME, "_save").click()
    
  1776.         alert = self.selenium.switch_to.alert
    
  1777.         try:
    
  1778.             self.assertEqual(
    
  1779.                 alert.text,
    
  1780.                 "You have selected an action, but you haven’t saved your "
    
  1781.                 "changes to individual fields yet. Please click OK to save. "
    
  1782.                 "You’ll need to re-run the action.",
    
  1783.             )
    
  1784.         finally:
    
  1785.             alert.dismiss()
    
  1786. 
    
  1787.     def test_save_without_changes_warns_on_pending_action(self):
    
  1788.         from selenium.webdriver.common.by import By
    
  1789.         from selenium.webdriver.support.ui import Select
    
  1790. 
    
  1791.         Parent.objects.create(name="parent")
    
  1792. 
    
  1793.         self.admin_login(username="super", password="secret")
    
  1794.         self.selenium.get(
    
  1795.             self.live_server_url + reverse("admin:admin_changelist_parent_changelist")
    
  1796.         )
    
  1797. 
    
  1798.         Select(self.selenium.find_element(By.NAME, "action")).select_by_value(
    
  1799.             "delete_selected"
    
  1800.         )
    
  1801.         self.selenium.find_element(By.NAME, "_save").click()
    
  1802.         alert = self.selenium.switch_to.alert
    
  1803.         try:
    
  1804.             self.assertEqual(
    
  1805.                 alert.text,
    
  1806.                 "You have selected an action, and you haven’t made any "
    
  1807.                 "changes on individual fields. You’re probably looking for "
    
  1808.                 "the Go button rather than the Save button.",
    
  1809.             )
    
  1810.         finally:
    
  1811.             alert.dismiss()
    
  1812. 
    
  1813.     def test_collapse_filters(self):
    
  1814.         from selenium.webdriver.common.by import By
    
  1815. 
    
  1816.         self.admin_login(username="super", password="secret")
    
  1817.         self.selenium.get(self.live_server_url + reverse("admin:auth_user_changelist"))
    
  1818. 
    
  1819.         # The UserAdmin has 3 field filters by default: "staff status",
    
  1820.         # "superuser status", and "active".
    
  1821.         details = self.selenium.find_elements(By.CSS_SELECTOR, "details")
    
  1822.         # All filters are opened at first.
    
  1823.         for detail in details:
    
  1824.             self.assertTrue(detail.get_attribute("open"))
    
  1825.         # Collapse "staff' and "superuser" filters.
    
  1826.         for detail in details[:2]:
    
  1827.             summary = detail.find_element(By.CSS_SELECTOR, "summary")
    
  1828.             summary.click()
    
  1829.             self.assertFalse(detail.get_attribute("open"))
    
  1830.         # Filters are in the same state after refresh.
    
  1831.         self.selenium.refresh()
    
  1832.         self.assertFalse(
    
  1833.             self.selenium.find_element(
    
  1834.                 By.CSS_SELECTOR, "[data-filter-title='staff status']"
    
  1835.             ).get_attribute("open")
    
  1836.         )
    
  1837.         self.assertFalse(
    
  1838.             self.selenium.find_element(
    
  1839.                 By.CSS_SELECTOR, "[data-filter-title='superuser status']"
    
  1840.             ).get_attribute("open")
    
  1841.         )
    
  1842.         self.assertTrue(
    
  1843.             self.selenium.find_element(
    
  1844.                 By.CSS_SELECTOR, "[data-filter-title='active']"
    
  1845.             ).get_attribute("open")
    
  1846.         )
    
  1847.         # Collapse a filter on another view (Bands).
    
  1848.         self.selenium.get(
    
  1849.             self.live_server_url + reverse("admin:admin_changelist_band_changelist")
    
  1850.         )
    
  1851.         self.selenium.find_element(By.CSS_SELECTOR, "summary").click()
    
  1852.         # Go to Users view and then, back again to Bands view.
    
  1853.         self.selenium.get(self.live_server_url + reverse("admin:auth_user_changelist"))
    
  1854.         self.selenium.get(
    
  1855.             self.live_server_url + reverse("admin:admin_changelist_band_changelist")
    
  1856.         )
    
  1857.         # The filter remains in the same state.
    
  1858.         self.assertFalse(
    
  1859.             self.selenium.find_element(
    
  1860.                 By.CSS_SELECTOR,
    
  1861.                 "[data-filter-title='number of members']",
    
  1862.             ).get_attribute("open")
    
  1863.         )