summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormgaligniana <marcelogaligniana@gmail.com>2022-04-13 15:27:21 +0200
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2022-04-15 07:46:37 +0200
commitc72f6f36c13a21f6db3d4f85d2d3cec87bad45e6 (patch)
tree5a97d9e0ca88e853fec2436731b1ef0dcf206d93
parentdeedf5bbc347e47b3be6e15783fc43a9c0a69256 (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.js31
-rw-r--r--django/contrib/admin/templates/admin/widgets/related_widget_wrapper.html2
-rw-r--r--django/contrib/admin/widgets.py1
-rw-r--r--tests/admin_views/admin.py4
-rw-r--r--tests/admin_views/models.py36
-rw-r--r--tests/admin_views/tests.py141
-rw-r--r--tests/modeladmin/tests.py4
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>'