mirror of https://github.com/django/django.git
Fixed #33029 -- Allowed multiple popups for self-related fields in admin.
This commit is contained in:
parent
37d9ea5d5c
commit
492ed60f23
1
AUTHORS
1
AUTHORS
|
@ -990,6 +990,7 @@ answer newbie questions, and generally made Django that much better:
|
||||||
Xia Kai <https://blog.xiaket.org/>
|
Xia Kai <https://blog.xiaket.org/>
|
||||||
Yann Fouillat <gagaro42@gmail.com>
|
Yann Fouillat <gagaro42@gmail.com>
|
||||||
Yann Malet
|
Yann Malet
|
||||||
|
Yash Jhunjhunwala
|
||||||
Yasushi Masuda <whosaysni@gmail.com>
|
Yasushi Masuda <whosaysni@gmail.com>
|
||||||
ye7cakf02@sneakemail.com
|
ye7cakf02@sneakemail.com
|
||||||
ymasuda@ethercube.com
|
ymasuda@ethercube.com
|
||||||
|
|
|
@ -4,14 +4,45 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
{
|
{
|
||||||
const $ = django.jQuery;
|
const $ = django.jQuery;
|
||||||
|
let popupIndex = 0;
|
||||||
|
const relatedWindows = [];
|
||||||
|
|
||||||
|
function dismissChildPopups() {
|
||||||
|
relatedWindows.forEach(function(win) {
|
||||||
|
if(!win.closed) {
|
||||||
|
win.dismissChildPopups();
|
||||||
|
win.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPopupIndex() {
|
||||||
|
if(document.getElementsByName("_popup").length > 0) {
|
||||||
|
const index = window.name.lastIndexOf("__") + 2;
|
||||||
|
popupIndex = parseInt(window.name.substring(index));
|
||||||
|
} else {
|
||||||
|
popupIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addPopupIndex(name) {
|
||||||
|
name = name + "__" + (popupIndex + 1);
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removePopupIndex(name) {
|
||||||
|
name = name.replace(new RegExp("__" + (popupIndex + 1) + "$"), '');
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
function showAdminPopup(triggeringLink, name_regexp, add_popup) {
|
function showAdminPopup(triggeringLink, name_regexp, add_popup) {
|
||||||
const name = triggeringLink.id.replace(name_regexp, '');
|
const name = addPopupIndex(triggeringLink.id.replace(name_regexp, ''));
|
||||||
const href = new URL(triggeringLink.href);
|
const href = new URL(triggeringLink.href);
|
||||||
if (add_popup) {
|
if (add_popup) {
|
||||||
href.searchParams.set('_popup', 1);
|
href.searchParams.set('_popup', 1);
|
||||||
}
|
}
|
||||||
const win = window.open(href, name, 'height=500,width=800,resizable=yes,scrollbars=yes');
|
const win = window.open(href, name, 'height=500,width=800,resizable=yes,scrollbars=yes');
|
||||||
|
relatedWindows.push(win);
|
||||||
win.focus();
|
win.focus();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -21,13 +52,17 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function dismissRelatedLookupPopup(win, chosenId) {
|
function dismissRelatedLookupPopup(win, chosenId) {
|
||||||
const name = win.name;
|
const name = removePopupIndex(win.name);
|
||||||
const elem = document.getElementById(name);
|
const elem = document.getElementById(name);
|
||||||
if (elem.classList.contains('vManyToManyRawIdAdminField') && elem.value) {
|
if (elem.classList.contains('vManyToManyRawIdAdminField') && elem.value) {
|
||||||
elem.value += ',' + chosenId;
|
elem.value += ',' + chosenId;
|
||||||
} else {
|
} else {
|
||||||
document.getElementById(name).value = chosenId;
|
document.getElementById(name).value = chosenId;
|
||||||
}
|
}
|
||||||
|
const index = relatedWindows.indexOf(win);
|
||||||
|
if (index > -1) {
|
||||||
|
relatedWindows.splice(index, 1);
|
||||||
|
}
|
||||||
win.close();
|
win.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,7 +88,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function dismissAddRelatedObjectPopup(win, newId, newRepr) {
|
function dismissAddRelatedObjectPopup(win, newId, newRepr) {
|
||||||
const name = win.name;
|
const name = removePopupIndex(win.name);
|
||||||
const elem = document.getElementById(name);
|
const elem = document.getElementById(name);
|
||||||
if (elem) {
|
if (elem) {
|
||||||
const elemName = elem.nodeName.toUpperCase();
|
const elemName = elem.nodeName.toUpperCase();
|
||||||
|
@ -74,11 +109,15 @@
|
||||||
SelectBox.add_to_cache(toId, o);
|
SelectBox.add_to_cache(toId, o);
|
||||||
SelectBox.redisplay(toId);
|
SelectBox.redisplay(toId);
|
||||||
}
|
}
|
||||||
|
const index = relatedWindows.indexOf(win);
|
||||||
|
if (index > -1) {
|
||||||
|
relatedWindows.splice(index, 1);
|
||||||
|
}
|
||||||
win.close();
|
win.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
function dismissChangeRelatedObjectPopup(win, objId, newRepr, newId) {
|
function dismissChangeRelatedObjectPopup(win, objId, newRepr, newId) {
|
||||||
const id = win.name.replace(/^edit_/, '');
|
const id = removePopupIndex(win.name.replace(/^edit_/, ''));
|
||||||
const selectsSelector = interpolate('#%s, #%s_from, #%s_to', [id, id, id]);
|
const selectsSelector = interpolate('#%s, #%s_from, #%s_to', [id, id, id]);
|
||||||
const selects = $(selectsSelector);
|
const selects = $(selectsSelector);
|
||||||
selects.find('option').each(function() {
|
selects.find('option').each(function() {
|
||||||
|
@ -93,11 +132,15 @@
|
||||||
this.lastChild.textContent = newRepr;
|
this.lastChild.textContent = newRepr;
|
||||||
this.title = newRepr;
|
this.title = newRepr;
|
||||||
});
|
});
|
||||||
|
const index = relatedWindows.indexOf(win);
|
||||||
|
if (index > -1) {
|
||||||
|
relatedWindows.splice(index, 1);
|
||||||
|
}
|
||||||
win.close();
|
win.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
function dismissDeleteRelatedObjectPopup(win, objId) {
|
function dismissDeleteRelatedObjectPopup(win, objId) {
|
||||||
const id = win.name.replace(/^delete_/, '');
|
const id = removePopupIndex(win.name.replace(/^delete_/, ''));
|
||||||
const selectsSelector = interpolate('#%s, #%s_from, #%s_to', [id, id, id]);
|
const selectsSelector = interpolate('#%s, #%s_from, #%s_to', [id, id, id]);
|
||||||
const selects = $(selectsSelector);
|
const selects = $(selectsSelector);
|
||||||
selects.find('option').each(function() {
|
selects.find('option').each(function() {
|
||||||
|
@ -105,6 +148,10 @@
|
||||||
$(this).remove();
|
$(this).remove();
|
||||||
}
|
}
|
||||||
}).trigger('change');
|
}).trigger('change');
|
||||||
|
const index = relatedWindows.indexOf(win);
|
||||||
|
if (index > -1) {
|
||||||
|
relatedWindows.splice(index, 1);
|
||||||
|
}
|
||||||
win.close();
|
win.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,12 +162,18 @@
|
||||||
window.dismissAddRelatedObjectPopup = dismissAddRelatedObjectPopup;
|
window.dismissAddRelatedObjectPopup = dismissAddRelatedObjectPopup;
|
||||||
window.dismissChangeRelatedObjectPopup = dismissChangeRelatedObjectPopup;
|
window.dismissChangeRelatedObjectPopup = dismissChangeRelatedObjectPopup;
|
||||||
window.dismissDeleteRelatedObjectPopup = dismissDeleteRelatedObjectPopup;
|
window.dismissDeleteRelatedObjectPopup = dismissDeleteRelatedObjectPopup;
|
||||||
|
window.dismissChildPopups = dismissChildPopups;
|
||||||
|
|
||||||
// Kept for backward compatibility
|
// Kept for backward compatibility
|
||||||
window.showAddAnotherPopup = showRelatedObjectPopup;
|
window.showAddAnotherPopup = showRelatedObjectPopup;
|
||||||
window.dismissAddAnotherPopup = dismissAddRelatedObjectPopup;
|
window.dismissAddAnotherPopup = dismissAddRelatedObjectPopup;
|
||||||
|
|
||||||
|
window.addEventListener('unload', function(evt) {
|
||||||
|
window.dismissChildPopups();
|
||||||
|
});
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
|
setPopupIndex();
|
||||||
$("a[data-popup-opener]").on('click', function(event) {
|
$("a[data-popup-opener]").on('click', function(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
opener.dismissRelatedLookupPopup(window, $(this).data("popup-opener"));
|
opener.dismissRelatedLookupPopup(window, $(this).data("popup-opener"));
|
||||||
|
|
|
@ -22,8 +22,8 @@ from .forms import MediaActionForm
|
||||||
from .models import (
|
from .models import (
|
||||||
Actor, AdminOrderedAdminMethod, AdminOrderedCallable, AdminOrderedField,
|
Actor, AdminOrderedAdminMethod, AdminOrderedCallable, AdminOrderedField,
|
||||||
AdminOrderedModelMethod, Album, Answer, Answer2, Article, BarAccount, Book,
|
AdminOrderedModelMethod, Album, Answer, Answer2, Article, BarAccount, Book,
|
||||||
Bookmark, Category, Chapter, ChapterXtra1, Child, ChildOfReferer, Choice,
|
Bookmark, Box, Category, Chapter, ChapterXtra1, Child, ChildOfReferer,
|
||||||
City, Collector, Color, Color2, ComplexSortedPerson, CoverLetter,
|
Choice, City, Collector, Color, Color2, ComplexSortedPerson, CoverLetter,
|
||||||
CustomArticle, CyclicOne, CyclicTwo, DependentChild, DooHickey, EmptyModel,
|
CustomArticle, CyclicOne, CyclicTwo, DependentChild, DooHickey, EmptyModel,
|
||||||
EmptyModelHidden, EmptyModelMixin, EmptyModelVisible, ExplicitlyProvidedPK,
|
EmptyModelHidden, EmptyModelMixin, EmptyModelVisible, ExplicitlyProvidedPK,
|
||||||
ExternalSubscriber, Fabric, FancyDoodad, FieldOverridePost,
|
ExternalSubscriber, Fabric, FancyDoodad, FieldOverridePost,
|
||||||
|
@ -1125,6 +1125,7 @@ site.register(NotReferenced)
|
||||||
site.register(ExplicitlyProvidedPK, GetFormsetsArgumentCheckingAdmin)
|
site.register(ExplicitlyProvidedPK, GetFormsetsArgumentCheckingAdmin)
|
||||||
site.register(ImplicitlyGeneratedPK, GetFormsetsArgumentCheckingAdmin)
|
site.register(ImplicitlyGeneratedPK, GetFormsetsArgumentCheckingAdmin)
|
||||||
site.register(UserProxy)
|
site.register(UserProxy)
|
||||||
|
site.register(Box)
|
||||||
|
|
||||||
# Register core models we need in our tests
|
# Register core models we need in our tests
|
||||||
site.register(User, UserAdmin)
|
site.register(User, UserAdmin)
|
||||||
|
|
|
@ -1050,3 +1050,9 @@ class ReadOnlyRelatedField(models.Model):
|
||||||
|
|
||||||
class Héllo(models.Model):
|
class Héllo(models.Model):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Box(models.Model):
|
||||||
|
title = models.CharField(max_length=100)
|
||||||
|
next_box = models.ForeignKey("self", null=True, on_delete=models.SET_NULL, blank=True)
|
||||||
|
next_box = models.ForeignKey("self", null=True, on_delete=models.SET_NULL, blank=True)
|
||||||
|
|
|
@ -48,8 +48,8 @@ from .admin import CityAdmin, site, site2
|
||||||
from .models import (
|
from .models import (
|
||||||
Actor, AdminOrderedAdminMethod, AdminOrderedCallable, AdminOrderedField,
|
Actor, AdminOrderedAdminMethod, AdminOrderedCallable, AdminOrderedField,
|
||||||
AdminOrderedModelMethod, Album, Answer, Answer2, Article, BarAccount, Book,
|
AdminOrderedModelMethod, Album, Answer, Answer2, Article, BarAccount, Book,
|
||||||
Bookmark, Category, Chapter, ChapterXtra1, ChapterXtra2, Character, Child,
|
Bookmark, Box, Category, Chapter, ChapterXtra1, ChapterXtra2, Character,
|
||||||
Choice, City, Collector, Color, ComplexSortedPerson, CoverLetter,
|
Child, Choice, City, Collector, Color, ComplexSortedPerson, CoverLetter,
|
||||||
CustomArticle, CyclicOne, CyclicTwo, DooHickey, Employee, EmptyModel,
|
CustomArticle, CyclicOne, CyclicTwo, DooHickey, Employee, EmptyModel,
|
||||||
Fabric, FancyDoodad, FieldOverridePost, FilteredManager, FooAccount,
|
Fabric, FancyDoodad, FieldOverridePost, FilteredManager, FooAccount,
|
||||||
FoodDelivery, FunkyTag, Gallery, Grommet, Inquisition, Language, Link,
|
FoodDelivery, FunkyTag, Gallery, Grommet, Inquisition, Language, Link,
|
||||||
|
@ -4983,6 +4983,76 @@ class SeleniumTests(AdminSeleniumTestCase):
|
||||||
50,
|
50,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_related_popup_index(self):
|
||||||
|
"""
|
||||||
|
Create a chain of 'self' related objects via popups.
|
||||||
|
"""
|
||||||
|
from selenium.webdriver.support.ui import Select
|
||||||
|
self.admin_login(username='super', password='secret', login_url=reverse('admin:index'))
|
||||||
|
add_url = reverse('admin:admin_views_box_add', current_app=site.name)
|
||||||
|
self.selenium.get(self.live_server_url + add_url)
|
||||||
|
|
||||||
|
self.selenium.find_element_by_id('add_id_next_box').click()
|
||||||
|
self.wait_for_and_switch_to_popup()
|
||||||
|
|
||||||
|
self.selenium.find_element_by_id('id_title').send_keys('test')
|
||||||
|
self.selenium.find_element_by_id('add_id_next_box').click()
|
||||||
|
self.wait_for_and_switch_to_popup(num_windows=3)
|
||||||
|
|
||||||
|
self.selenium.find_element_by_id('id_title').send_keys('test2')
|
||||||
|
self.selenium.find_element_by_id('add_id_next_box').click()
|
||||||
|
self.wait_for_and_switch_to_popup(num_windows=4)
|
||||||
|
|
||||||
|
self.selenium.find_element_by_id('id_title').send_keys('test3')
|
||||||
|
self.selenium.find_element_by_xpath('//input[@value="Save"]').click()
|
||||||
|
self.selenium.switch_to.window(self.selenium.window_handles[-1])
|
||||||
|
select = Select(self.selenium.find_element_by_id('id_next_box'))
|
||||||
|
next_box_id = str(Box.objects.get(title="test3").id)
|
||||||
|
self.assertEqual(select.first_selected_option.get_attribute('value'), next_box_id)
|
||||||
|
|
||||||
|
self.selenium.find_element_by_xpath('//input[@value="Save"]').click()
|
||||||
|
self.selenium.switch_to.window(self.selenium.window_handles[-1])
|
||||||
|
select = Select(self.selenium.find_element_by_id('id_next_box'))
|
||||||
|
next_box_id = str(Box.objects.get(title="test2").id)
|
||||||
|
self.assertEqual(select.first_selected_option.get_attribute('value'), next_box_id)
|
||||||
|
|
||||||
|
self.selenium.find_element_by_xpath('//input[@value="Save"]').click()
|
||||||
|
self.selenium.switch_to.window(self.selenium.window_handles[-1])
|
||||||
|
select = Select(self.selenium.find_element_by_id('id_next_box'))
|
||||||
|
next_box_id = str(Box.objects.get(title="test").id)
|
||||||
|
self.assertEqual(select.first_selected_option.get_attribute('value'), next_box_id)
|
||||||
|
|
||||||
|
def test_related_popup_incorrect_close(self):
|
||||||
|
"""
|
||||||
|
Cleanup child popups when closing a parent popup.
|
||||||
|
"""
|
||||||
|
self.admin_login(username='super', password='secret', login_url=reverse('admin:index'))
|
||||||
|
add_url = reverse('admin:admin_views_box_add', current_app=site.name)
|
||||||
|
self.selenium.get(self.live_server_url + add_url)
|
||||||
|
|
||||||
|
self.selenium.find_element_by_id('add_id_next_box').click()
|
||||||
|
self.wait_for_and_switch_to_popup()
|
||||||
|
|
||||||
|
test_window = self.selenium.current_window_handle
|
||||||
|
self.selenium.find_element_by_id('id_title').send_keys('test')
|
||||||
|
self.selenium.find_element_by_id('add_id_next_box').click()
|
||||||
|
self.wait_for_and_switch_to_popup(num_windows=3)
|
||||||
|
|
||||||
|
test2_window = self.selenium.current_window_handle
|
||||||
|
self.selenium.find_element_by_id('id_title').send_keys('test2')
|
||||||
|
self.selenium.find_element_by_id('add_id_next_box').click()
|
||||||
|
self.wait_for_and_switch_to_popup(num_windows=4)
|
||||||
|
self.assertEqual(len(self.selenium.window_handles), 4)
|
||||||
|
|
||||||
|
self.selenium.switch_to.window(test2_window)
|
||||||
|
self.selenium.find_element_by_xpath('//input[@value="Save"]').click()
|
||||||
|
self.assertEqual(len(self.selenium.window_handles), 2)
|
||||||
|
|
||||||
|
# Close final popup to clean up test.
|
||||||
|
self.selenium.switch_to.window(test_window)
|
||||||
|
self.selenium.find_element_by_xpath('//input[@value="Save"]').click()
|
||||||
|
self.selenium.switch_to.window(self.selenium.window_handles[-1])
|
||||||
|
|
||||||
|
|
||||||
@override_settings(ROOT_URLCONF='admin_views.urls')
|
@override_settings(ROOT_URLCONF='admin_views.urls')
|
||||||
class ReadonlyTest(AdminFieldExtractionMixin, TestCase):
|
class ReadonlyTest(AdminFieldExtractionMixin, TestCase):
|
||||||
|
|
Loading…
Reference in New Issue