diff options
| author | mgaligniana <marcelogaligniana@gmail.com> | 2022-04-13 15:27:21 +0200 |
|---|---|---|
| committer | Mariusz Felisiak <felisiak.mariusz@gmail.com> | 2022-04-15 07:46:37 +0200 |
| commit | c72f6f36c13a21f6db3d4f85d2d3cec87bad45e6 (patch) | |
| tree | 5a97d9e0ca88e853fec2436731b1ef0dcf206d93 | |
| parent | deedf5bbc347e47b3be6e15783fc43a9c0a69256 (diff) | |
Fixed #11803 -- Allowed admin select widgets to display new related objects.
Adjusted admin javascript to add newly created related objects to
already loaded select widgets.
In this version, applies only where limit_choices_to is not set.
| -rw-r--r-- | django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js | 31 | ||||
| -rw-r--r-- | django/contrib/admin/templates/admin/widgets/related_widget_wrapper.html | 2 | ||||
| -rw-r--r-- | django/contrib/admin/widgets.py | 1 | ||||
| -rw-r--r-- | tests/admin_views/admin.py | 4 | ||||
| -rw-r--r-- | tests/admin_views/models.py | 36 | ||||
| -rw-r--r-- | tests/admin_views/tests.py | 141 | ||||
| -rw-r--r-- | tests/modeladmin/tests.py | 4 |
7 files changed, 216 insertions, 3 deletions
diff --git a/django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js b/django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js index 284d44ad62..5c9c0d833d 100644 --- a/django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js +++ b/django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js @@ -87,6 +87,35 @@ } } + function updateRelatedSelectsOptions(currentSelect, win, objId, newRepr, newId) { + // After create/edit a model from the options next to the current + // select (+ or :pencil:) update ForeignKey PK of the rest of selects + // in the page. + + const path = win.location.pathname; + // Extract the model from the popup url '.../<model>/add/' or + // '.../<model>/<id>/change/' depending the action (add or change). + const modelName = path.split('/')[path.split('/').length - (objId ? 4 : 3)]; + const selectsRelated = document.querySelectorAll(`[data-model-ref="${modelName}"] select`); + + selectsRelated.forEach(function(select) { + if (currentSelect === select) { + return; + } + + let option = select.querySelector(`option[value="${objId}"]`); + + if (!option) { + option = new Option(newRepr, newId); + select.options.add(option); + return; + } + + option.textContent = newRepr; + option.value = newId; + }); + } + function dismissAddRelatedObjectPopup(win, newId, newRepr) { const name = removePopupIndex(win.name); const elem = document.getElementById(name); @@ -94,6 +123,7 @@ const elemName = elem.nodeName.toUpperCase(); if (elemName === 'SELECT') { elem.options[elem.options.length] = new Option(newRepr, newId, true, true); + updateRelatedSelectsOptions(elem, win, null, newRepr, newId); } else if (elemName === 'INPUT') { if (elem.classList.contains('vManyToManyRawIdAdminField') && elem.value) { elem.value += ',' + newId; @@ -126,6 +156,7 @@ this.value = newId; } }).trigger('change'); + updateRelatedSelectsOptions(selects[0], win, objId, newRepr, newId); selects.next().find('.select2-selection__rendered').each(function() { // The element can have a clear button as a child. // Use the lastChild to modify only the displayed value. diff --git a/django/contrib/admin/templates/admin/widgets/related_widget_wrapper.html b/django/contrib/admin/templates/admin/widgets/related_widget_wrapper.html index 9f7e586003..6c285ea044 100644 --- a/django/contrib/admin/templates/admin/widgets/related_widget_wrapper.html +++ b/django/contrib/admin/templates/admin/widgets/related_widget_wrapper.html @@ -1,5 +1,5 @@ {% load i18n static %} -<div class="related-widget-wrapper"> +<div class="related-widget-wrapper" {% if not model_has_limit_choices_to %}data-model-ref="{{ model }}"{% endif %}> {{ rendered_widget }} {% block links %} {% spaceless %} diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py index a361968b27..d48500e356 100644 --- a/django/contrib/admin/widgets.py +++ b/django/contrib/admin/widgets.py @@ -318,6 +318,7 @@ class RelatedFieldWidgetWrapper(forms.Widget): "can_change_related": self.can_change_related, "can_delete_related": self.can_delete_related, "can_view_related": self.can_view_related, + "model_has_limit_choices_to": self.rel.limit_choices_to, } if self.can_add_related: context["add_related_url"] = self.get_related_url(info, "add") diff --git a/tests/admin_views/admin.py b/tests/admin_views/admin.py index b78746c0ff..996f7d5afc 100644 --- a/tests/admin_views/admin.py +++ b/tests/admin_views/admin.py @@ -44,6 +44,7 @@ from .models import ( Color, Color2, ComplexSortedPerson, + Country, CoverLetter, CustomArticle, CyclicOne, @@ -126,6 +127,7 @@ from .models import ( Telegram, Thing, Topping, + Traveler, UnchangeableObject, UndeletableObject, UnorderedObject, @@ -1284,6 +1286,8 @@ site.register(ExplicitlyProvidedPK, GetFormsetsArgumentCheckingAdmin) site.register(ImplicitlyGeneratedPK, GetFormsetsArgumentCheckingAdmin) site.register(UserProxy) site.register(Box) +site.register(Country) +site.register(Traveler) # Register core models we need in our tests site.register(User, UserAdmin) diff --git a/tests/admin_views/models.py b/tests/admin_views/models.py index f6632816e8..85facf8821 100644 --- a/tests/admin_views/models.py +++ b/tests/admin_views/models.py @@ -1098,3 +1098,39 @@ class Box(models.Model): next_box = models.ForeignKey( "self", null=True, on_delete=models.SET_NULL, blank=True ) + + +class Country(models.Model): + NORTH_AMERICA = "North America" + SOUTH_AMERICA = "South America" + EUROPE = "Europe" + ASIA = "Asia" + OCEANIA = "Oceania" + ANTARCTICA = "Antarctica" + + CONTINENT_CHOICES = [ + (NORTH_AMERICA, NORTH_AMERICA), + (SOUTH_AMERICA, SOUTH_AMERICA), + (EUROPE, EUROPE), + (ASIA, ASIA), + (OCEANIA, OCEANIA), + (ANTARCTICA, ANTARCTICA), + ] + name = models.CharField(max_length=80) + continent = models.CharField(max_length=13, choices=CONTINENT_CHOICES) + + def __str__(self): + return self.name + + +class Traveler(models.Model): + born_country = models.ForeignKey(Country, models.CASCADE) + living_country = models.ForeignKey( + Country, models.CASCADE, related_name="living_country_set" + ) + favorite_country_to_vacation = models.ForeignKey( + Country, + models.CASCADE, + related_name="favorite_country_to_vacation_set", + limit_choices_to={"continent": Country.ASIA}, + ) diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py index 6fb917bd34..72fac695dd 100644 --- a/tests/admin_views/tests.py +++ b/tests/admin_views/tests.py @@ -134,6 +134,7 @@ from .models import ( Telegram, TitleTranslation, Topping, + Traveler, UnchangeableObject, UndeletableObject, UnorderedObject, @@ -6275,6 +6276,146 @@ class SeleniumTests(AdminSeleniumTestCase): finally: self.selenium.set_window_size(current_size["width"], current_size["height"]) + def test_updating_related_objects_updates_fk_selects(self): + from selenium.webdriver.common.by import By + from selenium.webdriver.support.ui import Select + + born_country_select_id = "id_born_country" + living_country_select_id = "id_living_country" + favorite_country_to_vacation_select_id = "id_favorite_country_to_vacation" + continent_select_id = "id_continent" + + def _get_HTML_inside_element_by_id(id_): + return self.selenium.find_element(By.ID, id_).get_attribute("innerHTML") + + self.admin_login( + username="super", password="secret", login_url=reverse("admin:index") + ) + add_url = reverse("admin:admin_views_traveler_add") + self.selenium.get(self.live_server_url + add_url) + + # Add new Country from the born_country select. + self.selenium.find_element(By.ID, f"add_{born_country_select_id}").click() + self.wait_for_and_switch_to_popup() + self.selenium.find_element(By.ID, "id_name").send_keys("Argentina") + continent_select = Select( + self.selenium.find_element(By.ID, continent_select_id) + ) + continent_select.select_by_visible_text("South America") + self.selenium.find_element(By.CSS_SELECTOR, '[type="submit"]').click() + self.selenium.switch_to.window(self.selenium.window_handles[0]) + + self.assertHTMLEqual( + _get_HTML_inside_element_by_id(born_country_select_id), + """ + <option value="" selected="">---------</option> + <option value="1" selected="">Argentina</option> + """, + ) + self.assertHTMLEqual( + _get_HTML_inside_element_by_id(living_country_select_id), + """ + <option value="" selected="">---------</option> + <option value="1">Argentina</option> + """, + ) + # Argentina won't appear because favorite_country_to_vacation field has + # limit_choices_to. + self.assertHTMLEqual( + _get_HTML_inside_element_by_id(favorite_country_to_vacation_select_id), + '<option value="" selected="">---------</option>', + ) + + # Add new Country from the living_country select. + self.selenium.find_element(By.ID, f"add_{living_country_select_id}").click() + self.wait_for_and_switch_to_popup() + self.selenium.find_element(By.ID, "id_name").send_keys("Spain") + continent_select = Select( + self.selenium.find_element(By.ID, continent_select_id) + ) + continent_select.select_by_visible_text("Europe") + self.selenium.find_element(By.CSS_SELECTOR, '[type="submit"]').click() + self.selenium.switch_to.window(self.selenium.window_handles[0]) + + self.assertHTMLEqual( + _get_HTML_inside_element_by_id(born_country_select_id), + """ + <option value="" selected="">---------</option> + <option value="1" selected="">Argentina</option> + <option value="2">Spain</option> + """, + ) + self.assertHTMLEqual( + _get_HTML_inside_element_by_id(living_country_select_id), + """ + <option value="" selected="">---------</option> + <option value="1">Argentina</option> + <option value="2" selected="">Spain</option> + """, + ) + # Spain won't appear because favorite_country_to_vacation field has + # limit_choices_to. + self.assertHTMLEqual( + _get_HTML_inside_element_by_id(favorite_country_to_vacation_select_id), + '<option value="" selected="">---------</option>', + ) + + # Edit second Country created from living_country select. + favorite_select = Select( + self.selenium.find_element(By.ID, living_country_select_id) + ) + favorite_select.select_by_visible_text("Spain") + self.selenium.find_element(By.ID, f"change_{living_country_select_id}").click() + self.wait_for_and_switch_to_popup() + favorite_name_input = self.selenium.find_element(By.ID, "id_name") + favorite_name_input.clear() + favorite_name_input.send_keys("Italy") + self.selenium.find_element(By.CSS_SELECTOR, '[type="submit"]').click() + self.selenium.switch_to.window(self.selenium.window_handles[0]) + + self.assertHTMLEqual( + _get_HTML_inside_element_by_id(born_country_select_id), + """ + <option value="" selected="">---------</option> + <option value="1" selected="">Argentina</option> + <option value="2">Italy</option> + """, + ) + self.assertHTMLEqual( + _get_HTML_inside_element_by_id(living_country_select_id), + """ + <option value="" selected="">---------</option> + <option value="1">Argentina</option> + <option value="2" selected="">Italy</option> + """, + ) + # favorite_country_to_vacation field has no options. + self.assertHTMLEqual( + _get_HTML_inside_element_by_id(favorite_country_to_vacation_select_id), + '<option value="" selected="">---------</option>', + ) + + # Add a new Asian country. + self.selenium.find_element( + By.ID, f"add_{favorite_country_to_vacation_select_id}" + ).click() + self.wait_for_and_switch_to_popup() + favorite_name_input = self.selenium.find_element(By.ID, "id_name") + favorite_name_input.send_keys("Qatar") + continent_select = Select( + self.selenium.find_element(By.ID, continent_select_id) + ) + continent_select.select_by_visible_text("Asia") + self.selenium.find_element(By.CSS_SELECTOR, '[type="submit"]').click() + self.selenium.switch_to.window(self.selenium.window_handles[0]) + + # Submit the new Traveler. + self.selenium.find_element(By.CSS_SELECTOR, '[name="_save"]').click() + traveler = Traveler.objects.get() + self.assertEqual(traveler.born_country.name, "Argentina") + self.assertEqual(traveler.living_country.name, "Italy") + self.assertEqual(traveler.favorite_country_to_vacation.name, "Qatar") + @override_settings(ROOT_URLCONF="admin_views.urls") class ReadonlyTest(AdminFieldExtractionMixin, TestCase): diff --git a/tests/modeladmin/tests.py b/tests/modeladmin/tests.py index ede1a09212..c49a132f7c 100644 --- a/tests/modeladmin/tests.py +++ b/tests/modeladmin/tests.py @@ -508,7 +508,7 @@ class ModelAdminTests(TestCase): self.assertHTMLEqual( str(form["main_band"]), - '<div class="related-widget-wrapper">' + '<div class="related-widget-wrapper" data-model-ref="band">' '<select name="main_band" id="id_main_band" required>' '<option value="" selected>---------</option>' '<option value="%d">The Beatles</option>' @@ -531,7 +531,7 @@ class ModelAdminTests(TestCase): self.assertHTMLEqual( str(form["main_band"]), - '<div class="related-widget-wrapper">' + '<div class="related-widget-wrapper" data-model-ref="band">' '<select name="main_band" id="id_main_band" required>' '<option value="" selected>---------</option>' '<option value="%d">The Doors</option>' |
