From 81057645f61fe545f4f11737dbd3040043ed2436 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Fri, 1 Dec 2017 14:00:00 -0500 Subject: [PATCH] Fixed #28871 -- Fixed initialization of autocomplete widgets in "Add another" inlines. Also allowed autocomplete widgets to work on AdminSites with a name other than 'admin'. --- django/contrib/admin/options.py | 4 ++-- .../admin/static/admin/js/autocomplete.js | 9 ++++---- django/contrib/admin/widgets.py | 7 ++++--- tests/admin_views/test_autocomplete_view.py | 21 +++++++++++++++++++ .../admin_widgets/test_autocomplete_widget.py | 11 ++++++---- 5 files changed, 38 insertions(+), 14 deletions(-) diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 627c76be6a..a5374527fc 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -220,7 +220,7 @@ class BaseModelAdmin(metaclass=forms.MediaDefiningClass): db = kwargs.get('using') if db_field.name in self.get_autocomplete_fields(request): - kwargs['widget'] = AutocompleteSelect(db_field.remote_field, using=db) + kwargs['widget'] = AutocompleteSelect(db_field.remote_field, self.admin_site, using=db) elif db_field.name in self.raw_id_fields: kwargs['widget'] = widgets.ForeignKeyRawIdWidget(db_field.remote_field, self.admin_site, using=db) elif db_field.name in self.radio_fields: @@ -248,7 +248,7 @@ class BaseModelAdmin(metaclass=forms.MediaDefiningClass): autocomplete_fields = self.get_autocomplete_fields(request) if db_field.name in autocomplete_fields: - kwargs['widget'] = AutocompleteSelectMultiple(db_field.remote_field, using=db) + kwargs['widget'] = AutocompleteSelectMultiple(db_field.remote_field, self.admin_site, using=db) elif db_field.name in self.raw_id_fields: kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.remote_field, self.admin_site, using=db) elif db_field.name in list(self.filter_vertical) + list(self.filter_horizontal): diff --git a/django/contrib/admin/static/admin/js/autocomplete.js b/django/contrib/admin/static/admin/js/autocomplete.js index 15321f974d..65c0702dd9 100644 --- a/django/contrib/admin/static/admin/js/autocomplete.js +++ b/django/contrib/admin/static/admin/js/autocomplete.js @@ -24,15 +24,14 @@ }; $(function() { - $('.admin-autocomplete').djangoAdminSelect2(); + // Initialize all autocomplete widgets except the one in the template + // form used when a new formset is added. + $('.admin-autocomplete').not('[name*=__prefix__]').djangoAdminSelect2(); }); $(document).on('formset:added', (function() { return function(event, $newFormset) { - var $widget = $newFormset.find('.admin-autocomplete'); - // Exclude already initialized Select2 inputs. - $widget = $widget.not('.select2-hidden-accessible'); - return init($widget); + return $newFormset.find('.admin-autocomplete').djangoAdminSelect2(); }; })(this)); }(django.jQuery)); diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py index dfb288642e..c8ead0e0ae 100644 --- a/django/contrib/admin/widgets.py +++ b/django/contrib/admin/widgets.py @@ -400,10 +400,11 @@ class AutocompleteMixin: Renders the necessary data attributes for select2 and adds the static form media. """ - url_name = 'admin:%s_%s_autocomplete' + url_name = '%s:%s_%s_autocomplete' - def __init__(self, rel, attrs=None, choices=(), using=None): + def __init__(self, rel, admin_site, attrs=None, choices=(), using=None): self.rel = rel + self.admin_site = admin_site self.db = using self.choices = choices if attrs is not None: @@ -413,7 +414,7 @@ class AutocompleteMixin: def get_url(self): model = self.rel.model - return reverse(self.url_name % (model._meta.app_label, model._meta.model_name)) + return reverse(self.url_name % (self.admin_site.name, model._meta.app_label, model._meta.model_name)) def build_attrs(self, base_attrs, extra_attrs=None): """ diff --git a/tests/admin_views/test_autocomplete_view.py b/tests/admin_views/test_autocomplete_view.py index b8c12448e1..8db18d2468 100644 --- a/tests/admin_views/test_autocomplete_view.py +++ b/tests/admin_views/test_autocomplete_view.py @@ -17,6 +17,7 @@ PAGINATOR_SIZE = AutocompleteJsonView.paginate_by class AuthorAdmin(admin.ModelAdmin): + ordering = ['id'] search_fields = ['id'] @@ -229,3 +230,23 @@ class SeleniumTests(AdminSeleniumTestCase): search.send_keys(Keys.RETURN) select = Select(self.selenium.find_element_by_id('id_related_questions')) self.assertEqual(len(select.all_selected_options), 2) + + def test_inline_add_another_widgets(self): + def assertNoResults(row): + elem = row.find_element_by_css_selector('.select2-selection') + elem.click() # Open the autocomplete dropdown. + results = self.selenium.find_element_by_css_selector('.select2-results') + self.assertTrue(results.is_displayed()) + option = self.selenium.find_element_by_css_selector('.select2-results__option') + self.assertEqual(option.text, 'No results found') + + # Autocomplete works in rows present when the page loads. + self.selenium.get(self.live_server_url + reverse('autocomplete_admin:admin_views_book_add')) + rows = self.selenium.find_elements_by_css_selector('.dynamic-authorship_set') + self.assertEqual(len(rows), 3) + assertNoResults(rows[0]) + # Autocomplete works in rows added using the "Add another" button. + self.selenium.find_element_by_link_text('Add another Authorship').click() + rows = self.selenium.find_elements_by_css_selector('.dynamic-authorship_set') + self.assertEqual(len(rows), 4) + assertNoResults(rows[-1]) diff --git a/tests/admin_widgets/test_autocomplete_widget.py b/tests/admin_widgets/test_autocomplete_widget.py index fd79ef9369..28167dfe1c 100644 --- a/tests/admin_widgets/test_autocomplete_widget.py +++ b/tests/admin_widgets/test_autocomplete_widget.py @@ -1,4 +1,5 @@ from django import forms +from django.contrib import admin from django.contrib.admin.widgets import AutocompleteSelect from django.forms import ModelChoiceField from django.test import TestCase, override_settings @@ -14,10 +15,12 @@ class AlbumForm(forms.ModelForm): widgets = { 'band': AutocompleteSelect( Album._meta.get_field('band').remote_field, + admin.site, attrs={'class': 'my-class'}, ), 'featuring': AutocompleteSelect( Album._meta.get_field('featuring').remote_field, + admin.site, ) } @@ -25,7 +28,7 @@ class AlbumForm(forms.ModelForm): class NotRequiredBandForm(forms.Form): band = ModelChoiceField( queryset=Album.objects.all(), - widget=AutocompleteSelect(Album._meta.get_field('band').remote_field), + widget=AutocompleteSelect(Album._meta.get_field('band').remote_field, admin.site), required=False, ) @@ -33,7 +36,7 @@ class NotRequiredBandForm(forms.Form): class RequiredBandForm(forms.Form): band = ModelChoiceField( queryset=Album.objects.all(), - widget=AutocompleteSelect(Album._meta.get_field('band').remote_field), + widget=AutocompleteSelect(Album._meta.get_field('band').remote_field, admin.site), required=True, ) @@ -68,7 +71,7 @@ class AutocompleteMixinTests(TestCase): def test_get_url(self): rel = Album._meta.get_field('band').remote_field - w = AutocompleteSelect(rel) + w = AutocompleteSelect(rel, admin.site) url = w.get_url() self.assertEqual(url, '/admin_widgets/band/autocomplete/') @@ -130,4 +133,4 @@ class AutocompleteMixinTests(TestCase): else: expected_files = base_files with translation.override(lang): - self.assertEqual(AutocompleteSelect(rel).media._js, expected_files) + self.assertEqual(AutocompleteSelect(rel, admin.site).media._js, expected_files)