mirror of https://github.com/django/django.git
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.
This commit is contained in:
parent
deedf5bbc3
commit
c72f6f36c1
|
@ -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) {
|
function dismissAddRelatedObjectPopup(win, newId, newRepr) {
|
||||||
const name = removePopupIndex(win.name);
|
const name = removePopupIndex(win.name);
|
||||||
const elem = document.getElementById(name);
|
const elem = document.getElementById(name);
|
||||||
|
@ -94,6 +123,7 @@
|
||||||
const elemName = elem.nodeName.toUpperCase();
|
const elemName = elem.nodeName.toUpperCase();
|
||||||
if (elemName === 'SELECT') {
|
if (elemName === 'SELECT') {
|
||||||
elem.options[elem.options.length] = new Option(newRepr, newId, true, true);
|
elem.options[elem.options.length] = new Option(newRepr, newId, true, true);
|
||||||
|
updateRelatedSelectsOptions(elem, win, null, newRepr, newId);
|
||||||
} else if (elemName === 'INPUT') {
|
} else if (elemName === 'INPUT') {
|
||||||
if (elem.classList.contains('vManyToManyRawIdAdminField') && elem.value) {
|
if (elem.classList.contains('vManyToManyRawIdAdminField') && elem.value) {
|
||||||
elem.value += ',' + newId;
|
elem.value += ',' + newId;
|
||||||
|
@ -126,6 +156,7 @@
|
||||||
this.value = newId;
|
this.value = newId;
|
||||||
}
|
}
|
||||||
}).trigger('change');
|
}).trigger('change');
|
||||||
|
updateRelatedSelectsOptions(selects[0], win, objId, newRepr, newId);
|
||||||
selects.next().find('.select2-selection__rendered').each(function() {
|
selects.next().find('.select2-selection__rendered').each(function() {
|
||||||
// The element can have a clear button as a child.
|
// The element can have a clear button as a child.
|
||||||
// Use the lastChild to modify only the displayed value.
|
// Use the lastChild to modify only the displayed value.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{% load i18n static %}
|
{% 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 }}
|
{{ rendered_widget }}
|
||||||
{% block links %}
|
{% block links %}
|
||||||
{% spaceless %}
|
{% spaceless %}
|
||||||
|
|
|
@ -318,6 +318,7 @@ class RelatedFieldWidgetWrapper(forms.Widget):
|
||||||
"can_change_related": self.can_change_related,
|
"can_change_related": self.can_change_related,
|
||||||
"can_delete_related": self.can_delete_related,
|
"can_delete_related": self.can_delete_related,
|
||||||
"can_view_related": self.can_view_related,
|
"can_view_related": self.can_view_related,
|
||||||
|
"model_has_limit_choices_to": self.rel.limit_choices_to,
|
||||||
}
|
}
|
||||||
if self.can_add_related:
|
if self.can_add_related:
|
||||||
context["add_related_url"] = self.get_related_url(info, "add")
|
context["add_related_url"] = self.get_related_url(info, "add")
|
||||||
|
|
|
@ -44,6 +44,7 @@ from .models import (
|
||||||
Color,
|
Color,
|
||||||
Color2,
|
Color2,
|
||||||
ComplexSortedPerson,
|
ComplexSortedPerson,
|
||||||
|
Country,
|
||||||
CoverLetter,
|
CoverLetter,
|
||||||
CustomArticle,
|
CustomArticle,
|
||||||
CyclicOne,
|
CyclicOne,
|
||||||
|
@ -126,6 +127,7 @@ from .models import (
|
||||||
Telegram,
|
Telegram,
|
||||||
Thing,
|
Thing,
|
||||||
Topping,
|
Topping,
|
||||||
|
Traveler,
|
||||||
UnchangeableObject,
|
UnchangeableObject,
|
||||||
UndeletableObject,
|
UndeletableObject,
|
||||||
UnorderedObject,
|
UnorderedObject,
|
||||||
|
@ -1284,6 +1286,8 @@ site.register(ExplicitlyProvidedPK, GetFormsetsArgumentCheckingAdmin)
|
||||||
site.register(ImplicitlyGeneratedPK, GetFormsetsArgumentCheckingAdmin)
|
site.register(ImplicitlyGeneratedPK, GetFormsetsArgumentCheckingAdmin)
|
||||||
site.register(UserProxy)
|
site.register(UserProxy)
|
||||||
site.register(Box)
|
site.register(Box)
|
||||||
|
site.register(Country)
|
||||||
|
site.register(Traveler)
|
||||||
|
|
||||||
# Register core models we need in our tests
|
# Register core models we need in our tests
|
||||||
site.register(User, UserAdmin)
|
site.register(User, UserAdmin)
|
||||||
|
|
|
@ -1098,3 +1098,39 @@ class Box(models.Model):
|
||||||
next_box = models.ForeignKey(
|
next_box = models.ForeignKey(
|
||||||
"self", null=True, on_delete=models.SET_NULL, blank=True
|
"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},
|
||||||
|
)
|
||||||
|
|
|
@ -134,6 +134,7 @@ from .models import (
|
||||||
Telegram,
|
Telegram,
|
||||||
TitleTranslation,
|
TitleTranslation,
|
||||||
Topping,
|
Topping,
|
||||||
|
Traveler,
|
||||||
UnchangeableObject,
|
UnchangeableObject,
|
||||||
UndeletableObject,
|
UndeletableObject,
|
||||||
UnorderedObject,
|
UnorderedObject,
|
||||||
|
@ -6275,6 +6276,146 @@ class SeleniumTests(AdminSeleniumTestCase):
|
||||||
finally:
|
finally:
|
||||||
self.selenium.set_window_size(current_size["width"], current_size["height"])
|
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")
|
@override_settings(ROOT_URLCONF="admin_views.urls")
|
||||||
class ReadonlyTest(AdminFieldExtractionMixin, TestCase):
|
class ReadonlyTest(AdminFieldExtractionMixin, TestCase):
|
||||||
|
|
|
@ -508,7 +508,7 @@ class ModelAdminTests(TestCase):
|
||||||
|
|
||||||
self.assertHTMLEqual(
|
self.assertHTMLEqual(
|
||||||
str(form["main_band"]),
|
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>'
|
'<select name="main_band" id="id_main_band" required>'
|
||||||
'<option value="" selected>---------</option>'
|
'<option value="" selected>---------</option>'
|
||||||
'<option value="%d">The Beatles</option>'
|
'<option value="%d">The Beatles</option>'
|
||||||
|
@ -531,7 +531,7 @@ class ModelAdminTests(TestCase):
|
||||||
|
|
||||||
self.assertHTMLEqual(
|
self.assertHTMLEqual(
|
||||||
str(form["main_band"]),
|
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>'
|
'<select name="main_band" id="id_main_band" required>'
|
||||||
'<option value="" selected>---------</option>'
|
'<option value="" selected>---------</option>'
|
||||||
'<option value="%d">The Doors</option>'
|
'<option value="%d">The Doors</option>'
|
||||||
|
|
Loading…
Reference in New Issue