1. import gettext
    
  2. import os
    
  3. import re
    
  4. from datetime import datetime, timedelta
    
  5. from importlib import import_module
    
  6. 
    
  7. try:
    
  8.     import zoneinfo
    
  9. except ImportError:
    
  10.     from backports import zoneinfo
    
  11. 
    
  12. from django import forms
    
  13. from django.conf import settings
    
  14. from django.contrib import admin
    
  15. from django.contrib.admin import widgets
    
  16. from django.contrib.admin.tests import AdminSeleniumTestCase
    
  17. from django.contrib.auth.models import User
    
  18. from django.core.files.storage import default_storage
    
  19. from django.core.files.uploadedfile import SimpleUploadedFile
    
  20. from django.db.models import (
    
  21.     CharField,
    
  22.     DateField,
    
  23.     DateTimeField,
    
  24.     ForeignKey,
    
  25.     ManyToManyField,
    
  26.     UUIDField,
    
  27. )
    
  28. from django.test import SimpleTestCase, TestCase, override_settings
    
  29. from django.urls import reverse
    
  30. from django.utils import translation
    
  31. 
    
  32. from .models import (
    
  33.     Advisor,
    
  34.     Album,
    
  35.     Band,
    
  36.     Bee,
    
  37.     Car,
    
  38.     Company,
    
  39.     Event,
    
  40.     Honeycomb,
    
  41.     Individual,
    
  42.     Inventory,
    
  43.     Member,
    
  44.     MyFileField,
    
  45.     Profile,
    
  46.     ReleaseEvent,
    
  47.     School,
    
  48.     Student,
    
  49.     UnsafeLimitChoicesTo,
    
  50.     VideoStream,
    
  51. )
    
  52. from .widgetadmin import site as widget_admin_site
    
  53. 
    
  54. 
    
  55. class TestDataMixin:
    
  56.     @classmethod
    
  57.     def setUpTestData(cls):
    
  58.         cls.superuser = User.objects.create_superuser(
    
  59.             username="super", password="secret", email=None
    
  60.         )
    
  61.         cls.u2 = User.objects.create_user(username="testser", password="secret")
    
  62.         Car.objects.create(owner=cls.superuser, make="Volkswagen", model="Passat")
    
  63.         Car.objects.create(owner=cls.u2, make="BMW", model="M3")
    
  64. 
    
  65. 
    
  66. class AdminFormfieldForDBFieldTests(SimpleTestCase):
    
  67.     """
    
  68.     Tests for correct behavior of ModelAdmin.formfield_for_dbfield
    
  69.     """
    
  70. 
    
  71.     def assertFormfield(self, model, fieldname, widgetclass, **admin_overrides):
    
  72.         """
    
  73.         Helper to call formfield_for_dbfield for a given model and field name
    
  74.         and verify that the returned formfield is appropriate.
    
  75.         """
    
  76. 
    
  77.         # Override any settings on the model admin
    
  78.         class MyModelAdmin(admin.ModelAdmin):
    
  79.             pass
    
  80. 
    
  81.         for k in admin_overrides:
    
  82.             setattr(MyModelAdmin, k, admin_overrides[k])
    
  83. 
    
  84.         # Construct the admin, and ask it for a formfield
    
  85.         ma = MyModelAdmin(model, admin.site)
    
  86.         ff = ma.formfield_for_dbfield(model._meta.get_field(fieldname), request=None)
    
  87. 
    
  88.         # "unwrap" the widget wrapper, if needed
    
  89.         if isinstance(ff.widget, widgets.RelatedFieldWidgetWrapper):
    
  90.             widget = ff.widget.widget
    
  91.         else:
    
  92.             widget = ff.widget
    
  93. 
    
  94.         self.assertIsInstance(widget, widgetclass)
    
  95. 
    
  96.         # Return the formfield so that other tests can continue
    
  97.         return ff
    
  98. 
    
  99.     def test_DateField(self):
    
  100.         self.assertFormfield(Event, "start_date", widgets.AdminDateWidget)
    
  101. 
    
  102.     def test_DateTimeField(self):
    
  103.         self.assertFormfield(Member, "birthdate", widgets.AdminSplitDateTime)
    
  104. 
    
  105.     def test_TimeField(self):
    
  106.         self.assertFormfield(Event, "start_time", widgets.AdminTimeWidget)
    
  107. 
    
  108.     def test_TextField(self):
    
  109.         self.assertFormfield(Event, "description", widgets.AdminTextareaWidget)
    
  110. 
    
  111.     def test_URLField(self):
    
  112.         self.assertFormfield(Event, "link", widgets.AdminURLFieldWidget)
    
  113. 
    
  114.     def test_IntegerField(self):
    
  115.         self.assertFormfield(Event, "min_age", widgets.AdminIntegerFieldWidget)
    
  116. 
    
  117.     def test_CharField(self):
    
  118.         self.assertFormfield(Member, "name", widgets.AdminTextInputWidget)
    
  119. 
    
  120.     def test_EmailField(self):
    
  121.         self.assertFormfield(Member, "email", widgets.AdminEmailInputWidget)
    
  122. 
    
  123.     def test_FileField(self):
    
  124.         self.assertFormfield(Album, "cover_art", widgets.AdminFileWidget)
    
  125. 
    
  126.     def test_ForeignKey(self):
    
  127.         self.assertFormfield(Event, "main_band", forms.Select)
    
  128. 
    
  129.     def test_raw_id_ForeignKey(self):
    
  130.         self.assertFormfield(
    
  131.             Event,
    
  132.             "main_band",
    
  133.             widgets.ForeignKeyRawIdWidget,
    
  134.             raw_id_fields=["main_band"],
    
  135.         )
    
  136. 
    
  137.     def test_radio_fields_ForeignKey(self):
    
  138.         ff = self.assertFormfield(
    
  139.             Event,
    
  140.             "main_band",
    
  141.             widgets.AdminRadioSelect,
    
  142.             radio_fields={"main_band": admin.VERTICAL},
    
  143.         )
    
  144.         self.assertIsNone(ff.empty_label)
    
  145. 
    
  146.     def test_radio_fields_foreignkey_formfield_overrides_empty_label(self):
    
  147.         class MyModelAdmin(admin.ModelAdmin):
    
  148.             radio_fields = {"parent": admin.VERTICAL}
    
  149.             formfield_overrides = {
    
  150.                 ForeignKey: {"empty_label": "Custom empty label"},
    
  151.             }
    
  152. 
    
  153.         ma = MyModelAdmin(Inventory, admin.site)
    
  154.         ff = ma.formfield_for_dbfield(Inventory._meta.get_field("parent"), request=None)
    
  155.         self.assertEqual(ff.empty_label, "Custom empty label")
    
  156. 
    
  157.     def test_many_to_many(self):
    
  158.         self.assertFormfield(Band, "members", forms.SelectMultiple)
    
  159. 
    
  160.     def test_raw_id_many_to_many(self):
    
  161.         self.assertFormfield(
    
  162.             Band, "members", widgets.ManyToManyRawIdWidget, raw_id_fields=["members"]
    
  163.         )
    
  164. 
    
  165.     def test_filtered_many_to_many(self):
    
  166.         self.assertFormfield(
    
  167.             Band, "members", widgets.FilteredSelectMultiple, filter_vertical=["members"]
    
  168.         )
    
  169. 
    
  170.     def test_formfield_overrides(self):
    
  171.         self.assertFormfield(
    
  172.             Event,
    
  173.             "start_date",
    
  174.             forms.TextInput,
    
  175.             formfield_overrides={DateField: {"widget": forms.TextInput}},
    
  176.         )
    
  177. 
    
  178.     def test_formfield_overrides_widget_instances(self):
    
  179.         """
    
  180.         Widget instances in formfield_overrides are not shared between
    
  181.         different fields. (#19423)
    
  182.         """
    
  183. 
    
  184.         class BandAdmin(admin.ModelAdmin):
    
  185.             formfield_overrides = {
    
  186.                 CharField: {"widget": forms.TextInput(attrs={"size": "10"})}
    
  187.             }
    
  188. 
    
  189.         ma = BandAdmin(Band, admin.site)
    
  190.         f1 = ma.formfield_for_dbfield(Band._meta.get_field("name"), request=None)
    
  191.         f2 = ma.formfield_for_dbfield(Band._meta.get_field("style"), request=None)
    
  192.         self.assertNotEqual(f1.widget, f2.widget)
    
  193.         self.assertEqual(f1.widget.attrs["maxlength"], "100")
    
  194.         self.assertEqual(f2.widget.attrs["maxlength"], "20")
    
  195.         self.assertEqual(f2.widget.attrs["size"], "10")
    
  196. 
    
  197.     def test_formfield_overrides_m2m_filter_widget(self):
    
  198.         """
    
  199.         The autocomplete_fields, raw_id_fields, filter_vertical, and
    
  200.         filter_horizontal widgets for ManyToManyFields may be overridden by
    
  201.         specifying a widget in formfield_overrides.
    
  202.         """
    
  203. 
    
  204.         class BandAdmin(admin.ModelAdmin):
    
  205.             filter_vertical = ["members"]
    
  206.             formfield_overrides = {
    
  207.                 ManyToManyField: {"widget": forms.CheckboxSelectMultiple},
    
  208.             }
    
  209. 
    
  210.         ma = BandAdmin(Band, admin.site)
    
  211.         field = ma.formfield_for_dbfield(Band._meta.get_field("members"), request=None)
    
  212.         self.assertIsInstance(field.widget.widget, forms.CheckboxSelectMultiple)
    
  213. 
    
  214.     def test_formfield_overrides_for_datetime_field(self):
    
  215.         """
    
  216.         Overriding the widget for DateTimeField doesn't overrides the default
    
  217.         form_class for that field (#26449).
    
  218.         """
    
  219. 
    
  220.         class MemberAdmin(admin.ModelAdmin):
    
  221.             formfield_overrides = {
    
  222.                 DateTimeField: {"widget": widgets.AdminSplitDateTime}
    
  223.             }
    
  224. 
    
  225.         ma = MemberAdmin(Member, admin.site)
    
  226.         f1 = ma.formfield_for_dbfield(Member._meta.get_field("birthdate"), request=None)
    
  227.         self.assertIsInstance(f1.widget, widgets.AdminSplitDateTime)
    
  228.         self.assertIsInstance(f1, forms.SplitDateTimeField)
    
  229. 
    
  230.     def test_formfield_overrides_for_custom_field(self):
    
  231.         """
    
  232.         formfield_overrides works for a custom field class.
    
  233.         """
    
  234. 
    
  235.         class AlbumAdmin(admin.ModelAdmin):
    
  236.             formfield_overrides = {MyFileField: {"widget": forms.TextInput()}}
    
  237. 
    
  238.         ma = AlbumAdmin(Member, admin.site)
    
  239.         f1 = ma.formfield_for_dbfield(
    
  240.             Album._meta.get_field("backside_art"), request=None
    
  241.         )
    
  242.         self.assertIsInstance(f1.widget, forms.TextInput)
    
  243. 
    
  244.     def test_field_with_choices(self):
    
  245.         self.assertFormfield(Member, "gender", forms.Select)
    
  246. 
    
  247.     def test_choices_with_radio_fields(self):
    
  248.         self.assertFormfield(
    
  249.             Member,
    
  250.             "gender",
    
  251.             widgets.AdminRadioSelect,
    
  252.             radio_fields={"gender": admin.VERTICAL},
    
  253.         )
    
  254. 
    
  255.     def test_inheritance(self):
    
  256.         self.assertFormfield(Album, "backside_art", widgets.AdminFileWidget)
    
  257. 
    
  258.     def test_m2m_widgets(self):
    
  259.         """m2m fields help text as it applies to admin app (#9321)."""
    
  260. 
    
  261.         class AdvisorAdmin(admin.ModelAdmin):
    
  262.             filter_vertical = ["companies"]
    
  263. 
    
  264.         self.assertFormfield(
    
  265.             Advisor,
    
  266.             "companies",
    
  267.             widgets.FilteredSelectMultiple,
    
  268.             filter_vertical=["companies"],
    
  269.         )
    
  270.         ma = AdvisorAdmin(Advisor, admin.site)
    
  271.         f = ma.formfield_for_dbfield(Advisor._meta.get_field("companies"), request=None)
    
  272.         self.assertEqual(
    
  273.             f.help_text,
    
  274.             "Hold down “Control”, or “Command” on a Mac, to select more than one.",
    
  275.         )
    
  276. 
    
  277. 
    
  278. @override_settings(ROOT_URLCONF="admin_widgets.urls")
    
  279. class AdminFormfieldForDBFieldWithRequestTests(TestDataMixin, TestCase):
    
  280.     def test_filter_choices_by_request_user(self):
    
  281.         """
    
  282.         Ensure the user can only see their own cars in the foreign key dropdown.
    
  283.         """
    
  284.         self.client.force_login(self.superuser)
    
  285.         response = self.client.get(reverse("admin:admin_widgets_cartire_add"))
    
  286.         self.assertNotContains(response, "BMW M3")
    
  287.         self.assertContains(response, "Volkswagen Passat")
    
  288. 
    
  289. 
    
  290. @override_settings(ROOT_URLCONF="admin_widgets.urls")
    
  291. class AdminForeignKeyWidgetChangeList(TestDataMixin, TestCase):
    
  292.     def setUp(self):
    
  293.         self.client.force_login(self.superuser)
    
  294. 
    
  295.     def test_changelist_ForeignKey(self):
    
  296.         response = self.client.get(reverse("admin:admin_widgets_car_changelist"))
    
  297.         self.assertContains(response, "/auth/user/add/")
    
  298. 
    
  299. 
    
  300. @override_settings(ROOT_URLCONF="admin_widgets.urls")
    
  301. class AdminForeignKeyRawIdWidget(TestDataMixin, TestCase):
    
  302.     def setUp(self):
    
  303.         self.client.force_login(self.superuser)
    
  304. 
    
  305.     def test_nonexistent_target_id(self):
    
  306.         band = Band.objects.create(name="Bogey Blues")
    
  307.         pk = band.pk
    
  308.         band.delete()
    
  309.         post_data = {
    
  310.             "main_band": str(pk),
    
  311.         }
    
  312.         # Try posting with a nonexistent pk in a raw id field: this
    
  313.         # should result in an error message, not a server exception.
    
  314.         response = self.client.post(reverse("admin:admin_widgets_event_add"), post_data)
    
  315.         self.assertContains(
    
  316.             response,
    
  317.             "Select a valid choice. That choice is not one of the available choices.",
    
  318.         )
    
  319. 
    
  320.     def test_invalid_target_id(self):
    
  321.         for test_str in ("Iñtërnâtiônàlizætiøn", "1234'", -1234):
    
  322.             # This should result in an error message, not a server exception.
    
  323.             response = self.client.post(
    
  324.                 reverse("admin:admin_widgets_event_add"), {"main_band": test_str}
    
  325.             )
    
  326. 
    
  327.             self.assertContains(
    
  328.                 response,
    
  329.                 "Select a valid choice. That choice is not one of the available "
    
  330.                 "choices.",
    
  331.             )
    
  332. 
    
  333.     def test_url_params_from_lookup_dict_any_iterable(self):
    
  334.         lookup1 = widgets.url_params_from_lookup_dict({"color__in": ("red", "blue")})
    
  335.         lookup2 = widgets.url_params_from_lookup_dict({"color__in": ["red", "blue"]})
    
  336.         self.assertEqual(lookup1, {"color__in": "red,blue"})
    
  337.         self.assertEqual(lookup1, lookup2)
    
  338. 
    
  339.     def test_url_params_from_lookup_dict_callable(self):
    
  340.         def my_callable():
    
  341.             return "works"
    
  342. 
    
  343.         lookup1 = widgets.url_params_from_lookup_dict({"myfield": my_callable})
    
  344.         lookup2 = widgets.url_params_from_lookup_dict({"myfield": my_callable()})
    
  345.         self.assertEqual(lookup1, lookup2)
    
  346. 
    
  347.     def test_label_and_url_for_value_invalid_uuid(self):
    
  348.         field = Bee._meta.get_field("honeycomb")
    
  349.         self.assertIsInstance(field.target_field, UUIDField)
    
  350.         widget = widgets.ForeignKeyRawIdWidget(field.remote_field, admin.site)
    
  351.         self.assertEqual(widget.label_and_url_for_value("invalid-uuid"), ("", ""))
    
  352. 
    
  353. 
    
  354. class FilteredSelectMultipleWidgetTest(SimpleTestCase):
    
  355.     def test_render(self):
    
  356.         # Backslash in verbose_name to ensure it is JavaScript escaped.
    
  357.         w = widgets.FilteredSelectMultiple("test\\", False)
    
  358.         self.assertHTMLEqual(
    
  359.             w.render("test", "test"),
    
  360.             '<select multiple name="test" class="selectfilter" '
    
  361.             'data-field-name="test\\" data-is-stacked="0">\n</select>',
    
  362.         )
    
  363. 
    
  364.     def test_stacked_render(self):
    
  365.         # Backslash in verbose_name to ensure it is JavaScript escaped.
    
  366.         w = widgets.FilteredSelectMultiple("test\\", True)
    
  367.         self.assertHTMLEqual(
    
  368.             w.render("test", "test"),
    
  369.             '<select multiple name="test" class="selectfilterstacked" '
    
  370.             'data-field-name="test\\" data-is-stacked="1">\n</select>',
    
  371.         )
    
  372. 
    
  373. 
    
  374. class AdminDateWidgetTest(SimpleTestCase):
    
  375.     def test_attrs(self):
    
  376.         w = widgets.AdminDateWidget()
    
  377.         self.assertHTMLEqual(
    
  378.             w.render("test", datetime(2007, 12, 1, 9, 30)),
    
  379.             '<input value="2007-12-01" type="text" class="vDateField" name="test" '
    
  380.             'size="10">',
    
  381.         )
    
  382.         # pass attrs to widget
    
  383.         w = widgets.AdminDateWidget(attrs={"size": 20, "class": "myDateField"})
    
  384.         self.assertHTMLEqual(
    
  385.             w.render("test", datetime(2007, 12, 1, 9, 30)),
    
  386.             '<input value="2007-12-01" type="text" class="myDateField" name="test" '
    
  387.             'size="20">',
    
  388.         )
    
  389. 
    
  390. 
    
  391. class AdminTimeWidgetTest(SimpleTestCase):
    
  392.     def test_attrs(self):
    
  393.         w = widgets.AdminTimeWidget()
    
  394.         self.assertHTMLEqual(
    
  395.             w.render("test", datetime(2007, 12, 1, 9, 30)),
    
  396.             '<input value="09:30:00" type="text" class="vTimeField" name="test" '
    
  397.             'size="8">',
    
  398.         )
    
  399.         # pass attrs to widget
    
  400.         w = widgets.AdminTimeWidget(attrs={"size": 20, "class": "myTimeField"})
    
  401.         self.assertHTMLEqual(
    
  402.             w.render("test", datetime(2007, 12, 1, 9, 30)),
    
  403.             '<input value="09:30:00" type="text" class="myTimeField" name="test" '
    
  404.             'size="20">',
    
  405.         )
    
  406. 
    
  407. 
    
  408. class AdminSplitDateTimeWidgetTest(SimpleTestCase):
    
  409.     def test_render(self):
    
  410.         w = widgets.AdminSplitDateTime()
    
  411.         self.assertHTMLEqual(
    
  412.             w.render("test", datetime(2007, 12, 1, 9, 30)),
    
  413.             '<p class="datetime">'
    
  414.             'Date: <input value="2007-12-01" type="text" class="vDateField" '
    
  415.             'name="test_0" size="10"><br>'
    
  416.             'Time: <input value="09:30:00" type="text" class="vTimeField" '
    
  417.             'name="test_1" size="8"></p>',
    
  418.         )
    
  419. 
    
  420.     def test_localization(self):
    
  421.         w = widgets.AdminSplitDateTime()
    
  422. 
    
  423.         with translation.override("de-at"):
    
  424.             w.is_localized = True
    
  425.             self.assertHTMLEqual(
    
  426.                 w.render("test", datetime(2007, 12, 1, 9, 30)),
    
  427.                 '<p class="datetime">'
    
  428.                 'Datum: <input value="01.12.2007" type="text" '
    
  429.                 'class="vDateField" name="test_0"size="10"><br>'
    
  430.                 'Zeit: <input value="09:30:00" type="text" class="vTimeField" '
    
  431.                 'name="test_1" size="8"></p>',
    
  432.             )
    
  433. 
    
  434. 
    
  435. class AdminURLWidgetTest(SimpleTestCase):
    
  436.     def test_get_context_validates_url(self):
    
  437.         w = widgets.AdminURLFieldWidget()
    
  438.         for invalid in ["", "/not/a/full/url/", 'javascript:alert("Danger XSS!")']:
    
  439.             with self.subTest(url=invalid):
    
  440.                 self.assertFalse(w.get_context("name", invalid, {})["url_valid"])
    
  441.         self.assertTrue(w.get_context("name", "http://example.com", {})["url_valid"])
    
  442. 
    
  443.     def test_render(self):
    
  444.         w = widgets.AdminURLFieldWidget()
    
  445.         self.assertHTMLEqual(
    
  446.             w.render("test", ""), '<input class="vURLField" name="test" type="url">'
    
  447.         )
    
  448.         self.assertHTMLEqual(
    
  449.             w.render("test", "http://example.com"),
    
  450.             '<p class="url">Currently:<a href="http://example.com">'
    
  451.             "http://example.com</a><br>"
    
  452.             'Change:<input class="vURLField" name="test" type="url" '
    
  453.             'value="http://example.com"></p>',
    
  454.         )
    
  455. 
    
  456.     def test_render_idn(self):
    
  457.         w = widgets.AdminURLFieldWidget()
    
  458.         self.assertHTMLEqual(
    
  459.             w.render("test", "http://example-äüö.com"),
    
  460.             '<p class="url">Currently: <a href="http://xn--example--7za4pnc.com">'
    
  461.             "http://example-äüö.com</a><br>"
    
  462.             'Change:<input class="vURLField" name="test" type="url" '
    
  463.             'value="http://example-äüö.com"></p>',
    
  464.         )
    
  465. 
    
  466.     def test_render_quoting(self):
    
  467.         """
    
  468.         WARNING: This test doesn't use assertHTMLEqual since it will get rid
    
  469.         of some escapes which are tested here!
    
  470.         """
    
  471.         HREF_RE = re.compile('href="([^"]+)"')
    
  472.         VALUE_RE = re.compile('value="([^"]+)"')
    
  473.         TEXT_RE = re.compile("<a[^>]+>([^>]+)</a>")
    
  474.         w = widgets.AdminURLFieldWidget()
    
  475.         output = w.render("test", "http://example.com/<sometag>some-text</sometag>")
    
  476.         self.assertEqual(
    
  477.             HREF_RE.search(output)[1],
    
  478.             "http://example.com/%3Csometag%3Esome-text%3C/sometag%3E",
    
  479.         )
    
  480.         self.assertEqual(
    
  481.             TEXT_RE.search(output)[1],
    
  482.             "http://example.com/&lt;sometag&gt;some-text&lt;/sometag&gt;",
    
  483.         )
    
  484.         self.assertEqual(
    
  485.             VALUE_RE.search(output)[1],
    
  486.             "http://example.com/&lt;sometag&gt;some-text&lt;/sometag&gt;",
    
  487.         )
    
  488.         output = w.render("test", "http://example-äüö.com/<sometag>some-text</sometag>")
    
  489.         self.assertEqual(
    
  490.             HREF_RE.search(output)[1],
    
  491.             "http://xn--example--7za4pnc.com/%3Csometag%3Esome-text%3C/sometag%3E",
    
  492.         )
    
  493.         self.assertEqual(
    
  494.             TEXT_RE.search(output)[1],
    
  495.             "http://example-äüö.com/&lt;sometag&gt;some-text&lt;/sometag&gt;",
    
  496.         )
    
  497.         self.assertEqual(
    
  498.             VALUE_RE.search(output)[1],
    
  499.             "http://example-äüö.com/&lt;sometag&gt;some-text&lt;/sometag&gt;",
    
  500.         )
    
  501.         output = w.render(
    
  502.             "test", 'http://www.example.com/%C3%A4"><script>alert("XSS!")</script>"'
    
  503.         )
    
  504.         self.assertEqual(
    
  505.             HREF_RE.search(output)[1],
    
  506.             "http://www.example.com/%C3%A4%22%3E%3Cscript%3Ealert(%22XSS!%22)"
    
  507.             "%3C/script%3E%22",
    
  508.         )
    
  509.         self.assertEqual(
    
  510.             TEXT_RE.search(output)[1],
    
  511.             "http://www.example.com/%C3%A4&quot;&gt;&lt;script&gt;"
    
  512.             "alert(&quot;XSS!&quot;)&lt;/script&gt;&quot;",
    
  513.         )
    
  514.         self.assertEqual(
    
  515.             VALUE_RE.search(output)[1],
    
  516.             "http://www.example.com/%C3%A4&quot;&gt;&lt;script&gt;"
    
  517.             "alert(&quot;XSS!&quot;)&lt;/script&gt;&quot;",
    
  518.         )
    
  519. 
    
  520. 
    
  521. class AdminUUIDWidgetTests(SimpleTestCase):
    
  522.     def test_attrs(self):
    
  523.         w = widgets.AdminUUIDInputWidget()
    
  524.         self.assertHTMLEqual(
    
  525.             w.render("test", "550e8400-e29b-41d4-a716-446655440000"),
    
  526.             '<input value="550e8400-e29b-41d4-a716-446655440000" type="text" '
    
  527.             'class="vUUIDField" name="test">',
    
  528.         )
    
  529.         w = widgets.AdminUUIDInputWidget(attrs={"class": "myUUIDInput"})
    
  530.         self.assertHTMLEqual(
    
  531.             w.render("test", "550e8400-e29b-41d4-a716-446655440000"),
    
  532.             '<input value="550e8400-e29b-41d4-a716-446655440000" type="text" '
    
  533.             'class="myUUIDInput" name="test">',
    
  534.         )
    
  535. 
    
  536. 
    
  537. @override_settings(ROOT_URLCONF="admin_widgets.urls")
    
  538. class AdminFileWidgetTests(TestDataMixin, TestCase):
    
  539.     @classmethod
    
  540.     def setUpTestData(cls):
    
  541.         super().setUpTestData()
    
  542.         band = Band.objects.create(name="Linkin Park")
    
  543.         cls.album = band.album_set.create(
    
  544.             name="Hybrid Theory", cover_art=r"albums\hybrid_theory.jpg"
    
  545.         )
    
  546. 
    
  547.     def test_render(self):
    
  548.         w = widgets.AdminFileWidget()
    
  549.         self.assertHTMLEqual(
    
  550.             w.render("test", self.album.cover_art),
    
  551.             '<p class="file-upload">Currently: <a href="%(STORAGE_URL)salbums/'
    
  552.             r'hybrid_theory.jpg">albums\hybrid_theory.jpg</a> '
    
  553.             '<span class="clearable-file-input">'
    
  554.             '<input type="checkbox" name="test-clear" id="test-clear_id"> '
    
  555.             '<label for="test-clear_id">Clear</label></span><br>'
    
  556.             'Change: <input type="file" name="test"></p>'
    
  557.             % {
    
  558.                 "STORAGE_URL": default_storage.url(""),
    
  559.             },
    
  560.         )
    
  561.         self.assertHTMLEqual(
    
  562.             w.render("test", SimpleUploadedFile("test", b"content")),
    
  563.             '<input type="file" name="test">',
    
  564.         )
    
  565. 
    
  566.     def test_render_required(self):
    
  567.         widget = widgets.AdminFileWidget()
    
  568.         widget.is_required = True
    
  569.         self.assertHTMLEqual(
    
  570.             widget.render("test", self.album.cover_art),
    
  571.             '<p class="file-upload">Currently: <a href="%(STORAGE_URL)salbums/'
    
  572.             r'hybrid_theory.jpg">albums\hybrid_theory.jpg</a><br>'
    
  573.             'Change: <input type="file" name="test"></p>'
    
  574.             % {
    
  575.                 "STORAGE_URL": default_storage.url(""),
    
  576.             },
    
  577.         )
    
  578. 
    
  579.     def test_render_disabled(self):
    
  580.         widget = widgets.AdminFileWidget(attrs={"disabled": True})
    
  581.         self.assertHTMLEqual(
    
  582.             widget.render("test", self.album.cover_art),
    
  583.             '<p class="file-upload">Currently: <a href="%(STORAGE_URL)salbums/'
    
  584.             r'hybrid_theory.jpg">albums\hybrid_theory.jpg</a> '
    
  585.             '<span class="clearable-file-input">'
    
  586.             '<input type="checkbox" name="test-clear" id="test-clear_id" disabled>'
    
  587.             '<label for="test-clear_id">Clear</label></span><br>'
    
  588.             'Change: <input type="file" name="test" disabled></p>'
    
  589.             % {
    
  590.                 "STORAGE_URL": default_storage.url(""),
    
  591.             },
    
  592.         )
    
  593. 
    
  594.     def test_readonly_fields(self):
    
  595.         """
    
  596.         File widgets should render as a link when they're marked "read only."
    
  597.         """
    
  598.         self.client.force_login(self.superuser)
    
  599.         response = self.client.get(
    
  600.             reverse("admin:admin_widgets_album_change", args=(self.album.id,))
    
  601.         )
    
  602.         self.assertContains(
    
  603.             response,
    
  604.             '<div class="readonly"><a href="%(STORAGE_URL)salbums/hybrid_theory.jpg">'
    
  605.             r"albums\hybrid_theory.jpg</a></div>"
    
  606.             % {"STORAGE_URL": default_storage.url("")},
    
  607.             html=True,
    
  608.         )
    
  609.         self.assertNotContains(
    
  610.             response,
    
  611.             '<input type="file" name="cover_art" id="id_cover_art">',
    
  612.             html=True,
    
  613.         )
    
  614.         response = self.client.get(reverse("admin:admin_widgets_album_add"))
    
  615.         self.assertContains(
    
  616.             response,
    
  617.             '<div class="readonly"></div>',
    
  618.             html=True,
    
  619.         )
    
  620. 
    
  621. 
    
  622. @override_settings(ROOT_URLCONF="admin_widgets.urls")
    
  623. class ForeignKeyRawIdWidgetTest(TestCase):
    
  624.     def test_render(self):
    
  625.         band = Band.objects.create(name="Linkin Park")
    
  626.         band.album_set.create(
    
  627.             name="Hybrid Theory", cover_art=r"albums\hybrid_theory.jpg"
    
  628.         )
    
  629.         rel_uuid = Album._meta.get_field("band").remote_field
    
  630.         w = widgets.ForeignKeyRawIdWidget(rel_uuid, widget_admin_site)
    
  631.         self.assertHTMLEqual(
    
  632.             w.render("test", band.uuid, attrs={}),
    
  633.             '<input type="text" name="test" value="%(banduuid)s" '
    
  634.             'class="vForeignKeyRawIdAdminField vUUIDField">'
    
  635.             '<a href="/admin_widgets/band/?_to_field=uuid" class="related-lookup" '
    
  636.             'id="lookup_id_test" title="Lookup"></a>&nbsp;<strong>'
    
  637.             '<a href="/admin_widgets/band/%(bandpk)s/change/">Linkin Park</a>'
    
  638.             "</strong>" % {"banduuid": band.uuid, "bandpk": band.pk},
    
  639.         )
    
  640. 
    
  641.         rel_id = ReleaseEvent._meta.get_field("album").remote_field
    
  642.         w = widgets.ForeignKeyRawIdWidget(rel_id, widget_admin_site)
    
  643.         self.assertHTMLEqual(
    
  644.             w.render("test", None, attrs={}),
    
  645.             '<input type="text" name="test" class="vForeignKeyRawIdAdminField">'
    
  646.             '<a href="/admin_widgets/album/?_to_field=id" class="related-lookup" '
    
  647.             'id="lookup_id_test" title="Lookup"></a>',
    
  648.         )
    
  649. 
    
  650.     def test_relations_to_non_primary_key(self):
    
  651.         # ForeignKeyRawIdWidget works with fields which aren't related to
    
  652.         # the model's primary key.
    
  653.         apple = Inventory.objects.create(barcode=86, name="Apple")
    
  654.         Inventory.objects.create(barcode=22, name="Pear")
    
  655.         core = Inventory.objects.create(barcode=87, name="Core", parent=apple)
    
  656.         rel = Inventory._meta.get_field("parent").remote_field
    
  657.         w = widgets.ForeignKeyRawIdWidget(rel, widget_admin_site)
    
  658.         self.assertHTMLEqual(
    
  659.             w.render("test", core.parent_id, attrs={}),
    
  660.             '<input type="text" name="test" value="86" '
    
  661.             'class="vForeignKeyRawIdAdminField">'
    
  662.             '<a href="/admin_widgets/inventory/?_to_field=barcode" '
    
  663.             'class="related-lookup" id="lookup_id_test" title="Lookup"></a>'
    
  664.             '&nbsp;<strong><a href="/admin_widgets/inventory/%(pk)s/change/">'
    
  665.             "Apple</a></strong>" % {"pk": apple.pk},
    
  666.         )
    
  667. 
    
  668.     def test_fk_related_model_not_in_admin(self):
    
  669.         # FK to a model not registered with admin site. Raw ID widget should
    
  670.         # have no magnifying glass link. See #16542
    
  671.         big_honeycomb = Honeycomb.objects.create(location="Old tree")
    
  672.         big_honeycomb.bee_set.create()
    
  673.         rel = Bee._meta.get_field("honeycomb").remote_field
    
  674. 
    
  675.         w = widgets.ForeignKeyRawIdWidget(rel, widget_admin_site)
    
  676.         self.assertHTMLEqual(
    
  677.             w.render("honeycomb_widget", big_honeycomb.pk, attrs={}),
    
  678.             '<input type="text" name="honeycomb_widget" value="%(hcombpk)s">'
    
  679.             "&nbsp;<strong>%(hcomb)s</strong>"
    
  680.             % {"hcombpk": big_honeycomb.pk, "hcomb": big_honeycomb},
    
  681.         )
    
  682. 
    
  683.     def test_fk_to_self_model_not_in_admin(self):
    
  684.         # FK to self, not registered with admin site. Raw ID widget should have
    
  685.         # no magnifying glass link. See #16542
    
  686.         subject1 = Individual.objects.create(name="Subject #1")
    
  687.         Individual.objects.create(name="Child", parent=subject1)
    
  688.         rel = Individual._meta.get_field("parent").remote_field
    
  689. 
    
  690.         w = widgets.ForeignKeyRawIdWidget(rel, widget_admin_site)
    
  691.         self.assertHTMLEqual(
    
  692.             w.render("individual_widget", subject1.pk, attrs={}),
    
  693.             '<input type="text" name="individual_widget" value="%(subj1pk)s">'
    
  694.             "&nbsp;<strong>%(subj1)s</strong>"
    
  695.             % {"subj1pk": subject1.pk, "subj1": subject1},
    
  696.         )
    
  697. 
    
  698.     def test_proper_manager_for_label_lookup(self):
    
  699.         # see #9258
    
  700.         rel = Inventory._meta.get_field("parent").remote_field
    
  701.         w = widgets.ForeignKeyRawIdWidget(rel, widget_admin_site)
    
  702. 
    
  703.         hidden = Inventory.objects.create(barcode=93, name="Hidden", hidden=True)
    
  704.         child_of_hidden = Inventory.objects.create(
    
  705.             barcode=94, name="Child of hidden", parent=hidden
    
  706.         )
    
  707.         self.assertHTMLEqual(
    
  708.             w.render("test", child_of_hidden.parent_id, attrs={}),
    
  709.             '<input type="text" name="test" value="93" '
    
  710.             '   class="vForeignKeyRawIdAdminField">'
    
  711.             '<a href="/admin_widgets/inventory/?_to_field=barcode" '
    
  712.             'class="related-lookup" id="lookup_id_test" title="Lookup"></a>'
    
  713.             '&nbsp;<strong><a href="/admin_widgets/inventory/%(pk)s/change/">'
    
  714.             "Hidden</a></strong>" % {"pk": hidden.pk},
    
  715.         )
    
  716. 
    
  717.     def test_render_unsafe_limit_choices_to(self):
    
  718.         rel = UnsafeLimitChoicesTo._meta.get_field("band").remote_field
    
  719.         w = widgets.ForeignKeyRawIdWidget(rel, widget_admin_site)
    
  720.         self.assertHTMLEqual(
    
  721.             w.render("test", None),
    
  722.             '<input type="text" name="test" class="vForeignKeyRawIdAdminField">\n'
    
  723.             '<a href="/admin_widgets/band/?name=%22%26%3E%3Cescapeme&amp;'
    
  724.             '_to_field=artist_ptr" class="related-lookup" id="lookup_id_test" '
    
  725.             'title="Lookup"></a>',
    
  726.         )
    
  727. 
    
  728.     def test_render_fk_as_pk_model(self):
    
  729.         rel = VideoStream._meta.get_field("release_event").remote_field
    
  730.         w = widgets.ForeignKeyRawIdWidget(rel, widget_admin_site)
    
  731.         self.assertHTMLEqual(
    
  732.             w.render("test", None),
    
  733.             '<input type="text" name="test" class="vForeignKeyRawIdAdminField">\n'
    
  734.             '<a href="/admin_widgets/releaseevent/?_to_field=album" '
    
  735.             'class="related-lookup" id="lookup_id_test" title="Lookup"></a>',
    
  736.         )
    
  737. 
    
  738. 
    
  739. @override_settings(ROOT_URLCONF="admin_widgets.urls")
    
  740. class ManyToManyRawIdWidgetTest(TestCase):
    
  741.     def test_render(self):
    
  742.         band = Band.objects.create(name="Linkin Park")
    
  743. 
    
  744.         m1 = Member.objects.create(name="Chester")
    
  745.         m2 = Member.objects.create(name="Mike")
    
  746.         band.members.add(m1, m2)
    
  747.         rel = Band._meta.get_field("members").remote_field
    
  748. 
    
  749.         w = widgets.ManyToManyRawIdWidget(rel, widget_admin_site)
    
  750.         self.assertHTMLEqual(
    
  751.             w.render("test", [m1.pk, m2.pk], attrs={}),
    
  752.             (
    
  753.                 '<input type="text" name="test" value="%(m1pk)s,%(m2pk)s" '
    
  754.                 '   class="vManyToManyRawIdAdminField">'
    
  755.                 '<a href="/admin_widgets/member/" class="related-lookup" '
    
  756.                 '   id="lookup_id_test" title="Lookup"></a>'
    
  757.             )
    
  758.             % {"m1pk": m1.pk, "m2pk": m2.pk},
    
  759.         )
    
  760. 
    
  761.         self.assertHTMLEqual(
    
  762.             w.render("test", [m1.pk]),
    
  763.             (
    
  764.                 '<input type="text" name="test" value="%(m1pk)s" '
    
  765.                 '   class="vManyToManyRawIdAdminField">'
    
  766.                 '<a href="/admin_widgets/member/" class="related-lookup" '
    
  767.                 '   id="lookup_id_test" title="Lookup"></a>'
    
  768.             )
    
  769.             % {"m1pk": m1.pk},
    
  770.         )
    
  771. 
    
  772.     def test_m2m_related_model_not_in_admin(self):
    
  773.         # M2M relationship with model not registered with admin site. Raw ID
    
  774.         # widget should have no magnifying glass link. See #16542
    
  775.         consultor1 = Advisor.objects.create(name="Rockstar Techie")
    
  776. 
    
  777.         c1 = Company.objects.create(name="Doodle")
    
  778.         c2 = Company.objects.create(name="Pear")
    
  779.         consultor1.companies.add(c1, c2)
    
  780.         rel = Advisor._meta.get_field("companies").remote_field
    
  781. 
    
  782.         w = widgets.ManyToManyRawIdWidget(rel, widget_admin_site)
    
  783.         self.assertHTMLEqual(
    
  784.             w.render("company_widget1", [c1.pk, c2.pk], attrs={}),
    
  785.             '<input type="text" name="company_widget1" value="%(c1pk)s,%(c2pk)s">'
    
  786.             % {"c1pk": c1.pk, "c2pk": c2.pk},
    
  787.         )
    
  788. 
    
  789.         self.assertHTMLEqual(
    
  790.             w.render("company_widget2", [c1.pk]),
    
  791.             '<input type="text" name="company_widget2" value="%(c1pk)s">'
    
  792.             % {"c1pk": c1.pk},
    
  793.         )
    
  794. 
    
  795. 
    
  796. @override_settings(ROOT_URLCONF="admin_widgets.urls")
    
  797. class RelatedFieldWidgetWrapperTests(SimpleTestCase):
    
  798.     def test_no_can_add_related(self):
    
  799.         rel = Individual._meta.get_field("parent").remote_field
    
  800.         w = widgets.AdminRadioSelect()
    
  801.         # Used to fail with a name error.
    
  802.         w = widgets.RelatedFieldWidgetWrapper(w, rel, widget_admin_site)
    
  803.         self.assertFalse(w.can_add_related)
    
  804. 
    
  805.     def test_select_multiple_widget_cant_change_delete_related(self):
    
  806.         rel = Individual._meta.get_field("parent").remote_field
    
  807.         widget = forms.SelectMultiple()
    
  808.         wrapper = widgets.RelatedFieldWidgetWrapper(
    
  809.             widget,
    
  810.             rel,
    
  811.             widget_admin_site,
    
  812.             can_add_related=True,
    
  813.             can_change_related=True,
    
  814.             can_delete_related=True,
    
  815.         )
    
  816.         self.assertTrue(wrapper.can_add_related)
    
  817.         self.assertFalse(wrapper.can_change_related)
    
  818.         self.assertFalse(wrapper.can_delete_related)
    
  819. 
    
  820.     def test_on_delete_cascade_rel_cant_delete_related(self):
    
  821.         rel = Individual._meta.get_field("soulmate").remote_field
    
  822.         widget = forms.Select()
    
  823.         wrapper = widgets.RelatedFieldWidgetWrapper(
    
  824.             widget,
    
  825.             rel,
    
  826.             widget_admin_site,
    
  827.             can_add_related=True,
    
  828.             can_change_related=True,
    
  829.             can_delete_related=True,
    
  830.         )
    
  831.         self.assertTrue(wrapper.can_add_related)
    
  832.         self.assertTrue(wrapper.can_change_related)
    
  833.         self.assertFalse(wrapper.can_delete_related)
    
  834. 
    
  835.     def test_custom_widget_render(self):
    
  836.         class CustomWidget(forms.Select):
    
  837.             def render(self, *args, **kwargs):
    
  838.                 return "custom render output"
    
  839. 
    
  840.         rel = Album._meta.get_field("band").remote_field
    
  841.         widget = CustomWidget()
    
  842.         wrapper = widgets.RelatedFieldWidgetWrapper(
    
  843.             widget,
    
  844.             rel,
    
  845.             widget_admin_site,
    
  846.             can_add_related=True,
    
  847.             can_change_related=True,
    
  848.             can_delete_related=True,
    
  849.         )
    
  850.         output = wrapper.render("name", "value")
    
  851.         self.assertIn("custom render output", output)
    
  852. 
    
  853.     def test_widget_delegates_value_omitted_from_data(self):
    
  854.         class CustomWidget(forms.Select):
    
  855.             def value_omitted_from_data(self, data, files, name):
    
  856.                 return False
    
  857. 
    
  858.         rel = Album._meta.get_field("band").remote_field
    
  859.         widget = CustomWidget()
    
  860.         wrapper = widgets.RelatedFieldWidgetWrapper(widget, rel, widget_admin_site)
    
  861.         self.assertIs(wrapper.value_omitted_from_data({}, {}, "band"), False)
    
  862. 
    
  863.     def test_widget_is_hidden(self):
    
  864.         rel = Album._meta.get_field("band").remote_field
    
  865.         widget = forms.HiddenInput()
    
  866.         widget.choices = ()
    
  867.         wrapper = widgets.RelatedFieldWidgetWrapper(widget, rel, widget_admin_site)
    
  868.         self.assertIs(wrapper.is_hidden, True)
    
  869.         context = wrapper.get_context("band", None, {})
    
  870.         self.assertIs(context["is_hidden"], True)
    
  871.         output = wrapper.render("name", "value")
    
  872.         # Related item links are hidden.
    
  873.         self.assertNotIn("<a ", output)
    
  874. 
    
  875.     def test_widget_is_not_hidden(self):
    
  876.         rel = Album._meta.get_field("band").remote_field
    
  877.         widget = forms.Select()
    
  878.         wrapper = widgets.RelatedFieldWidgetWrapper(widget, rel, widget_admin_site)
    
  879.         self.assertIs(wrapper.is_hidden, False)
    
  880.         context = wrapper.get_context("band", None, {})
    
  881.         self.assertIs(context["is_hidden"], False)
    
  882.         output = wrapper.render("name", "value")
    
  883.         # Related item links are present.
    
  884.         self.assertIn("<a ", output)
    
  885. 
    
  886. 
    
  887. @override_settings(ROOT_URLCONF="admin_widgets.urls")
    
  888. class AdminWidgetSeleniumTestCase(AdminSeleniumTestCase):
    
  889.     available_apps = ["admin_widgets"] + AdminSeleniumTestCase.available_apps
    
  890. 
    
  891.     def setUp(self):
    
  892.         self.u1 = User.objects.create_superuser(
    
  893.             username="super", password="secret", email="[email protected]"
    
  894.         )
    
  895. 
    
  896. 
    
  897. class DateTimePickerSeleniumTests(AdminWidgetSeleniumTestCase):
    
  898.     def test_show_hide_date_time_picker_widgets(self):
    
  899.         """
    
  900.         Pressing the ESC key or clicking on a widget value closes the date and
    
  901.         time picker widgets.
    
  902.         """
    
  903.         from selenium.webdriver.common.by import By
    
  904.         from selenium.webdriver.common.keys import Keys
    
  905. 
    
  906.         self.admin_login(username="super", password="secret", login_url="/")
    
  907.         # Open a page that has a date and time picker widgets
    
  908.         self.selenium.get(
    
  909.             self.live_server_url + reverse("admin:admin_widgets_member_add")
    
  910.         )
    
  911. 
    
  912.         # First, with the date picker widget ---------------------------------
    
  913.         cal_icon = self.selenium.find_element(By.ID, "calendarlink0")
    
  914.         # The date picker is hidden
    
  915.         self.assertFalse(
    
  916.             self.selenium.find_element(By.ID, "calendarbox0").is_displayed()
    
  917.         )
    
  918.         # Click the calendar icon
    
  919.         cal_icon.click()
    
  920.         # The date picker is visible
    
  921.         self.assertTrue(
    
  922.             self.selenium.find_element(By.ID, "calendarbox0").is_displayed()
    
  923.         )
    
  924.         # Press the ESC key
    
  925.         self.selenium.find_element(By.TAG_NAME, "body").send_keys([Keys.ESCAPE])
    
  926.         # The date picker is hidden again
    
  927.         self.assertFalse(
    
  928.             self.selenium.find_element(By.ID, "calendarbox0").is_displayed()
    
  929.         )
    
  930.         # Click the calendar icon, then on the 15th of current month
    
  931.         cal_icon.click()
    
  932.         self.selenium.find_element(By.XPATH, "//a[contains(text(), '15')]").click()
    
  933.         self.assertFalse(
    
  934.             self.selenium.find_element(By.ID, "calendarbox0").is_displayed()
    
  935.         )
    
  936.         self.assertEqual(
    
  937.             self.selenium.find_element(By.ID, "id_birthdate_0").get_attribute("value"),
    
  938.             datetime.today().strftime("%Y-%m-") + "15",
    
  939.         )
    
  940. 
    
  941.         # Then, with the time picker widget ----------------------------------
    
  942.         time_icon = self.selenium.find_element(By.ID, "clocklink0")
    
  943.         # The time picker is hidden
    
  944.         self.assertFalse(self.selenium.find_element(By.ID, "clockbox0").is_displayed())
    
  945.         # Click the time icon
    
  946.         time_icon.click()
    
  947.         # The time picker is visible
    
  948.         self.assertTrue(self.selenium.find_element(By.ID, "clockbox0").is_displayed())
    
  949.         self.assertEqual(
    
  950.             [
    
  951.                 x.text
    
  952.                 for x in self.selenium.find_elements(
    
  953.                     By.XPATH, "//ul[@class='timelist']/li/a"
    
  954.                 )
    
  955.             ],
    
  956.             ["Now", "Midnight", "6 a.m.", "Noon", "6 p.m."],
    
  957.         )
    
  958.         # Press the ESC key
    
  959.         self.selenium.find_element(By.TAG_NAME, "body").send_keys([Keys.ESCAPE])
    
  960.         # The time picker is hidden again
    
  961.         self.assertFalse(self.selenium.find_element(By.ID, "clockbox0").is_displayed())
    
  962.         # Click the time icon, then select the 'Noon' value
    
  963.         time_icon.click()
    
  964.         self.selenium.find_element(By.XPATH, "//a[contains(text(), 'Noon')]").click()
    
  965.         self.assertFalse(self.selenium.find_element(By.ID, "clockbox0").is_displayed())
    
  966.         self.assertEqual(
    
  967.             self.selenium.find_element(By.ID, "id_birthdate_1").get_attribute("value"),
    
  968.             "12:00:00",
    
  969.         )
    
  970. 
    
  971.     def test_calendar_nonday_class(self):
    
  972.         """
    
  973.         Ensure cells that are not days of the month have the `nonday` CSS class.
    
  974.         Refs #4574.
    
  975.         """
    
  976.         from selenium.webdriver.common.by import By
    
  977. 
    
  978.         self.admin_login(username="super", password="secret", login_url="/")
    
  979.         # Open a page that has a date and time picker widgets
    
  980.         self.selenium.get(
    
  981.             self.live_server_url + reverse("admin:admin_widgets_member_add")
    
  982.         )
    
  983. 
    
  984.         # fill in the birth date.
    
  985.         self.selenium.find_element(By.ID, "id_birthdate_0").send_keys("2013-06-01")
    
  986. 
    
  987.         # Click the calendar icon
    
  988.         self.selenium.find_element(By.ID, "calendarlink0").click()
    
  989. 
    
  990.         # get all the tds within the calendar
    
  991.         calendar0 = self.selenium.find_element(By.ID, "calendarin0")
    
  992.         tds = calendar0.find_elements(By.TAG_NAME, "td")
    
  993. 
    
  994.         # make sure the first and last 6 cells have class nonday
    
  995.         for td in tds[:6] + tds[-6:]:
    
  996.             self.assertEqual(td.get_attribute("class"), "nonday")
    
  997. 
    
  998.     def test_calendar_selected_class(self):
    
  999.         """
    
  1000.         Ensure cell for the day in the input has the `selected` CSS class.
    
  1001.         Refs #4574.
    
  1002.         """
    
  1003.         from selenium.webdriver.common.by import By
    
  1004. 
    
  1005.         self.admin_login(username="super", password="secret", login_url="/")
    
  1006.         # Open a page that has a date and time picker widgets
    
  1007.         self.selenium.get(
    
  1008.             self.live_server_url + reverse("admin:admin_widgets_member_add")
    
  1009.         )
    
  1010. 
    
  1011.         # fill in the birth date.
    
  1012.         self.selenium.find_element(By.ID, "id_birthdate_0").send_keys("2013-06-01")
    
  1013. 
    
  1014.         # Click the calendar icon
    
  1015.         self.selenium.find_element(By.ID, "calendarlink0").click()
    
  1016. 
    
  1017.         # get all the tds within the calendar
    
  1018.         calendar0 = self.selenium.find_element(By.ID, "calendarin0")
    
  1019.         tds = calendar0.find_elements(By.TAG_NAME, "td")
    
  1020. 
    
  1021.         # verify the selected cell
    
  1022.         selected = tds[6]
    
  1023.         self.assertEqual(selected.get_attribute("class"), "selected")
    
  1024. 
    
  1025.         self.assertEqual(selected.text, "1")
    
  1026. 
    
  1027.     def test_calendar_no_selected_class(self):
    
  1028.         """
    
  1029.         Ensure no cells are given the selected class when the field is empty.
    
  1030.         Refs #4574.
    
  1031.         """
    
  1032.         from selenium.webdriver.common.by import By
    
  1033. 
    
  1034.         self.admin_login(username="super", password="secret", login_url="/")
    
  1035.         # Open a page that has a date and time picker widgets
    
  1036.         self.selenium.get(
    
  1037.             self.live_server_url + reverse("admin:admin_widgets_member_add")
    
  1038.         )
    
  1039. 
    
  1040.         # Click the calendar icon
    
  1041.         self.selenium.find_element(By.ID, "calendarlink0").click()
    
  1042. 
    
  1043.         # get all the tds within the calendar
    
  1044.         calendar0 = self.selenium.find_element(By.ID, "calendarin0")
    
  1045.         tds = calendar0.find_elements(By.TAG_NAME, "td")
    
  1046. 
    
  1047.         # verify there are no cells with the selected class
    
  1048.         selected = [td for td in tds if td.get_attribute("class") == "selected"]
    
  1049. 
    
  1050.         self.assertEqual(len(selected), 0)
    
  1051. 
    
  1052.     def test_calendar_show_date_from_input(self):
    
  1053.         """
    
  1054.         The calendar shows the date from the input field for every locale
    
  1055.         supported by Django.
    
  1056.         """
    
  1057.         from selenium.webdriver.common.by import By
    
  1058. 
    
  1059.         self.selenium.set_window_size(1024, 768)
    
  1060.         self.admin_login(username="super", password="secret", login_url="/")
    
  1061. 
    
  1062.         # Enter test data
    
  1063.         member = Member.objects.create(
    
  1064.             name="Bob", birthdate=datetime(1984, 5, 15), gender="M"
    
  1065.         )
    
  1066. 
    
  1067.         # Get month name translations for every locale
    
  1068.         month_string = "May"
    
  1069.         path = os.path.join(
    
  1070.             os.path.dirname(import_module("django.contrib.admin").__file__), "locale"
    
  1071.         )
    
  1072.         for language_code, language_name in settings.LANGUAGES:
    
  1073.             try:
    
  1074.                 catalog = gettext.translation("djangojs", path, [language_code])
    
  1075.             except OSError:
    
  1076.                 continue
    
  1077.             if month_string in catalog._catalog:
    
  1078.                 month_name = catalog._catalog[month_string]
    
  1079.             else:
    
  1080.                 month_name = month_string
    
  1081. 
    
  1082.             # Get the expected caption
    
  1083.             may_translation = month_name
    
  1084.             expected_caption = "{:s} {:d}".format(may_translation.upper(), 1984)
    
  1085. 
    
  1086.             # Test with every locale
    
  1087.             with override_settings(LANGUAGE_CODE=language_code):
    
  1088.                 # Open a page that has a date picker widget
    
  1089.                 url = reverse("admin:admin_widgets_member_change", args=(member.pk,))
    
  1090.                 self.selenium.get(self.live_server_url + url)
    
  1091.                 # Click on the calendar icon
    
  1092.                 self.selenium.find_element(By.ID, "calendarlink0").click()
    
  1093.                 # Make sure that the right month and year are displayed
    
  1094.                 self.wait_for_text("#calendarin0 caption", expected_caption)
    
  1095. 
    
  1096. 
    
  1097. @override_settings(TIME_ZONE="Asia/Singapore")
    
  1098. class DateTimePickerShortcutsSeleniumTests(AdminWidgetSeleniumTestCase):
    
  1099.     def test_date_time_picker_shortcuts(self):
    
  1100.         """
    
  1101.         date/time/datetime picker shortcuts work in the current time zone.
    
  1102.         Refs #20663.
    
  1103. 
    
  1104.         This test case is fairly tricky, it relies on selenium still running the browser
    
  1105.         in the default time zone "America/Chicago" despite `override_settings` changing
    
  1106.         the time zone to "Asia/Singapore".
    
  1107.         """
    
  1108.         from selenium.webdriver.common.by import By
    
  1109. 
    
  1110.         self.admin_login(username="super", password="secret", login_url="/")
    
  1111. 
    
  1112.         error_margin = timedelta(seconds=10)
    
  1113. 
    
  1114.         # If we are neighbouring a DST, we add an hour of error margin.
    
  1115.         tz = zoneinfo.ZoneInfo("America/Chicago")
    
  1116.         utc_now = datetime.now(zoneinfo.ZoneInfo("UTC"))
    
  1117.         tz_yesterday = (utc_now - timedelta(days=1)).astimezone(tz).tzname()
    
  1118.         tz_tomorrow = (utc_now + timedelta(days=1)).astimezone(tz).tzname()
    
  1119.         if tz_yesterday != tz_tomorrow:
    
  1120.             error_margin += timedelta(hours=1)
    
  1121. 
    
  1122.         self.selenium.get(
    
  1123.             self.live_server_url + reverse("admin:admin_widgets_member_add")
    
  1124.         )
    
  1125. 
    
  1126.         self.selenium.find_element(By.ID, "id_name").send_keys("test")
    
  1127. 
    
  1128.         # Click on the "today" and "now" shortcuts.
    
  1129.         shortcuts = self.selenium.find_elements(
    
  1130.             By.CSS_SELECTOR, ".field-birthdate .datetimeshortcuts"
    
  1131.         )
    
  1132. 
    
  1133.         now = datetime.now()
    
  1134.         for shortcut in shortcuts:
    
  1135.             shortcut.find_element(By.TAG_NAME, "a").click()
    
  1136. 
    
  1137.         # There is a time zone mismatch warning.
    
  1138.         # Warning: This would effectively fail if the TIME_ZONE defined in the
    
  1139.         # settings has the same UTC offset as "Asia/Singapore" because the
    
  1140.         # mismatch warning would be rightfully missing from the page.
    
  1141.         self.assertCountSeleniumElements(".field-birthdate .timezonewarning", 1)
    
  1142. 
    
  1143.         # Submit the form.
    
  1144.         with self.wait_page_loaded():
    
  1145.             self.selenium.find_element(By.NAME, "_save").click()
    
  1146. 
    
  1147.         # Make sure that "now" in JavaScript is within 10 seconds
    
  1148.         # from "now" on the server side.
    
  1149.         member = Member.objects.get(name="test")
    
  1150.         self.assertGreater(member.birthdate, now - error_margin)
    
  1151.         self.assertLess(member.birthdate, now + error_margin)
    
  1152. 
    
  1153. 
    
  1154. # The above tests run with Asia/Singapore which are on the positive side of
    
  1155. # UTC. Here we test with a timezone on the negative side.
    
  1156. @override_settings(TIME_ZONE="US/Eastern")
    
  1157. class DateTimePickerAltTimezoneSeleniumTests(DateTimePickerShortcutsSeleniumTests):
    
  1158.     pass
    
  1159. 
    
  1160. 
    
  1161. class HorizontalVerticalFilterSeleniumTests(AdminWidgetSeleniumTestCase):
    
  1162.     def setUp(self):
    
  1163.         super().setUp()
    
  1164.         self.lisa = Student.objects.create(name="Lisa")
    
  1165.         self.john = Student.objects.create(name="John")
    
  1166.         self.bob = Student.objects.create(name="Bob")
    
  1167.         self.peter = Student.objects.create(name="Peter")
    
  1168.         self.jenny = Student.objects.create(name="Jenny")
    
  1169.         self.jason = Student.objects.create(name="Jason")
    
  1170.         self.cliff = Student.objects.create(name="Cliff")
    
  1171.         self.arthur = Student.objects.create(name="Arthur")
    
  1172.         self.school = School.objects.create(name="School of Awesome")
    
  1173. 
    
  1174.     def assertActiveButtons(
    
  1175.         self, mode, field_name, choose, remove, choose_all=None, remove_all=None
    
  1176.     ):
    
  1177.         choose_link = "#id_%s_add_link" % field_name
    
  1178.         choose_all_link = "#id_%s_add_all_link" % field_name
    
  1179.         remove_link = "#id_%s_remove_link" % field_name
    
  1180.         remove_all_link = "#id_%s_remove_all_link" % field_name
    
  1181.         self.assertEqual(self.has_css_class(choose_link, "active"), choose)
    
  1182.         self.assertEqual(self.has_css_class(remove_link, "active"), remove)
    
  1183.         if mode == "horizontal":
    
  1184.             self.assertEqual(self.has_css_class(choose_all_link, "active"), choose_all)
    
  1185.             self.assertEqual(self.has_css_class(remove_all_link, "active"), remove_all)
    
  1186. 
    
  1187.     def execute_basic_operations(self, mode, field_name):
    
  1188.         from selenium.webdriver.common.by import By
    
  1189. 
    
  1190.         original_url = self.selenium.current_url
    
  1191. 
    
  1192.         from_box = "#id_%s_from" % field_name
    
  1193.         to_box = "#id_%s_to" % field_name
    
  1194.         choose_link = "id_%s_add_link" % field_name
    
  1195.         choose_all_link = "id_%s_add_all_link" % field_name
    
  1196.         remove_link = "id_%s_remove_link" % field_name
    
  1197.         remove_all_link = "id_%s_remove_all_link" % field_name
    
  1198. 
    
  1199.         # Initial positions ---------------------------------------------------
    
  1200.         self.assertSelectOptions(
    
  1201.             from_box,
    
  1202.             [
    
  1203.                 str(self.arthur.id),
    
  1204.                 str(self.bob.id),
    
  1205.                 str(self.cliff.id),
    
  1206.                 str(self.jason.id),
    
  1207.                 str(self.jenny.id),
    
  1208.                 str(self.john.id),
    
  1209.             ],
    
  1210.         )
    
  1211.         self.assertSelectOptions(to_box, [str(self.lisa.id), str(self.peter.id)])
    
  1212.         self.assertActiveButtons(mode, field_name, False, False, True, True)
    
  1213. 
    
  1214.         # Click 'Choose all' --------------------------------------------------
    
  1215.         if mode == "horizontal":
    
  1216.             self.selenium.find_element(By.ID, choose_all_link).click()
    
  1217.         elif mode == "vertical":
    
  1218.             # There 's no 'Choose all' button in vertical mode, so individually
    
  1219.             # select all options and click 'Choose'.
    
  1220.             for option in self.selenium.find_elements(
    
  1221.                 By.CSS_SELECTOR, from_box + " > option"
    
  1222.             ):
    
  1223.                 option.click()
    
  1224.             self.selenium.find_element(By.ID, choose_link).click()
    
  1225.         self.assertSelectOptions(from_box, [])
    
  1226.         self.assertSelectOptions(
    
  1227.             to_box,
    
  1228.             [
    
  1229.                 str(self.lisa.id),
    
  1230.                 str(self.peter.id),
    
  1231.                 str(self.arthur.id),
    
  1232.                 str(self.bob.id),
    
  1233.                 str(self.cliff.id),
    
  1234.                 str(self.jason.id),
    
  1235.                 str(self.jenny.id),
    
  1236.                 str(self.john.id),
    
  1237.             ],
    
  1238.         )
    
  1239.         self.assertActiveButtons(mode, field_name, False, False, False, True)
    
  1240. 
    
  1241.         # Click 'Remove all' --------------------------------------------------
    
  1242.         if mode == "horizontal":
    
  1243.             self.selenium.find_element(By.ID, remove_all_link).click()
    
  1244.         elif mode == "vertical":
    
  1245.             # There 's no 'Remove all' button in vertical mode, so individually
    
  1246.             # select all options and click 'Remove'.
    
  1247.             for option in self.selenium.find_elements(
    
  1248.                 By.CSS_SELECTOR, to_box + " > option"
    
  1249.             ):
    
  1250.                 option.click()
    
  1251.             self.selenium.find_element(By.ID, remove_link).click()
    
  1252.         self.assertSelectOptions(
    
  1253.             from_box,
    
  1254.             [
    
  1255.                 str(self.lisa.id),
    
  1256.                 str(self.peter.id),
    
  1257.                 str(self.arthur.id),
    
  1258.                 str(self.bob.id),
    
  1259.                 str(self.cliff.id),
    
  1260.                 str(self.jason.id),
    
  1261.                 str(self.jenny.id),
    
  1262.                 str(self.john.id),
    
  1263.             ],
    
  1264.         )
    
  1265.         self.assertSelectOptions(to_box, [])
    
  1266.         self.assertActiveButtons(mode, field_name, False, False, True, False)
    
  1267. 
    
  1268.         # Choose some options ------------------------------------------------
    
  1269.         from_lisa_select_option = self.selenium.find_element(
    
  1270.             By.CSS_SELECTOR, '{} > option[value="{}"]'.format(from_box, self.lisa.id)
    
  1271.         )
    
  1272. 
    
  1273.         # Check the title attribute is there for tool tips: ticket #20821
    
  1274.         self.assertEqual(
    
  1275.             from_lisa_select_option.get_attribute("title"),
    
  1276.             from_lisa_select_option.get_attribute("text"),
    
  1277.         )
    
  1278. 
    
  1279.         self.select_option(from_box, str(self.lisa.id))
    
  1280.         self.select_option(from_box, str(self.jason.id))
    
  1281.         self.select_option(from_box, str(self.bob.id))
    
  1282.         self.select_option(from_box, str(self.john.id))
    
  1283.         self.assertActiveButtons(mode, field_name, True, False, True, False)
    
  1284.         self.selenium.find_element(By.ID, choose_link).click()
    
  1285.         self.assertActiveButtons(mode, field_name, False, False, True, True)
    
  1286. 
    
  1287.         self.assertSelectOptions(
    
  1288.             from_box,
    
  1289.             [
    
  1290.                 str(self.peter.id),
    
  1291.                 str(self.arthur.id),
    
  1292.                 str(self.cliff.id),
    
  1293.                 str(self.jenny.id),
    
  1294.             ],
    
  1295.         )
    
  1296.         self.assertSelectOptions(
    
  1297.             to_box,
    
  1298.             [
    
  1299.                 str(self.lisa.id),
    
  1300.                 str(self.bob.id),
    
  1301.                 str(self.jason.id),
    
  1302.                 str(self.john.id),
    
  1303.             ],
    
  1304.         )
    
  1305. 
    
  1306.         # Check the tooltip is still there after moving: ticket #20821
    
  1307.         to_lisa_select_option = self.selenium.find_element(
    
  1308.             By.CSS_SELECTOR, '{} > option[value="{}"]'.format(to_box, self.lisa.id)
    
  1309.         )
    
  1310.         self.assertEqual(
    
  1311.             to_lisa_select_option.get_attribute("title"),
    
  1312.             to_lisa_select_option.get_attribute("text"),
    
  1313.         )
    
  1314. 
    
  1315.         # Remove some options -------------------------------------------------
    
  1316.         self.select_option(to_box, str(self.lisa.id))
    
  1317.         self.select_option(to_box, str(self.bob.id))
    
  1318.         self.assertActiveButtons(mode, field_name, False, True, True, True)
    
  1319.         self.selenium.find_element(By.ID, remove_link).click()
    
  1320.         self.assertActiveButtons(mode, field_name, False, False, True, True)
    
  1321. 
    
  1322.         self.assertSelectOptions(
    
  1323.             from_box,
    
  1324.             [
    
  1325.                 str(self.peter.id),
    
  1326.                 str(self.arthur.id),
    
  1327.                 str(self.cliff.id),
    
  1328.                 str(self.jenny.id),
    
  1329.                 str(self.lisa.id),
    
  1330.                 str(self.bob.id),
    
  1331.             ],
    
  1332.         )
    
  1333.         self.assertSelectOptions(to_box, [str(self.jason.id), str(self.john.id)])
    
  1334. 
    
  1335.         # Choose some more options --------------------------------------------
    
  1336.         self.select_option(from_box, str(self.arthur.id))
    
  1337.         self.select_option(from_box, str(self.cliff.id))
    
  1338.         self.selenium.find_element(By.ID, choose_link).click()
    
  1339. 
    
  1340.         self.assertSelectOptions(
    
  1341.             from_box,
    
  1342.             [
    
  1343.                 str(self.peter.id),
    
  1344.                 str(self.jenny.id),
    
  1345.                 str(self.lisa.id),
    
  1346.                 str(self.bob.id),
    
  1347.             ],
    
  1348.         )
    
  1349.         self.assertSelectOptions(
    
  1350.             to_box,
    
  1351.             [
    
  1352.                 str(self.jason.id),
    
  1353.                 str(self.john.id),
    
  1354.                 str(self.arthur.id),
    
  1355.                 str(self.cliff.id),
    
  1356.             ],
    
  1357.         )
    
  1358. 
    
  1359.         # Choose some more options --------------------------------------------
    
  1360.         self.select_option(from_box, str(self.peter.id))
    
  1361.         self.select_option(from_box, str(self.lisa.id))
    
  1362. 
    
  1363.         # Confirm they're selected after clicking inactive buttons: ticket #26575
    
  1364.         self.assertSelectedOptions(from_box, [str(self.peter.id), str(self.lisa.id)])
    
  1365.         self.selenium.find_element(By.ID, remove_link).click()
    
  1366.         self.assertSelectedOptions(from_box, [str(self.peter.id), str(self.lisa.id)])
    
  1367. 
    
  1368.         # Unselect the options ------------------------------------------------
    
  1369.         self.deselect_option(from_box, str(self.peter.id))
    
  1370.         self.deselect_option(from_box, str(self.lisa.id))
    
  1371. 
    
  1372.         # Choose some more options --------------------------------------------
    
  1373.         self.select_option(to_box, str(self.jason.id))
    
  1374.         self.select_option(to_box, str(self.john.id))
    
  1375. 
    
  1376.         # Confirm they're selected after clicking inactive buttons: ticket #26575
    
  1377.         self.assertSelectedOptions(to_box, [str(self.jason.id), str(self.john.id)])
    
  1378.         self.selenium.find_element(By.ID, choose_link).click()
    
  1379.         self.assertSelectedOptions(to_box, [str(self.jason.id), str(self.john.id)])
    
  1380. 
    
  1381.         # Unselect the options ------------------------------------------------
    
  1382.         self.deselect_option(to_box, str(self.jason.id))
    
  1383.         self.deselect_option(to_box, str(self.john.id))
    
  1384. 
    
  1385.         # Pressing buttons shouldn't change the URL.
    
  1386.         self.assertEqual(self.selenium.current_url, original_url)
    
  1387. 
    
  1388.     def test_basic(self):
    
  1389.         from selenium.webdriver.common.by import By
    
  1390. 
    
  1391.         self.selenium.set_window_size(1024, 768)
    
  1392.         self.school.students.set([self.lisa, self.peter])
    
  1393.         self.school.alumni.set([self.lisa, self.peter])
    
  1394. 
    
  1395.         self.admin_login(username="super", password="secret", login_url="/")
    
  1396.         self.selenium.get(
    
  1397.             self.live_server_url
    
  1398.             + reverse("admin:admin_widgets_school_change", args=(self.school.id,))
    
  1399.         )
    
  1400. 
    
  1401.         self.wait_page_ready()
    
  1402.         self.execute_basic_operations("vertical", "students")
    
  1403.         self.execute_basic_operations("horizontal", "alumni")
    
  1404. 
    
  1405.         # Save and check that everything is properly stored in the database ---
    
  1406.         self.selenium.find_element(By.XPATH, '//input[@value="Save"]').click()
    
  1407.         self.wait_page_ready()
    
  1408.         self.school = School.objects.get(id=self.school.id)  # Reload from database
    
  1409.         self.assertEqual(
    
  1410.             list(self.school.students.all()),
    
  1411.             [self.arthur, self.cliff, self.jason, self.john],
    
  1412.         )
    
  1413.         self.assertEqual(
    
  1414.             list(self.school.alumni.all()),
    
  1415.             [self.arthur, self.cliff, self.jason, self.john],
    
  1416.         )
    
  1417. 
    
  1418.     def test_filter(self):
    
  1419.         """
    
  1420.         Typing in the search box filters out options displayed in the 'from'
    
  1421.         box.
    
  1422.         """
    
  1423.         from selenium.webdriver.common.by import By
    
  1424.         from selenium.webdriver.common.keys import Keys
    
  1425. 
    
  1426.         self.selenium.set_window_size(1024, 768)
    
  1427.         self.school.students.set([self.lisa, self.peter])
    
  1428.         self.school.alumni.set([self.lisa, self.peter])
    
  1429. 
    
  1430.         self.admin_login(username="super", password="secret", login_url="/")
    
  1431.         self.selenium.get(
    
  1432.             self.live_server_url
    
  1433.             + reverse("admin:admin_widgets_school_change", args=(self.school.id,))
    
  1434.         )
    
  1435. 
    
  1436.         for field_name in ["students", "alumni"]:
    
  1437.             from_box = "#id_%s_from" % field_name
    
  1438.             to_box = "#id_%s_to" % field_name
    
  1439.             choose_link = "id_%s_add_link" % field_name
    
  1440.             remove_link = "id_%s_remove_link" % field_name
    
  1441.             input = self.selenium.find_element(By.ID, "id_%s_input" % field_name)
    
  1442. 
    
  1443.             # Initial values
    
  1444.             self.assertSelectOptions(
    
  1445.                 from_box,
    
  1446.                 [
    
  1447.                     str(self.arthur.id),
    
  1448.                     str(self.bob.id),
    
  1449.                     str(self.cliff.id),
    
  1450.                     str(self.jason.id),
    
  1451.                     str(self.jenny.id),
    
  1452.                     str(self.john.id),
    
  1453.                 ],
    
  1454.             )
    
  1455. 
    
  1456.             # Typing in some characters filters out non-matching options
    
  1457.             input.send_keys("a")
    
  1458.             self.assertSelectOptions(
    
  1459.                 from_box, [str(self.arthur.id), str(self.jason.id)]
    
  1460.             )
    
  1461.             input.send_keys("R")
    
  1462.             self.assertSelectOptions(from_box, [str(self.arthur.id)])
    
  1463. 
    
  1464.             # Clearing the text box makes the other options reappear
    
  1465.             input.send_keys([Keys.BACK_SPACE])
    
  1466.             self.assertSelectOptions(
    
  1467.                 from_box, [str(self.arthur.id), str(self.jason.id)]
    
  1468.             )
    
  1469.             input.send_keys([Keys.BACK_SPACE])
    
  1470.             self.assertSelectOptions(
    
  1471.                 from_box,
    
  1472.                 [
    
  1473.                     str(self.arthur.id),
    
  1474.                     str(self.bob.id),
    
  1475.                     str(self.cliff.id),
    
  1476.                     str(self.jason.id),
    
  1477.                     str(self.jenny.id),
    
  1478.                     str(self.john.id),
    
  1479.                 ],
    
  1480.             )
    
  1481. 
    
  1482.             # -----------------------------------------------------------------
    
  1483.             # Choosing a filtered option sends it properly to the 'to' box.
    
  1484.             input.send_keys("a")
    
  1485.             self.assertSelectOptions(
    
  1486.                 from_box, [str(self.arthur.id), str(self.jason.id)]
    
  1487.             )
    
  1488.             self.select_option(from_box, str(self.jason.id))
    
  1489.             self.selenium.find_element(By.ID, choose_link).click()
    
  1490.             self.assertSelectOptions(from_box, [str(self.arthur.id)])
    
  1491.             self.assertSelectOptions(
    
  1492.                 to_box,
    
  1493.                 [
    
  1494.                     str(self.lisa.id),
    
  1495.                     str(self.peter.id),
    
  1496.                     str(self.jason.id),
    
  1497.                 ],
    
  1498.             )
    
  1499. 
    
  1500.             self.select_option(to_box, str(self.lisa.id))
    
  1501.             self.selenium.find_element(By.ID, remove_link).click()
    
  1502.             self.assertSelectOptions(from_box, [str(self.arthur.id), str(self.lisa.id)])
    
  1503.             self.assertSelectOptions(to_box, [str(self.peter.id), str(self.jason.id)])
    
  1504. 
    
  1505.             input.send_keys([Keys.BACK_SPACE])  # Clear text box
    
  1506.             self.assertSelectOptions(
    
  1507.                 from_box,
    
  1508.                 [
    
  1509.                     str(self.arthur.id),
    
  1510.                     str(self.bob.id),
    
  1511.                     str(self.cliff.id),
    
  1512.                     str(self.jenny.id),
    
  1513.                     str(self.john.id),
    
  1514.                     str(self.lisa.id),
    
  1515.                 ],
    
  1516.             )
    
  1517.             self.assertSelectOptions(to_box, [str(self.peter.id), str(self.jason.id)])
    
  1518. 
    
  1519.             # -----------------------------------------------------------------
    
  1520.             # Pressing enter on a filtered option sends it properly to
    
  1521.             # the 'to' box.
    
  1522.             self.select_option(to_box, str(self.jason.id))
    
  1523.             self.selenium.find_element(By.ID, remove_link).click()
    
  1524.             input.send_keys("ja")
    
  1525.             self.assertSelectOptions(from_box, [str(self.jason.id)])
    
  1526.             input.send_keys([Keys.ENTER])
    
  1527.             self.assertSelectOptions(to_box, [str(self.peter.id), str(self.jason.id)])
    
  1528.             input.send_keys([Keys.BACK_SPACE, Keys.BACK_SPACE])
    
  1529. 
    
  1530.         # Save and check that everything is properly stored in the database ---
    
  1531.         with self.wait_page_loaded():
    
  1532.             self.selenium.find_element(By.XPATH, '//input[@value="Save"]').click()
    
  1533.         self.school = School.objects.get(id=self.school.id)  # Reload from database
    
  1534.         self.assertEqual(list(self.school.students.all()), [self.jason, self.peter])
    
  1535.         self.assertEqual(list(self.school.alumni.all()), [self.jason, self.peter])
    
  1536. 
    
  1537.     def test_back_button_bug(self):
    
  1538.         """
    
  1539.         Some browsers had a bug where navigating away from the change page
    
  1540.         and then clicking the browser's back button would clear the
    
  1541.         filter_horizontal/filter_vertical widgets (#13614).
    
  1542.         """
    
  1543.         from selenium.webdriver.common.by import By
    
  1544. 
    
  1545.         self.school.students.set([self.lisa, self.peter])
    
  1546.         self.school.alumni.set([self.lisa, self.peter])
    
  1547.         self.admin_login(username="super", password="secret", login_url="/")
    
  1548.         change_url = reverse(
    
  1549.             "admin:admin_widgets_school_change", args=(self.school.id,)
    
  1550.         )
    
  1551.         self.selenium.get(self.live_server_url + change_url)
    
  1552.         # Navigate away and go back to the change form page.
    
  1553.         self.selenium.find_element(By.LINK_TEXT, "Home").click()
    
  1554.         self.selenium.back()
    
  1555.         expected_unselected_values = [
    
  1556.             str(self.arthur.id),
    
  1557.             str(self.bob.id),
    
  1558.             str(self.cliff.id),
    
  1559.             str(self.jason.id),
    
  1560.             str(self.jenny.id),
    
  1561.             str(self.john.id),
    
  1562.         ]
    
  1563.         expected_selected_values = [str(self.lisa.id), str(self.peter.id)]
    
  1564.         # Everything is still in place
    
  1565.         self.assertSelectOptions("#id_students_from", expected_unselected_values)
    
  1566.         self.assertSelectOptions("#id_students_to", expected_selected_values)
    
  1567.         self.assertSelectOptions("#id_alumni_from", expected_unselected_values)
    
  1568.         self.assertSelectOptions("#id_alumni_to", expected_selected_values)
    
  1569. 
    
  1570.     def test_refresh_page(self):
    
  1571.         """
    
  1572.         Horizontal and vertical filter widgets keep selected options on page
    
  1573.         reload (#22955).
    
  1574.         """
    
  1575.         self.school.students.add(self.arthur, self.jason)
    
  1576.         self.school.alumni.add(self.arthur, self.jason)
    
  1577. 
    
  1578.         self.admin_login(username="super", password="secret", login_url="/")
    
  1579.         change_url = reverse(
    
  1580.             "admin:admin_widgets_school_change", args=(self.school.id,)
    
  1581.         )
    
  1582.         self.selenium.get(self.live_server_url + change_url)
    
  1583. 
    
  1584.         self.assertCountSeleniumElements("#id_students_to > option", 2)
    
  1585. 
    
  1586.         # self.selenium.refresh() or send_keys(Keys.F5) does hard reload and
    
  1587.         # doesn't replicate what happens when a user clicks the browser's
    
  1588.         # 'Refresh' button.
    
  1589.         with self.wait_page_loaded():
    
  1590.             self.selenium.execute_script("location.reload()")
    
  1591. 
    
  1592.         self.assertCountSeleniumElements("#id_students_to > option", 2)
    
  1593. 
    
  1594. 
    
  1595. class AdminRawIdWidgetSeleniumTests(AdminWidgetSeleniumTestCase):
    
  1596.     def setUp(self):
    
  1597.         super().setUp()
    
  1598.         Band.objects.create(id=42, name="Bogey Blues")
    
  1599.         Band.objects.create(id=98, name="Green Potatoes")
    
  1600. 
    
  1601.     def test_ForeignKey(self):
    
  1602.         from selenium.webdriver.common.by import By
    
  1603. 
    
  1604.         self.admin_login(username="super", password="secret", login_url="/")
    
  1605.         self.selenium.get(
    
  1606.             self.live_server_url + reverse("admin:admin_widgets_event_add")
    
  1607.         )
    
  1608.         main_window = self.selenium.current_window_handle
    
  1609. 
    
  1610.         # No value has been selected yet
    
  1611.         self.assertEqual(
    
  1612.             self.selenium.find_element(By.ID, "id_main_band").get_attribute("value"), ""
    
  1613.         )
    
  1614. 
    
  1615.         # Open the popup window and click on a band
    
  1616.         self.selenium.find_element(By.ID, "lookup_id_main_band").click()
    
  1617.         self.wait_for_and_switch_to_popup()
    
  1618.         link = self.selenium.find_element(By.LINK_TEXT, "Bogey Blues")
    
  1619.         self.assertIn("/band/42/", link.get_attribute("href"))
    
  1620.         link.click()
    
  1621. 
    
  1622.         # The field now contains the selected band's id
    
  1623.         self.selenium.switch_to.window(main_window)
    
  1624.         self.wait_for_value("#id_main_band", "42")
    
  1625. 
    
  1626.         # Reopen the popup window and click on another band
    
  1627.         self.selenium.find_element(By.ID, "lookup_id_main_band").click()
    
  1628.         self.wait_for_and_switch_to_popup()
    
  1629.         link = self.selenium.find_element(By.LINK_TEXT, "Green Potatoes")
    
  1630.         self.assertIn("/band/98/", link.get_attribute("href"))
    
  1631.         link.click()
    
  1632. 
    
  1633.         # The field now contains the other selected band's id
    
  1634.         self.selenium.switch_to.window(main_window)
    
  1635.         self.wait_for_value("#id_main_band", "98")
    
  1636. 
    
  1637.     def test_many_to_many(self):
    
  1638.         from selenium.webdriver.common.by import By
    
  1639. 
    
  1640.         self.admin_login(username="super", password="secret", login_url="/")
    
  1641.         self.selenium.get(
    
  1642.             self.live_server_url + reverse("admin:admin_widgets_event_add")
    
  1643.         )
    
  1644.         main_window = self.selenium.current_window_handle
    
  1645. 
    
  1646.         # No value has been selected yet
    
  1647.         self.assertEqual(
    
  1648.             self.selenium.find_element(By.ID, "id_supporting_bands").get_attribute(
    
  1649.                 "value"
    
  1650.             ),
    
  1651.             "",
    
  1652.         )
    
  1653. 
    
  1654.         # Help text for the field is displayed
    
  1655.         self.assertEqual(
    
  1656.             self.selenium.find_element(
    
  1657.                 By.CSS_SELECTOR, ".field-supporting_bands div.help"
    
  1658.             ).text,
    
  1659.             "Supporting Bands.",
    
  1660.         )
    
  1661. 
    
  1662.         # Open the popup window and click on a band
    
  1663.         self.selenium.find_element(By.ID, "lookup_id_supporting_bands").click()
    
  1664.         self.wait_for_and_switch_to_popup()
    
  1665.         link = self.selenium.find_element(By.LINK_TEXT, "Bogey Blues")
    
  1666.         self.assertIn("/band/42/", link.get_attribute("href"))
    
  1667.         link.click()
    
  1668. 
    
  1669.         # The field now contains the selected band's id
    
  1670.         self.selenium.switch_to.window(main_window)
    
  1671.         self.wait_for_value("#id_supporting_bands", "42")
    
  1672. 
    
  1673.         # Reopen the popup window and click on another band
    
  1674.         self.selenium.find_element(By.ID, "lookup_id_supporting_bands").click()
    
  1675.         self.wait_for_and_switch_to_popup()
    
  1676.         link = self.selenium.find_element(By.LINK_TEXT, "Green Potatoes")
    
  1677.         self.assertIn("/band/98/", link.get_attribute("href"))
    
  1678.         link.click()
    
  1679. 
    
  1680.         # The field now contains the two selected bands' ids
    
  1681.         self.selenium.switch_to.window(main_window)
    
  1682.         self.wait_for_value("#id_supporting_bands", "42,98")
    
  1683. 
    
  1684. 
    
  1685. class RelatedFieldWidgetSeleniumTests(AdminWidgetSeleniumTestCase):
    
  1686.     def test_ForeignKey_using_to_field(self):
    
  1687.         from selenium.webdriver.common.by import By
    
  1688.         from selenium.webdriver.support.ui import Select
    
  1689. 
    
  1690.         self.admin_login(username="super", password="secret", login_url="/")
    
  1691.         self.selenium.get(
    
  1692.             self.live_server_url + reverse("admin:admin_widgets_profile_add")
    
  1693.         )
    
  1694. 
    
  1695.         main_window = self.selenium.current_window_handle
    
  1696.         # Click the Add User button to add new
    
  1697.         self.selenium.find_element(By.ID, "add_id_user").click()
    
  1698.         self.wait_for_and_switch_to_popup()
    
  1699.         password_field = self.selenium.find_element(By.ID, "id_password")
    
  1700.         password_field.send_keys("password")
    
  1701. 
    
  1702.         username_field = self.selenium.find_element(By.ID, "id_username")
    
  1703.         username_value = "newuser"
    
  1704.         username_field.send_keys(username_value)
    
  1705. 
    
  1706.         save_button_css_selector = ".submit-row > input[type=submit]"
    
  1707.         self.selenium.find_element(By.CSS_SELECTOR, save_button_css_selector).click()
    
  1708.         self.selenium.switch_to.window(main_window)
    
  1709.         # The field now contains the new user
    
  1710.         self.selenium.find_element(By.CSS_SELECTOR, "#id_user option[value=newuser]")
    
  1711. 
    
  1712.         self.selenium.find_element(By.ID, "view_id_user").click()
    
  1713.         self.wait_for_value("#id_username", "newuser")
    
  1714.         self.selenium.back()
    
  1715. 
    
  1716.         # Chrome and Safari don't update related object links when selecting
    
  1717.         # the same option as previously submitted. As a consequence, the
    
  1718.         # "pencil" and "eye" buttons remain disable, so select "---------"
    
  1719.         # first.
    
  1720.         select = Select(self.selenium.find_element(By.ID, "id_user"))
    
  1721.         select.select_by_index(0)
    
  1722.         select.select_by_value("newuser")
    
  1723.         # Click the Change User button to change it
    
  1724.         self.selenium.find_element(By.ID, "change_id_user").click()
    
  1725.         self.wait_for_and_switch_to_popup()
    
  1726. 
    
  1727.         username_field = self.selenium.find_element(By.ID, "id_username")
    
  1728.         username_value = "changednewuser"
    
  1729.         username_field.clear()
    
  1730.         username_field.send_keys(username_value)
    
  1731. 
    
  1732.         save_button_css_selector = ".submit-row > input[type=submit]"
    
  1733.         self.selenium.find_element(By.CSS_SELECTOR, save_button_css_selector).click()
    
  1734.         self.selenium.switch_to.window(main_window)
    
  1735.         self.selenium.find_element(
    
  1736.             By.CSS_SELECTOR, "#id_user option[value=changednewuser]"
    
  1737.         )
    
  1738. 
    
  1739.         self.selenium.find_element(By.ID, "view_id_user").click()
    
  1740.         self.wait_for_value("#id_username", "changednewuser")
    
  1741.         self.selenium.back()
    
  1742. 
    
  1743.         select = Select(self.selenium.find_element(By.ID, "id_user"))
    
  1744.         select.select_by_value("changednewuser")
    
  1745.         # Go ahead and submit the form to make sure it works
    
  1746.         self.selenium.find_element(By.CSS_SELECTOR, save_button_css_selector).click()
    
  1747.         self.wait_for_text(
    
  1748.             "li.success", "The profile “changednewuser” was added successfully."
    
  1749.         )
    
  1750.         profiles = Profile.objects.all()
    
  1751.         self.assertEqual(len(profiles), 1)
    
  1752.         self.assertEqual(profiles[0].user.username, username_value)