diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py
index fd0021c057..491c78870f 100644
--- a/django/contrib/admin/widgets.py
+++ b/django/contrib/admin/widgets.py
@@ -15,7 +15,7 @@ from django.template.loader import render_to_string
from django.utils import six
from django.utils.encoding import force_text
from django.utils.html import (
- escape, format_html, format_html_join, smart_urlquote,
+ escape, escapejs, format_html, format_html_join, smart_urlquote,
)
from django.utils.safestring import mark_safe
from django.utils.text import Truncator
@@ -50,7 +50,7 @@ class FilteredSelectMultiple(forms.SelectMultiple):
# TODO: "id_" is hard-coded here. This should instead use the correct
# API to determine the ID dynamically.
output.append('SelectFilter.init("id_%s", "%s", %s); });\n'
- % (name, self.verbose_name.replace('"', '\\"'), int(self.is_stacked)))
+ % (name, escapejs(self.verbose_name), int(self.is_stacked)))
return mark_safe(''.join(output))
diff --git a/tests/admin_inlines/test_templates.py b/tests/admin_inlines/test_templates.py
new file mode 100644
index 0000000000..1ed52c9115
--- /dev/null
+++ b/tests/admin_inlines/test_templates.py
@@ -0,0 +1,21 @@
+from __future__ import unicode_literals
+
+from django.template.loader import render_to_string
+from django.test import SimpleTestCase
+
+
+class TestTemplates(SimpleTestCase):
+ def test_javascript_escaping(self):
+ context = {
+ 'inline_admin_formset': {
+ 'formset': {'prefix': 'my-prefix'},
+ 'opts': {'verbose_name': 'verbose name\\'},
+ },
+ }
+ output = render_to_string('admin/edit_inline/stacked.html', context)
+ self.assertIn('prefix: "my\\u002Dprefix",', output)
+ self.assertIn('addText: "Add another Verbose name\\u005C"', output)
+
+ output = render_to_string('admin/edit_inline/tabular.html', context)
+ self.assertIn('prefix: "my\\u002Dprefix",', output)
+ self.assertIn('addText: "Add another Verbose name\\u005C"', output)
diff --git a/tests/admin_inlines/tests.py b/tests/admin_inlines/tests.py
index c262de7d27..19260baf4d 100644
--- a/tests/admin_inlines/tests.py
+++ b/tests/admin_inlines/tests.py
@@ -78,7 +78,7 @@ class TestInline(TestDataMixin, TestCase):
# The heading for the m2m inline block uses the right text
self.assertContains(response, '
Author-book relationships
')
# The "add another" label is correct
- self.assertContains(response, 'Add another Author-book relationship')
+ self.assertContains(response, 'Add another Author\\u002Dbook relationship')
# The '+' is dropped from the autogenerated form prefix (Author_books+)
self.assertContains(response, 'id="id_Author_books-TOTAL_FORMS"')
@@ -524,7 +524,7 @@ class TestInlinePermissions(TestCase):
response = self.client.get(reverse('admin:admin_inlines_author_add'))
# No change permission on books, so no inline
self.assertNotContains(response, '
Author-book relationships
')
- self.assertNotContains(response, 'Add another Author-Book Relationship')
+ self.assertNotContains(response, 'Add another Author\\u002DBook Relationship')
self.assertNotContains(response, 'id="id_Author_books-TOTAL_FORMS"')
def test_inline_add_fk_noperm(self):
@@ -538,7 +538,7 @@ class TestInlinePermissions(TestCase):
response = self.client.get(self.author_change_url)
# No change permission on books, so no inline
self.assertNotContains(response, '
Author-book relationships
')
- self.assertNotContains(response, 'Add another Author-Book Relationship')
+ self.assertNotContains(response, 'Add another Author\\u002DBook Relationship')
self.assertNotContains(response, 'id="id_Author_books-TOTAL_FORMS"')
def test_inline_change_fk_noperm(self):
@@ -554,7 +554,7 @@ class TestInlinePermissions(TestCase):
response = self.client.get(reverse('admin:admin_inlines_author_add'))
# No change permission on Books, so no inline
self.assertNotContains(response, '
Author-book relationships
')
- self.assertNotContains(response, 'Add another Author-Book Relationship')
+ self.assertNotContains(response, 'Add another Author\\u002DBook Relationship')
self.assertNotContains(response, 'id="id_Author_books-TOTAL_FORMS"')
def test_inline_add_fk_add_perm(self):
@@ -573,7 +573,7 @@ class TestInlinePermissions(TestCase):
response = self.client.get(self.author_change_url)
# No change permission on books, so no inline
self.assertNotContains(response, '
Author-book relationships
')
- self.assertNotContains(response, 'Add another Author-Book Relationship')
+ self.assertNotContains(response, 'Add another Author\\u002DBook Relationship')
self.assertNotContains(response, 'id="id_Author_books-TOTAL_FORMS"')
self.assertNotContains(response, 'id="id_Author_books-0-DELETE"')
@@ -583,7 +583,7 @@ class TestInlinePermissions(TestCase):
response = self.client.get(self.author_change_url)
# We have change perm on books, so we can add/change/delete inlines
self.assertContains(response, '