summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThomas Grainger <tom@yplanapp.com>2015-11-23 10:46:19 +0000
committerTim Graham <timograham@gmail.com>2015-12-05 15:51:57 -0500
commitd638cdc42acec608c1967f44af6be32a477c239f (patch)
tree44e102c555476dae8f2f339b6fa11bb57f982386
parent105028eec64064c11dfd3d5415317ba1fab81214 (diff)
Fixed #25165 -- Removed inline JavaScript from the admin.
This allows setting a Content-Security-Policy HTTP header (refs #15727). Special thanks to blighj, the original author of this patch.
-rw-r--r--.eslintrc2
-rw-r--r--django/contrib/admin/actions.py1
-rw-r--r--django/contrib/admin/helpers.py16
-rw-r--r--django/contrib/admin/options.py31
-rw-r--r--django/contrib/admin/static/admin/js/SelectFilter2.js16
-rw-r--r--django/contrib/admin/static/admin/js/actions.js13
-rw-r--r--django/contrib/admin/static/admin/js/actions.min.js12
-rw-r--r--django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js117
-rw-r--r--django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js11
-rw-r--r--django/contrib/admin/static/admin/js/calendar.js14
-rw-r--r--django/contrib/admin/static/admin/js/cancel.js9
-rw-r--r--django/contrib/admin/static/admin/js/change_form.js46
-rw-r--r--django/contrib/admin/static/admin/js/inlines.js25
-rw-r--r--django/contrib/admin/static/admin/js/inlines.min.js19
-rw-r--r--django/contrib/admin/static/admin/js/popup_response.js16
-rw-r--r--django/contrib/admin/static/admin/js/prepopulate_init.js10
-rw-r--r--django/contrib/admin/templates/admin/actions.html7
-rw-r--r--django/contrib/admin/templates/admin/auth/user/add_form.html4
-rw-r--r--django/contrib/admin/templates/admin/auth/user/change_password.html1
-rw-r--r--django/contrib/admin/templates/admin/change_form.html47
-rw-r--r--django/contrib/admin/templates/admin/change_list.html9
-rw-r--r--django/contrib/admin/templates/admin/delete_confirmation.html9
-rw-r--r--django/contrib/admin/templates/admin/delete_selected_confirmation.html9
-rw-r--r--django/contrib/admin/templates/admin/edit_inline/stacked.html15
-rw-r--r--django/contrib/admin/templates/admin/edit_inline/tabular.html15
-rw-r--r--django/contrib/admin/templates/admin/login.html3
-rw-r--r--django/contrib/admin/templates/admin/popup_response.html14
-rw-r--r--django/contrib/admin/templates/admin/prepopulated_fields_js.html32
-rw-r--r--django/contrib/admin/templates/admin/search_form.html3
-rw-r--r--django/contrib/admin/templates/registration/password_change_form.html1
-rw-r--r--django/contrib/admin/templatetags/admin_list.py6
-rw-r--r--django/contrib/admin/templatetags/admin_modify.py19
-rw-r--r--django/contrib/admin/tests.py11
-rw-r--r--django/contrib/admin/widgets.py17
-rw-r--r--django/contrib/auth/forms.py17
-rw-r--r--docs/ref/contrib/admin/javascript.txt33
-rw-r--r--docs/releases/1.10.txt3
-rw-r--r--tests/admin_custom_urls/tests.py1
-rw-r--r--tests/admin_inlines/test_templates.py16
-rw-r--r--tests/admin_inlines/tests.py22
-rw-r--r--tests/admin_views/tests.py48
-rw-r--r--tests/admin_widgets/tests.py10
42 files changed, 455 insertions, 275 deletions
diff --git a/.eslintrc b/.eslintrc
index ec705d2257..cfe7f53010 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -14,7 +14,7 @@
"no-octal-escape": [2],
"no-underscore-dangle": [2],
"no-unused-vars": [2, {"vars": "local", "args": "none"}],
- "no-script-url": [1],
+ "no-script-url": [2],
"no-shadow": [2, {"hoist": "functions"}],
"quotes": [0, "single"],
"linebreak-style": [2, "unix"],
diff --git a/django/contrib/admin/actions.py b/django/contrib/admin/actions.py
index c4cccfd990..80b0ac83d6 100644
--- a/django/contrib/admin/actions.py
+++ b/django/contrib/admin/actions.py
@@ -74,6 +74,7 @@ def delete_selected(modeladmin, request, queryset):
protected=protected,
opts=opts,
action_checkbox_name=helpers.ACTION_CHECKBOX_NAME,
+ media=modeladmin.media,
)
request.current_app = modeladmin.admin_site.name
diff --git a/django/contrib/admin/helpers.py b/django/contrib/admin/helpers.py
index 2c712f6e93..775fb1b5d3 100644
--- a/django/contrib/admin/helpers.py
+++ b/django/contrib/admin/helpers.py
@@ -1,5 +1,6 @@
from __future__ import unicode_literals
+import json
import warnings
from django import forms
@@ -18,7 +19,7 @@ from django.utils.deprecation import RemovedInDjango20Warning
from django.utils.encoding import force_text, smart_text
from django.utils.html import conditional_escape, format_html
from django.utils.safestring import mark_safe
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import ugettext, ugettext_lazy as _
ACTION_CHECKBOX_NAME = '_selected_action'
@@ -276,6 +277,19 @@ class InlineAdminFormSet(object):
'help_text': form_field.help_text,
}
+ def inline_formset_data(self):
+ verbose_name = self.opts.verbose_name
+ return json.dumps({
+ 'name': '#%s' % self.formset.prefix,
+ 'options': {
+ 'prefix': self.formset.prefix,
+ 'addText': ugettext('Add another %(verbose_name)s') % {
+ 'verbose_name': capfirst(verbose_name),
+ },
+ 'deleteText': ugettext('Remove'),
+ }
+ })
+
def _media(self):
media = self.opts.media + self.formset.media
for fs in self:
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index f6ed33b5ee..24d698f540 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -1,6 +1,7 @@
from __future__ import unicode_literals
import copy
+import json
import operator
from collections import OrderedDict
from functools import partial, reduce, update_wrapper
@@ -40,7 +41,7 @@ from django.template.response import SimpleTemplateResponse, TemplateResponse
from django.utils import six
from django.utils.decorators import method_decorator
from django.utils.encoding import force_text, python_2_unicode_compatible
-from django.utils.html import escape, escapejs, format_html
+from django.utils.html import escape, format_html
from django.utils.http import urlencode, urlquote
from django.utils.safestring import mark_safe
from django.utils.text import capfirst, get_text_list
@@ -568,9 +569,9 @@ class ModelAdmin(BaseModelAdmin):
extra = '' if settings.DEBUG else '.min'
js = [
'core.js',
- 'admin/RelatedObjectLookups.js',
'vendor/jquery/jquery%s.js' % extra,
'jquery.init.js',
+ 'admin/RelatedObjectLookups.js',
'actions%s.js' % extra,
'urlify.js',
'prepopulate%s.js' % extra,
@@ -1084,9 +1085,12 @@ class ModelAdmin(BaseModelAdmin):
else:
attr = obj._meta.pk.attname
value = obj.serializable_value(attr)
- return SimpleTemplateResponse('admin/popup_response.html', {
+ popup_response_data = json.dumps({
'value': value,
- 'obj': obj,
+ 'obj': six.text_type(obj),
+ })
+ return SimpleTemplateResponse('admin/popup_response.html', {
+ 'popup_response_data': popup_response_data,
})
elif "_continue" in request.POST:
@@ -1132,11 +1136,14 @@ class ModelAdmin(BaseModelAdmin):
# Retrieve the `object_id` from the resolved pattern arguments.
value = request.resolver_match.args[0]
new_value = obj.serializable_value(attr)
- return SimpleTemplateResponse('admin/popup_response.html', {
+ popup_response_data = json.dumps({
'action': 'change',
- 'value': escape(value),
- 'obj': escapejs(obj),
- 'new_value': escape(new_value),
+ 'value': value,
+ 'obj': six.text_type(obj),
+ 'new_value': new_value,
+ })
+ return SimpleTemplateResponse('admin/popup_response.html', {
+ 'popup_response_data': popup_response_data,
})
opts = self.model._meta
@@ -1300,9 +1307,12 @@ class ModelAdmin(BaseModelAdmin):
opts = self.model._meta
if IS_POPUP_VAR in request.POST:
- return SimpleTemplateResponse('admin/popup_response.html', {
+ popup_response_data = json.dumps({
'action': 'delete',
- 'value': escape(obj_id),
+ 'value': obj_id,
+ })
+ return SimpleTemplateResponse('admin/popup_response.html', {
+ 'popup_response_data': popup_response_data,
})
self.message_user(request,
@@ -1332,6 +1342,7 @@ class ModelAdmin(BaseModelAdmin):
context.update(
to_field_var=TO_FIELD_VAR,
is_popup_var=IS_POPUP_VAR,
+ media=self.media,
)
return TemplateResponse(request,
diff --git a/django/contrib/admin/static/admin/js/SelectFilter2.js b/django/contrib/admin/static/admin/js/SelectFilter2.js
index acf3996c38..e73f8a50ff 100644
--- a/django/contrib/admin/static/admin/js/SelectFilter2.js
+++ b/django/contrib/admin/static/admin/js/SelectFilter2.js
@@ -75,15 +75,15 @@ Requires core.js, SelectBox.js and addevent.js.
filter_input.id = field_id + '_input';
selector_available.appendChild(from_box);
- var choose_all = quickElement('a', selector_available, gettext('Choose all'), 'title', interpolate(gettext('Click to choose all %s at once.'), [field_name]), 'href', 'javascript:void(0);', 'id', field_id + '_add_all_link');
+ var choose_all = quickElement('a', selector_available, gettext('Choose all'), 'title', interpolate(gettext('Click to choose all %s at once.'), [field_name]), 'href', '#', 'id', field_id + '_add_all_link');
choose_all.className = 'selector-chooseall';
// <ul class="selector-chooser">
var selector_chooser = quickElement('ul', selector_div);
selector_chooser.className = 'selector-chooser';
- var add_link = quickElement('a', quickElement('li', selector_chooser), gettext('Choose'), 'title', gettext('Choose'), 'href', 'javascript:void(0);', 'id', field_id + '_add_link');
+ var add_link = quickElement('a', quickElement('li', selector_chooser), gettext('Choose'), 'title', gettext('Choose'), 'href', '#', 'id', field_id + '_add_link');
add_link.className = 'selector-add';
- var remove_link = quickElement('a', quickElement('li', selector_chooser), gettext('Remove'), 'title', gettext('Remove'), 'href', 'javascript:void(0);', 'id', field_id + '_remove_link');
+ var remove_link = quickElement('a', quickElement('li', selector_chooser), gettext('Remove'), 'title', gettext('Remove'), 'href', '#', 'id', field_id + '_remove_link');
remove_link.className = 'selector-remove';
// <div class="selector-chosen">
@@ -105,7 +105,7 @@ Requires core.js, SelectBox.js and addevent.js.
var to_box = quickElement('select', selector_chosen, '', 'id', field_id + '_to', 'multiple', 'multiple', 'size', from_box.size, 'name', from_box.getAttribute('name'));
to_box.className = 'filtered';
- var clear_all = quickElement('a', selector_chosen, gettext('Remove all'), 'title', interpolate(gettext('Click to remove all chosen %s at once.'), [field_name]), 'href', 'javascript:void(0);', 'id', field_id + '_remove_all_link');
+ var clear_all = quickElement('a', selector_chosen, gettext('Remove all'), 'title', interpolate(gettext('Click to remove all chosen %s at once.'), [field_name]), 'href', '#', 'id', field_id + '_remove_all_link');
clear_all.className = 'selector-clearall';
from_box.setAttribute('name', from_box.getAttribute('name') + '_old');
@@ -195,4 +195,12 @@ Requires core.js, SelectBox.js and addevent.js.
}
};
+ addEvent(window, 'load', function(e) {
+ $('select.selectfilter, select.selectfilterstacked').each(function() {
+ var $el = $(this),
+ data = $el.data();
+ SelectFilter.init($el.attr('id'), data.fieldName, parseInt(data.isStacked, 10));
+ });
+ });
+
})(django.jQuery);
diff --git a/django/contrib/admin/static/admin/js/actions.js b/django/contrib/admin/static/admin/js/actions.js
index 95e8492b98..7041701f27 100644
--- a/django/contrib/admin/static/admin/js/actions.js
+++ b/django/contrib/admin/static/admin/js/actions.js
@@ -1,4 +1,4 @@
-/*global _actions_icnt, gettext, interpolate, ngettext*/
+/*global gettext, interpolate, ngettext*/
(function($) {
'use strict';
var lastChecked;
@@ -41,12 +41,13 @@
},
updateCounter = function() {
var sel = $(actionCheckboxes).filter(":checked").length;
- // _actions_icnt is defined in the generated HTML
+ // data-actions-icnt is defined in the generated HTML
// and contains the total amount of objects in the queryset
+ var actions_icnt = $('.action-counter').data('actionsIcnt');
$(options.counterContainer).html(interpolate(
ngettext('%(sel)s of %(cnt)s selected', '%(sel)s of %(cnt)s selected', sel), {
sel: sel,
- cnt: _actions_icnt
+ cnt: actions_icnt
}, true));
$(options.allToggle).prop("checked", function() {
var value;
@@ -143,4 +144,10 @@
allToggle: "#action-toggle",
selectedClass: "selected"
};
+ $(document).ready(function() {
+ var $actionsEls = $('tr input.action-select');
+ if ($actionsEls.length > 0) {
+ $actionsEls.actions();
+ }
+ });
})(django.jQuery);
diff --git a/django/contrib/admin/static/admin/js/actions.min.js b/django/contrib/admin/static/admin/js/actions.min.js
index d0e87bcfa9..c83b06a5f8 100644
--- a/django/contrib/admin/static/admin/js/actions.min.js
+++ b/django/contrib/admin/static/admin/js/actions.min.js
@@ -1,6 +1,6 @@
-(function(a){var f;a.fn.actions=function(q){var b=a.extend({},a.fn.actions.defaults,q),g=a(this),e=!1,k=function(){a(b.acrossClears).hide();a(b.acrossQuestions).show();a(b.allContainer).hide()},l=function(){a(b.acrossClears).show();a(b.acrossQuestions).hide();a(b.actionContainer).toggleClass(b.selectedClass);a(b.allContainer).show();a(b.counterContainer).hide()},m=function(){a(b.acrossClears).hide();a(b.acrossQuestions).hide();a(b.allContainer).hide();a(b.counterContainer).show()},n=function(){m();
-a(b.acrossInput).val(0);a(b.actionContainer).removeClass(b.selectedClass)},p=function(c){c?k():m();a(g).prop("checked",c).parent().parent().toggleClass(b.selectedClass,c)},h=function(){var c=a(g).filter(":checked").length;a(b.counterContainer).html(interpolate(ngettext("%(sel)s of %(cnt)s selected","%(sel)s of %(cnt)s selected",c),{sel:c,cnt:_actions_icnt},!0));a(b.allToggle).prop("checked",function(){var a;c===g.length?(a=!0,k()):(a=!1,n());return a})};a(b.counterContainer).show();a(this).filter(":checked").each(function(c){a(this).parent().parent().toggleClass(b.selectedClass);
-h();1===a(b.acrossInput).val()&&l()});a(b.allToggle).show().click(function(){p(a(this).prop("checked"));h()});a("a",b.acrossQuestions).click(function(c){c.preventDefault();a(b.acrossInput).val(1);l()});a("a",b.acrossClears).click(function(c){c.preventDefault();a(b.allToggle).prop("checked",!1);n();p(0);h()});f=null;a(g).click(function(c){c||(c=window.event);var d=c.target?c.target:c.srcElement;if(f&&a.data(f)!==a.data(d)&&!0===c.shiftKey){var e=!1;a(f).prop("checked",d.checked).parent().parent().toggleClass(b.selectedClass,
-d.checked);a(g).each(function(){if(a.data(this)===a.data(f)||a.data(this)===a.data(d))e=e?!1:!0;e&&a(this).prop("checked",d.checked).parent().parent().toggleClass(b.selectedClass,d.checked)})}a(d).parent().parent().toggleClass(b.selectedClass,d.checked);f=d;h()});a("form#changelist-form table#result_list tr").find("td:gt(0) :input").change(function(){e=!0});a('form#changelist-form button[name="index"]').click(function(a){if(e)return confirm(gettext("You have unsaved changes on individual editable fields. If you run an action, your unsaved changes will be lost."))});
-a('form#changelist-form input[name="_save"]').click(function(c){var d=!1;a("select option:selected",b.actionContainer).each(function(){a(this).val()&&(d=!0)});if(d)return e?confirm(gettext("You have selected an action, but you haven't saved your changes to individual fields yet. Please click OK to save. You'll need to re-run the action.")):confirm(gettext("You have selected an action, and you haven't made any changes on individual fields. You're probably looking for the Go button rather than the Save button."))})};
-a.fn.actions.defaults={actionContainer:"div.actions",counterContainer:"span.action-counter",allContainer:"div.actions span.all",acrossInput:"div.actions input.select-across",acrossQuestions:"div.actions span.question",acrossClears:"div.actions span.clear",allToggle:"#action-toggle",selectedClass:"selected"}})(django.jQuery);
+(function(a){var f;a.fn.actions=function(e){var b=a.extend({},a.fn.actions.defaults,e),g=a(this),k=!1,l=function(){a(b.acrossClears).hide();a(b.acrossQuestions).show();a(b.allContainer).hide()},m=function(){a(b.acrossClears).show();a(b.acrossQuestions).hide();a(b.actionContainer).toggleClass(b.selectedClass);a(b.allContainer).show();a(b.counterContainer).hide()},n=function(){a(b.acrossClears).hide();a(b.acrossQuestions).hide();a(b.allContainer).hide();a(b.counterContainer).show()},p=function(){n();
+a(b.acrossInput).val(0);a(b.actionContainer).removeClass(b.selectedClass)},q=function(c){c?l():n();a(g).prop("checked",c).parent().parent().toggleClass(b.selectedClass,c)},h=function(){var c=a(g).filter(":checked").length,d=a(".action-counter").data("actionsIcnt");a(b.counterContainer).html(interpolate(ngettext("%(sel)s of %(cnt)s selected","%(sel)s of %(cnt)s selected",c),{sel:c,cnt:d},!0));a(b.allToggle).prop("checked",function(){var a;c===g.length?(a=!0,l()):(a=!1,p());return a})};a(b.counterContainer).show();
+a(this).filter(":checked").each(function(c){a(this).parent().parent().toggleClass(b.selectedClass);h();1===a(b.acrossInput).val()&&m()});a(b.allToggle).show().click(function(){q(a(this).prop("checked"));h()});a("a",b.acrossQuestions).click(function(c){c.preventDefault();a(b.acrossInput).val(1);m()});a("a",b.acrossClears).click(function(c){c.preventDefault();a(b.allToggle).prop("checked",!1);p();q(0);h()});f=null;a(g).click(function(c){c||(c=window.event);var d=c.target?c.target:c.srcElement;if(f&&
+a.data(f)!==a.data(d)&&!0===c.shiftKey){var e=!1;a(f).prop("checked",d.checked).parent().parent().toggleClass(b.selectedClass,d.checked);a(g).each(function(){if(a.data(this)===a.data(f)||a.data(this)===a.data(d))e=e?!1:!0;e&&a(this).prop("checked",d.checked).parent().parent().toggleClass(b.selectedClass,d.checked)})}a(d).parent().parent().toggleClass(b.selectedClass,d.checked);f=d;h()});a("form#changelist-form table#result_list tr").find("td:gt(0) :input").change(function(){k=!0});a('form#changelist-form button[name="index"]').click(function(a){if(k)return confirm(gettext("You have unsaved changes on individual editable fields. If you run an action, your unsaved changes will be lost."))});
+a('form#changelist-form input[name="_save"]').click(function(c){var d=!1;a("select option:selected",b.actionContainer).each(function(){a(this).val()&&(d=!0)});if(d)return k?confirm(gettext("You have selected an action, but you haven't saved your changes to individual fields yet. Please click OK to save. You'll need to re-run the action.")):confirm(gettext("You have selected an action, and you haven't made any changes on individual fields. You're probably looking for the Go button rather than the Save button."))})};
+a.fn.actions.defaults={actionContainer:"div.actions",counterContainer:"span.action-counter",allContainer:"div.actions span.all",acrossInput:"div.actions input.select-across",acrossQuestions:"div.actions span.question",acrossClears:"div.actions span.clear",allToggle:"#action-toggle",selectedClass:"selected"};a(document).ready(function(){var e=a("tr input.action-select");0<e.length&&e.actions()})})(django.jQuery);
diff --git a/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js b/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js
index d0acc23bb6..fd7243ed95 100644
--- a/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js
+++ b/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js
@@ -105,11 +105,22 @@
shortcuts_span.className = DateTimeShortcuts.shortCutsClass;
inp.parentNode.insertBefore(shortcuts_span, inp.nextSibling);
var now_link = document.createElement('a');
- now_link.setAttribute('href', "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", -1);");
+ now_link.setAttribute('href', "#");
now_link.appendChild(document.createTextNode(gettext('Now')));
+ addEvent(now_link, 'click', function(e) {
+ e.preventDefault();
+ DateTimeShortcuts.handleClockQuicklink(num, -1);
+ });
var clock_link = document.createElement('a');
- clock_link.setAttribute('href', 'javascript:DateTimeShortcuts.openClock(' + num + ');');
+ clock_link.setAttribute('href', '#');
clock_link.id = DateTimeShortcuts.clockLinkName + num;
+ addEvent(clock_link, 'click', function(e) {
+ e.preventDefault();
+ // avoid triggering the document click handler to dismiss the clock
+ e.stopPropagation();
+ DateTimeShortcuts.openClock(num);
+ });
+
quickElement(
'span', clock_link, '',
'class', 'clock-icon',
@@ -146,15 +157,40 @@
quickElement('h2', clock_box, gettext('Choose a time'));
var time_list = quickElement('ul', clock_box);
time_list.className = 'timelist';
- quickElement("a", quickElement("li", time_list), gettext("Now"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", -1);");
- quickElement("a", quickElement("li", time_list), gettext("Midnight"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", 0);");
- quickElement("a", quickElement("li", time_list), gettext("6 a.m."), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", 6);");
- quickElement("a", quickElement("li", time_list), gettext("Noon"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", 12);");
- quickElement("a", quickElement("li", time_list), gettext("6 p.m."), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", 18);");
+ var time_link = quickElement("a", quickElement("li", time_list), gettext("Now"), "href", "#");
+ addEvent(time_link, 'click', function(e) {
+ e.preventDefault();
+ DateTimeShortcuts.handleClockQuicklink(num, -1);
+ });
+ time_link = quickElement("a", quickElement("li", time_list), gettext("Midnight"), "href", "#");
+ addEvent(time_link, 'click', function(e) {
+ e.preventDefault();
+ DateTimeShortcuts.handleClockQuicklink(num, 0);
+ });
+ time_link = quickElement("a", quickElement("li", time_list), gettext("6 a.m."), "href", "#");
+ addEvent(time_link, 'click', function(e) {
+ e.preventDefault();
+ DateTimeShortcuts.handleClockQuicklink(num, 6);
+ });
+ time_link = quickElement("a", quickElement("li", time_list), gettext("Noon"), "href", "#");
+ addEvent(time_link, 'click', function(e) {
+ e.preventDefault();
+ DateTimeShortcuts.handleClockQuicklink(num, 12);
+ });
+ time_link = quickElement("a", quickElement("li", time_list), gettext("6 p.m."), "href", "#");
+ addEvent(time_link, 'click', function(e) {
+ e.preventDefault();
+ DateTimeShortcuts.handleClockQuicklink(num, 18);
+ });
var cancel_p = quickElement('p', clock_box);
cancel_p.className = 'calendar-cancel';
- quickElement('a', cancel_p, gettext('Cancel'), 'href', 'javascript:DateTimeShortcuts.dismissClock(' + num + ');');
+ var cancel_link = quickElement('a', cancel_p, gettext('Cancel'), 'href', '#');
+ addEvent(cancel_link, 'click', function(e) {
+ e.preventDefault();
+ DateTimeShortcuts.dismissClock(num);
+ });
+
django.jQuery(document).bind('keyup', function(event) {
if (event.which === 27) {
// ESC key closes popup
@@ -213,11 +249,21 @@
shortcuts_span.className = DateTimeShortcuts.shortCutsClass;
inp.parentNode.insertBefore(shortcuts_span, inp.nextSibling);
var today_link = document.createElement('a');
- today_link.setAttribute('href', 'javascript:DateTimeShortcuts.handleCalendarQuickLink(' + num + ', 0);');
+ today_link.setAttribute('href', '#');
today_link.appendChild(document.createTextNode(gettext('Today')));
+ addEvent(today_link, 'click', function(e) {
+ e.preventDefault();
+ DateTimeShortcuts.handleCalendarQuickLink(num, 0);
+ });
var cal_link = document.createElement('a');
- cal_link.setAttribute('href', 'javascript:DateTimeShortcuts.openCalendar(' + num + ');');
+ cal_link.setAttribute('href', '#');
cal_link.id = DateTimeShortcuts.calendarLinkName + num;
+ addEvent(cal_link, 'click', function(e) {
+ e.preventDefault();
+ // avoid triggering the document click handler to dismiss the calendar
+ e.stopPropagation();
+ DateTimeShortcuts.openCalendar(num);
+ });
quickElement(
'span', cal_link, '',
'class', 'date-icon',
@@ -255,10 +301,19 @@
// next-prev links
var cal_nav = quickElement('div', cal_box);
- var cal_nav_prev = quickElement('a', cal_nav, '<', 'href', 'javascript:DateTimeShortcuts.drawPrev(' + num + ');');
+ var cal_nav_prev = quickElement('a', cal_nav, '<', 'href', '#');
cal_nav_prev.className = 'calendarnav-previous';
- var cal_nav_next = quickElement('a', cal_nav, '>', 'href', 'javascript:DateTimeShortcuts.drawNext(' + num + ');');
+ addEvent(cal_nav_prev, 'click', function(e) {
+ e.preventDefault();
+ DateTimeShortcuts.drawPrev(num);
+ });
+
+ var cal_nav_next = quickElement('a', cal_nav, '>', 'href', '#');
cal_nav_next.className = 'calendarnav-next';
+ addEvent(cal_nav_next, 'click', function(e) {
+ e.preventDefault();
+ DateTimeShortcuts.drawNext(num);
+ });
// main box
var cal_main = quickElement('div', cal_box, '', 'id', DateTimeShortcuts.calendarDivName2 + num);
@@ -269,16 +324,32 @@
// calendar shortcuts
var shortcuts = quickElement('div', cal_box);
shortcuts.className = 'calendar-shortcuts';
- quickElement('a', shortcuts, gettext('Yesterday'), 'href', 'javascript:DateTimeShortcuts.handleCalendarQuickLink(' + num + ', -1);');
+ var day_link = quickElement('a', shortcuts, gettext('Yesterday'), 'href', '#');
+ addEvent(day_link, 'click', function(e) {
+ e.preventDefault();
+ DateTimeShortcuts.handleCalendarQuickLink(num, -1);
+ });
shortcuts.appendChild(document.createTextNode('\u00A0|\u00A0'));
- quickElement('a', shortcuts, gettext('Today'), 'href', 'javascript:DateTimeShortcuts.handleCalendarQuickLink(' + num + ', 0);');
+ day_link = quickElement('a', shortcuts, gettext('Today'), 'href', '#');
+ addEvent(day_link, 'click', function(e) {
+ e.preventDefault();
+ DateTimeShortcuts.handleCalendarQuickLink(num, 0);
+ });
shortcuts.appendChild(document.createTextNode('\u00A0|\u00A0'));
- quickElement('a', shortcuts, gettext('Tomorrow'), 'href', 'javascript:DateTimeShortcuts.handleCalendarQuickLink(' + num + ', +1);');
+ day_link = quickElement('a', shortcuts, gettext('Tomorrow'), 'href', '#');
+ addEvent(day_link, 'click', function(e) {
+ e.preventDefault();
+ DateTimeShortcuts.handleCalendarQuickLink(num, +1);
+ });
// cancel bar
var cancel_p = quickElement('p', cal_box);
cancel_p.className = 'calendar-cancel';
- quickElement('a', cancel_p, gettext('Cancel'), 'href', 'javascript:DateTimeShortcuts.dismissCalendar(' + num + ');');
+ var cancel_link = quickElement('a', cancel_p, gettext('Cancel'), 'href', '#');
+ addEvent(cancel_link, 'click', function(e) {
+ e.preventDefault();
+ DateTimeShortcuts.dismissCalendar(num);
+ });
django.jQuery(document).bind('keyup', function(event) {
if (event.which === 27) {
// ESC key closes popup
@@ -340,15 +411,11 @@
format = format.replace('\n', '\\n');
format = format.replace('\t', '\\t');
format = format.replace("'", "\\'");
- return ["function(y, m, d) { DateTimeShortcuts.calendarInputs[",
- num,
- "].value = new Date(y, m-1, d).strftime('",
- format,
- "');DateTimeShortcuts.calendarInputs[",
- num,
- "].focus();document.getElementById(DateTimeShortcuts.calendarDivName1+",
- num,
- ").style.display='none';}"].join('');
+ return function(y, m, d) {
+ DateTimeShortcuts.calendarInputs[num].value = new Date(y, m - 1, d).strftime(format);
+ DateTimeShortcuts.calendarInputs[num].focus();
+ document.getElementById(DateTimeShortcuts.calendarDivName1 + num).style.display = 'none';
+ };
},
handleCalendarQuickLink: function(num, offset) {
var d = DateTimeShortcuts.now();
diff --git a/django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js b/django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js
index fa782fe2b8..0d468d4b6d 100644
--- a/django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js
+++ b/django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js
@@ -2,7 +2,7 @@
// Handles related-objects functionality: lookup link for raw_id_fields
// and Add Another links.
-(function() {
+(function($) {
'use strict';
function html_unescape(text) {
@@ -157,4 +157,11 @@
window.showAddAnotherPopup = showRelatedObjectPopup;
window.dismissAddAnotherPopup = dismissAddRelatedObjectPopup;
-})();
+ $(document).ready(function() {
+ $("a[data-popup-opener]").click(function(event) {
+ event.preventDefault();
+ opener.dismissRelatedLookupPopup(window, $(this).data("popup-opener"));
+ });
+ });
+
+})(django.jQuery);
diff --git a/django/contrib/admin/static/admin/js/calendar.js b/django/contrib/admin/static/admin/js/calendar.js
index 75e7bce80c..8432ebd1bf 100644
--- a/django/contrib/admin/static/admin/js/calendar.js
+++ b/django/contrib/admin/static/admin/js/calendar.js
@@ -1,4 +1,4 @@
-/*global gettext, pgettext, get_format, quickElement, removeChildren*/
+/*global gettext, pgettext, get_format, quickElement, removeChildren, addEvent*/
/*
calendar.js - Calendar functions by Adrian Holovaty
depends on core.js for utility functions like removeChildren or quickElement
@@ -100,6 +100,14 @@ depends on core.js for utility functions like removeChildren or quickElement
nonDayCell.className = "nonday";
}
+ function calendarMonth(y, m) {
+ function onClick(e) {
+ e.preventDefault();
+ callback(y, m, django.jQuery(this).text());
+ }
+ return onClick;
+ }
+
// Draw days of month
var currentDay = 1;
for (i = startingPos; currentDay <= days; i++) {
@@ -121,8 +129,8 @@ depends on core.js for utility functions like removeChildren or quickElement
}
var cell = quickElement('td', tableRow, '', 'class', todayClass);
-
- quickElement('a', cell, currentDay, 'href', 'javascript:void(' + callback + '(' + year + ',' + month + ',' + currentDay + '));');
+ var link = quickElement('a', cell, currentDay, 'href', '#');
+ addEvent(link, 'click', calendarMonth(year, month));
currentDay++;
}
diff --git a/django/contrib/admin/static/admin/js/cancel.js b/django/contrib/admin/static/admin/js/cancel.js
new file mode 100644
index 0000000000..b64138789f
--- /dev/null
+++ b/django/contrib/admin/static/admin/js/cancel.js
@@ -0,0 +1,9 @@
+(function($) {
+ 'use strict';
+ $(function() {
+ $('.cancel-link').click(function(e) {
+ e.preventDefault();
+ window.history.back();
+ });
+ });
+})(django.jQuery);
diff --git a/django/contrib/admin/static/admin/js/change_form.js b/django/contrib/admin/static/admin/js/change_form.js
new file mode 100644
index 0000000000..883376379f
--- /dev/null
+++ b/django/contrib/admin/static/admin/js/change_form.js
@@ -0,0 +1,46 @@
+/*global showAddAnotherPopup, showRelatedObjectLookupPopup showRelatedObjectPopup updateRelatedObjectLinks*/
+
+(function($) {
+ 'use strict';
+ $(document).ready(function() {
+ var modelName = $('#django-admin-form-add-constants').data('modelName');
+ $('.add-another').click(function(e) {
+ e.preventDefault();
+ var event = $.Event('django:add-another-related');
+ $(this).trigger(event);
+ if (!event.isDefaultPrevented()) {
+ showAddAnotherPopup(this);
+ }
+ });
+ $('.related-lookup').click(function(e) {
+ e.preventDefault();
+ var event = $.Event('django:lookup-related');
+ $(this).trigger(event);
+ if (!event.isDefaultPrevented()) {
+ showRelatedObjectLookupPopup(this);
+ }
+ });
+ $('body').on('click', '.related-widget-wrapper-link', function(e) {
+ e.preventDefault();
+ if (this.href) {
+ var event = $.Event('django:show-related', {href: this.href});
+ $(this).trigger(event);
+ if (!event.isDefaultPrevented()) {
+ showRelatedObjectPopup(this);
+ }
+ }
+ });
+ $('body').on('change', '.related-widget-wrapper select', function(e) {
+ var event = $.Event('django:update-related');
+ $(this).trigger(event);
+ if (!event.isDefaultPrevented()) {
+ updateRelatedObjectLinks(this);
+ }
+ });
+ $('.related-widget-wrapper select').trigger('change');
+
+ if (modelName) {
+ $('form#' + modelName + '_form :input:visible:enabled:first').focus();
+ }
+ });
+})(django.jQuery);
diff --git a/django/contrib/admin/static/admin/js/inlines.js b/django/contrib/admin/static/admin/js/inlines.js
index c725e30cbb..a284d76c3c 100644
--- a/django/contrib/admin/static/admin/js/inlines.js
+++ b/django/contrib/admin/static/admin/js/inlines.js
@@ -49,11 +49,11 @@
// If forms are laid out as table rows, insert the
// "add" button in a new table row:
var numCols = this.eq(-1).children().length;
- $parent.append('<tr class="' + options.addCssClass + '"><td colspan="' + numCols + '"><a href="javascript:void(0)">' + options.addText + "</a></tr>");
+ $parent.append('<tr class="' + options.addCssClass + '"><td colspan="' + numCols + '"><a href="#">' + options.addText + "</a></tr>");
addButton = $parent.find("tr:last a");
} else {
// Otherwise, insert it immediately after the last form:
- $this.filter(":last").after('<div class="' + options.addCssClass + '"><a href="javascript:void(0)">' + options.addText + "</a></div>");
+ $this.filter(":last").after('<div class="' + options.addCssClass + '"><a href="#">' + options.addText + "</a></div>");
addButton = $this.filter(":last").next().find("a");
}
addButton.click(function(e) {
@@ -66,15 +66,15 @@
if (row.is("tr")) {
// If the forms are laid out in table rows, insert
// the remove button into the last table cell:
- row.children(":last").append('<div><a class="' + options.deleteCssClass + '" href="javascript:void(0)">' + options.deleteText + "</a></div>");
+ row.children(":last").append('<div><a class="' + options.deleteCssClass + '" href="#">' + options.deleteText + "</a></div>");
} else if (row.is("ul") || row.is("ol")) {
// If they're laid out as an ordered/unordered list,
// insert an <li> after the last list item:
- row.append('<li><a class="' + options.deleteCssClass + '" href="javascript:void(0)">' + options.deleteText + "</a></li>");
+ row.append('<li><a class="' + options.deleteCssClass + '" href="#">' + options.deleteText + "</a></li>");
} else {
// Otherwise, just insert the remove button as the
// last child element of the form's container:
- row.children(":first").append('<span><a class="' + options.deleteCssClass + '" href="javascript:void(0)">' + options.deleteText + "</a></span>");
+ row.children(":first").append('<span><a class="' + options.deleteCssClass + '" href="#">' + options.deleteText + "</a></span>");
}
row.find("*").each(function() {
updateElementIndex(this, options.prefix, totalForms.val());
@@ -272,4 +272,19 @@
return $rows;
};
+
+ $(document).ready(function() {
+ $(".js-inline-admin-formset").each(function() {
+ var data = $(this).data(),
+ inlineOptions = data.inlineFormset;
+ switch(data.inlineType) {
+ case "stacked":
+ $(inlineOptions.name + "-group .inline-related").stackedFormset(inlineOptions.options);
+ break;
+ case "tabular":
+ $(inlineOptions.name + "-group .tabular.inline-related tbody tr").tabularFormset(inlineOptions.options);
+ break;
+ }
+ });
+ });
})(django.jQuery);
diff --git a/django/contrib/admin/static/admin/js/inlines.min.js b/django/contrib/admin/static/admin/js/inlines.min.js
index f83cbec6fc..7e5228d7ad 100644
--- a/django/contrib/admin/static/admin/js/inlines.min.js
+++ b/django/contrib/admin/static/admin/js/inlines.min.js
@@ -1,9 +1,10 @@
-(function(b){b.fn.formset=function(d){var a=b.extend({},b.fn.formset.defaults,d),e=b(this);d=e.parent();var k=function(a,f,l){var c=new RegExp("("+f+"-(\\d+|__prefix__))");f=f+"-"+l;b(a).prop("for")&&b(a).prop("for",b(a).prop("for").replace(c,f));a.id&&(a.id=a.id.replace(c,f));a.name&&(a.name=a.name.replace(c,f))},h=b("#id_"+a.prefix+"-TOTAL_FORMS").prop("autocomplete","off"),l=parseInt(h.val(),10),f=b("#id_"+a.prefix+"-MAX_NUM_FORMS").prop("autocomplete","off"),c=""===f.val()||0<f.val()-h.val();
-e.each(function(f){b(this).not("."+a.emptyCssClass).addClass(a.formCssClass)});if(e.length&&c){var m;"TR"===e.prop("tagName")?(e=this.eq(-1).children().length,d.append('<tr class="'+a.addCssClass+'"><td colspan="'+e+'"><a href="javascript:void(0)">'+a.addText+"</a></tr>"),m=d.find("tr:last a")):(e.filter(":last").after('<div class="'+a.addCssClass+'"><a href="javascript:void(0)">'+a.addText+"</a></div>"),m=e.filter(":last").next().find("a"));m.click(function(c){c.preventDefault();c=b("#"+a.prefix+
-"-empty");var g=c.clone(!0);g.removeClass(a.emptyCssClass).addClass(a.formCssClass).attr("id",a.prefix+"-"+l);g.is("tr")?g.children(":last").append('<div><a class="'+a.deleteCssClass+'" href="javascript:void(0)">'+a.deleteText+"</a></div>"):g.is("ul")||g.is("ol")?g.append('<li><a class="'+a.deleteCssClass+'" href="javascript:void(0)">'+a.deleteText+"</a></li>"):g.children(":first").append('<span><a class="'+a.deleteCssClass+'" href="javascript:void(0)">'+a.deleteText+"</a></span>");g.find("*").each(function(){k(this,
-a.prefix,h.val())});g.insertBefore(b(c));b(h).val(parseInt(h.val(),10)+1);l+=1;""!==f.val()&&0>=f.val()-h.val()&&m.parent().hide();g.find("a."+a.deleteCssClass).click(function(c){c.preventDefault();g.remove();--l;a.removed&&a.removed(g);b(document).trigger("formset:removed",[g,a.prefix]);c=b("."+a.formCssClass);b("#id_"+a.prefix+"-TOTAL_FORMS").val(c.length);(""===f.val()||0<f.val()-c.length)&&m.parent().show();var d,e,h=function(){k(this,a.prefix,d)};d=0;for(e=c.length;d<e;d++)k(b(c).get(d),a.prefix,
-d),b(c.get(d)).find("*").each(h)});a.added&&a.added(g);b(document).trigger("formset:added",[g,a.prefix])})}return this};b.fn.formset.defaults={prefix:"form",addText:"add another",deleteText:"remove",addCssClass:"add-row",deleteCssClass:"delete-row",emptyCssClass:"empty-row",formCssClass:"dynamic-form",added:null,removed:null};b.fn.tabularFormset=function(d){var a=b(this),e=function(l){b(a.selector).not(".add-row").removeClass("row1 row2").filter(":even").addClass("row1").end().filter(":odd").addClass("row2")},
-k=function(){"undefined"!==typeof SelectFilter&&(b(".selectfilter").each(function(a,b){var c=b.name.split("-");SelectFilter.init(b.id,c[c.length-1],!1)}),b(".selectfilterstacked").each(function(a,b){var c=b.name.split("-");SelectFilter.init(b.id,c[c.length-1],!0)}))},h=function(a){a.find(".prepopulated_field").each(function(){var f=b(this).find("input, select, textarea"),c=f.data("dependency_list")||[],d=[];b.each(c,function(b,c){d.push("#"+a.find(".field-"+c).find("input, select, textarea").attr("id"))});
-d.length&&f.prepopulate(d,f.attr("maxlength"))})};a.formset({prefix:d.prefix,addText:d.addText,formCssClass:"dynamic-"+d.prefix,deleteCssClass:"inline-deletelink",deleteText:d.deleteText,emptyCssClass:"empty-form",removed:e,added:function(a){h(a);"undefined"!==typeof DateTimeShortcuts&&(b(".datetimeshortcuts").remove(),DateTimeShortcuts.init());k();e(a)}});return a};b.fn.stackedFormset=function(d){var a=b(this),e=function(d){b(a.selector).find(".inline_label").each(function(a){a+=1;b(this).html(b(this).html().replace(/(#\d+)/g,
-"#"+a))})},k=function(){"undefined"!==typeof SelectFilter&&(b(".selectfilter").each(function(a,b){var c=b.name.split("-");SelectFilter.init(b.id,c[c.length-1],!1)}),b(".selectfilterstacked").each(function(a,b){var c=b.name.split("-");SelectFilter.init(b.id,c[c.length-1],!0)}))},h=function(a){a.find(".prepopulated_field").each(function(){var d=b(this).find("input, select, textarea"),c=d.data("dependency_list")||[],e=[];b.each(c,function(b,c){e.push("#"+a.find(".form-row .field-"+c).find("input, select, textarea").attr("id"))});
-e.length&&d.prepopulate(e,d.attr("maxlength"))})};a.formset({prefix:d.prefix,addText:d.addText,formCssClass:"dynamic-"+d.prefix,deleteCssClass:"inline-deletelink",deleteText:d.deleteText,emptyCssClass:"empty-form",removed:e,added:function(a){h(a);"undefined"!==typeof DateTimeShortcuts&&(b(".datetimeshortcuts").remove(),DateTimeShortcuts.init());k();e(a)}});return a}})(django.jQuery);
+(function(c){c.fn.formset=function(b){var a=c.extend({},c.fn.formset.defaults,b),d=c(this);b=d.parent();var k=function(a,g,l){var b=new RegExp("("+g+"-(\\d+|__prefix__))");g=g+"-"+l;c(a).prop("for")&&c(a).prop("for",c(a).prop("for").replace(b,g));a.id&&(a.id=a.id.replace(b,g));a.name&&(a.name=a.name.replace(b,g))},e=c("#id_"+a.prefix+"-TOTAL_FORMS").prop("autocomplete","off"),l=parseInt(e.val(),10),g=c("#id_"+a.prefix+"-MAX_NUM_FORMS").prop("autocomplete","off"),h=""===g.val()||0<g.val()-e.val();
+d.each(function(g){c(this).not("."+a.emptyCssClass).addClass(a.formCssClass)});if(d.length&&h){var m;"TR"===d.prop("tagName")?(d=this.eq(-1).children().length,b.append('<tr class="'+a.addCssClass+'"><td colspan="'+d+'"><a href="#">'+a.addText+"</a></tr>"),m=b.find("tr:last a")):(d.filter(":last").after('<div class="'+a.addCssClass+'"><a href="#">'+a.addText+"</a></div>"),m=d.filter(":last").next().find("a"));m.click(function(b){b.preventDefault();b=c("#"+a.prefix+"-empty");var f=b.clone(!0);f.removeClass(a.emptyCssClass).addClass(a.formCssClass).attr("id",
+a.prefix+"-"+l);f.is("tr")?f.children(":last").append('<div><a class="'+a.deleteCssClass+'" href="#">'+a.deleteText+"</a></div>"):f.is("ul")||f.is("ol")?f.append('<li><a class="'+a.deleteCssClass+'" href="#">'+a.deleteText+"</a></li>"):f.children(":first").append('<span><a class="'+a.deleteCssClass+'" href="#">'+a.deleteText+"</a></span>");f.find("*").each(function(){k(this,a.prefix,e.val())});f.insertBefore(c(b));c(e).val(parseInt(e.val(),10)+1);l+=1;""!==g.val()&&0>=g.val()-e.val()&&m.parent().hide();
+f.find("a."+a.deleteCssClass).click(function(b){b.preventDefault();f.remove();--l;a.removed&&a.removed(f);c(document).trigger("formset:removed",[f,a.prefix]);b=c("."+a.formCssClass);c("#id_"+a.prefix+"-TOTAL_FORMS").val(b.length);(""===g.val()||0<g.val()-b.length)&&m.parent().show();var h,d,e=function(){k(this,a.prefix,h)};h=0;for(d=b.length;h<d;h++)k(c(b).get(h),a.prefix,h),c(b.get(h)).find("*").each(e)});a.added&&a.added(f);c(document).trigger("formset:added",[f,a.prefix])})}return this};c.fn.formset.defaults=
+{prefix:"form",addText:"add another",deleteText:"remove",addCssClass:"add-row",deleteCssClass:"delete-row",emptyCssClass:"empty-row",formCssClass:"dynamic-form",added:null,removed:null};c.fn.tabularFormset=function(b){var a=c(this),d=function(b){c(a.selector).not(".add-row").removeClass("row1 row2").filter(":even").addClass("row1").end().filter(":odd").addClass("row2")},k=function(){"undefined"!==typeof SelectFilter&&(c(".selectfilter").each(function(a,c){var b=c.name.split("-");SelectFilter.init(c.id,
+b[b.length-1],!1)}),c(".selectfilterstacked").each(function(a,c){var b=c.name.split("-");SelectFilter.init(c.id,b[b.length-1],!0)}))},e=function(a){a.find(".prepopulated_field").each(function(){var b=c(this).find("input, select, textarea"),h=b.data("dependency_list")||[],d=[];c.each(h,function(c,b){d.push("#"+a.find(".field-"+b).find("input, select, textarea").attr("id"))});d.length&&b.prepopulate(d,b.attr("maxlength"))})};a.formset({prefix:b.prefix,addText:b.addText,formCssClass:"dynamic-"+b.prefix,
+deleteCssClass:"inline-deletelink",deleteText:b.deleteText,emptyCssClass:"empty-form",removed:d,added:function(a){e(a);"undefined"!==typeof DateTimeShortcuts&&(c(".datetimeshortcuts").remove(),DateTimeShortcuts.init());k();d(a)}});return a};c.fn.stackedFormset=function(b){var a=c(this),d=function(b){c(a.selector).find(".inline_label").each(function(a){a+=1;c(this).html(c(this).html().replace(/(#\d+)/g,"#"+a))})},k=function(){"undefined"!==typeof SelectFilter&&(c(".selectfilter").each(function(a,c){var b=
+c.name.split("-");SelectFilter.init(c.id,b[b.length-1],!1)}),c(".selectfilterstacked").each(function(a,c){var b=c.name.split("-");SelectFilter.init(c.id,b[b.length-1],!0)}))},e=function(a){a.find(".prepopulated_field").each(function(){var b=c(this).find("input, select, textarea"),d=b.data("dependency_list")||[],e=[];c.each(d,function(b,c){e.push("#"+a.find(".form-row .field-"+c).find("input, select, textarea").attr("id"))});e.length&&b.prepopulate(e,b.attr("maxlength"))})};a.formset({prefix:b.prefix,
+addText:b.addText,formCssClass:"dynamic-"+b.prefix,deleteCssClass:"inline-deletelink",deleteText:b.deleteText,emptyCssClass:"empty-form",removed:d,added:function(a){e(a);"undefined"!==typeof DateTimeShortcuts&&(c(".datetimeshortcuts").remove(),DateTimeShortcuts.init());k();d(a)}});return a};c(document).ready(function(){c(".js-inline-admin-formset").each(function(){var b=c(this).data(),a=b.inlineFormset;switch(b.inlineType){case "stacked":c(a.name+"-group .inline-related").stackedFormset(a.options);
+break;case "tabular":c(a.name+"-group .tabular.inline-related tbody tr").tabularFormset(a.options)}})})})(django.jQuery);
diff --git a/django/contrib/admin/static/admin/js/popup_response.js b/django/contrib/admin/static/admin/js/popup_response.js
new file mode 100644
index 0000000000..b4a07e7454
--- /dev/null
+++ b/django/contrib/admin/static/admin/js/popup_response.js
@@ -0,0 +1,16 @@
+/*global opener */
+(function() {
+ 'use strict';
+ var initData = JSON.parse(document.getElementById('django-admin-popup-response-constants').dataset.popupResponse);
+ switch(initData.action) {
+ case 'change':
+ opener.dismissChangeRelatedObjectPopup(window, initData.value, initData.obj, initData.new_value);
+ break;
+ case 'delete':
+ opener.dismissDeleteRelatedObjectPopup(window, initData.value);
+ break;
+ default:
+ opener.dismissAddRelatedObjectPopup(window, initData.value, initData.obj);
+ break;
+ }
+})();
diff --git a/django/contrib/admin/static/admin/js/prepopulate_init.js b/django/contrib/admin/static/admin/js/prepopulate_init.js
new file mode 100644
index 0000000000..184df9240c
--- /dev/null
+++ b/django/contrib/admin/static/admin/js/prepopulate_init.js
@@ -0,0 +1,10 @@
+(function($) {
+ 'use strict';
+ var fields = $('#django-admin-prepopulated-fields-constants').data('prepopulatedFields');
+ $.each(fields, function(index, field) {
+ $('.empty-form .form-row .field-' + field.name + ', .empty-form.form-row .field-' + field.name).addClass('prepopulated_field');
+ $(field.id).data('dependency_list', field.dependency_list).prepopulate(
+ field.dependency_ids, field.maxLength, field.allowUnicode
+ );
+ });
+})(django.jQuery);
diff --git a/django/contrib/admin/templates/admin/actions.html b/django/contrib/admin/templates/admin/actions.html
index aaaa2458c4..80ffa066ed 100644
--- a/django/contrib/admin/templates/admin/actions.html
+++ b/django/contrib/admin/templates/admin/actions.html
@@ -3,14 +3,13 @@
{% for field in action_form %}{% if field.label %}<label>{{ field.label }} {% endif %}{{ field }}{% if field.label %}</label>{% endif %}{% endfor %}
<button type="submit" class="button" title="{% trans "Run the selected action" %}" name="index" value="{{ action_index|default:0 }}">{% trans "Go" %}</button>
{% if actions_selection_counter %}
- <script type="text/javascript">var _actions_icnt="{{ cl.result_list|length|default:"0" }}";</script>
- <span class="action-counter">{{ selection_note }}</span>
+ <span class="action-counter" data-actions-icnt="{{ cl.result_list|length }}">{{ selection_note }}</span>
{% if cl.result_count != cl.result_list|length %}
<span class="all">{{ selection_note_all }}</span>
<span class="question">
- <a href="javascript:;" title="{% trans "Click here to select the objects across all pages" %}">{% blocktrans with cl.result_count as total_count %}Select all {{ total_count }} {{ module_name }}{% endblocktrans %}</a>
+ <a href="#" title="{% trans "Click here to select the objects across all pages" %}">{% blocktrans with cl.result_count as total_count %}Select all {{ total_count }} {{ module_name }}{% endblocktrans %}</a>
</span>
- <span class="clear"><a href="javascript:;">{% trans "Clear selection" %}</a></span>
+ <span class="clear"><a href="#">{% trans "Clear selection" %}</a></span>
{% endif %}
{% endif %}
</div>
diff --git a/django/contrib/admin/templates/admin/auth/user/add_form.html b/django/contrib/admin/templates/admin/auth/user/add_form.html
index c8889eb069..5c240d5a6a 100644
--- a/django/contrib/admin/templates/admin/auth/user/add_form.html
+++ b/django/contrib/admin/templates/admin/auth/user/add_form.html
@@ -8,7 +8,3 @@
<p>{% trans "Enter a username and password." %}</p>
{% endif %}
{% endblock %}
-
-{% block after_field_sets %}
-<script type="text/javascript">document.getElementById("id_username").focus();</script>
-{% endblock %}
diff --git a/django/contrib/admin/templates/admin/auth/user/change_password.html b/django/contrib/admin/templates/admin/auth/user/change_password.html
index c788571fb6..124f9279cf 100644
--- a/django/contrib/admin/templates/admin/auth/user/change_password.html
+++ b/django/contrib/admin/templates/admin/auth/user/change_password.html
@@ -54,7 +54,6 @@
<input type="submit" value="{% trans 'Change password' %}" class="default" />
</div>
-<script type="text/javascript">document.getElementById("id_password1").focus();</script>
</div>
</form></div>
{% endblock %}
diff --git a/django/contrib/admin/templates/admin/change_form.html b/django/contrib/admin/templates/admin/change_form.html
index 0e2255f6ec..60c68f1d03 100644
--- a/django/contrib/admin/templates/admin/change_form.html
+++ b/django/contrib/admin/templates/admin/change_form.html
@@ -68,49 +68,12 @@
{% block submit_buttons_bottom %}{% submit_row %}{% endblock %}
{% block admin_change_form_document_ready %}
- <script type="text/javascript">
- (function($) {
- $(document).ready(function() {
- $('.add-another').click(function(e) {
- e.preventDefault();
- var event = $.Event('django:add-another-related');
- $(this).trigger(event);
- if (!event.isDefaultPrevented()) {
- showAddAnotherPopup(this);
- }
- });
- $('.related-lookup').click(function(e) {
- e.preventDefault();
- var event = $.Event('django:lookup-related');
- $(this).trigger(event);
- if (!event.isDefaultPrevented()) {
- showRelatedObjectLookupPopup(this);
- }
- });
- $('body').on('click', '.related-widget-wrapper-link', function(e) {
- e.preventDefault();
- if (this.href) {
- var event = $.Event('django:show-related', {href: this.href});
- $(this).trigger(event);
- if (!event.isDefaultPrevented()) {
- showRelatedObjectPopup(this);
- }
- }
- });
- $('body').on('change', '.related-widget-wrapper select', function(e) {
- var event = $.Event('django:update-related');
- $(this).trigger(event);
- if (!event.isDefaultPrevented()) {
- updateRelatedObjectLinks(this);
- }
- });
- $('.related-widget-wrapper select').trigger('change');
-
+ <script type="text/javascript"
+ id="django-admin-form-add-constants"
+ src="{% static 'admin/js/change_form.js' %}"
{% if adminform and add %}
- $('form#{{ opts.model_name }}_form :input:visible:enabled:first').focus()
- {% endif %}
- });
- })(django.jQuery);
+ data-model-name="{{ opts.model_name }}"
+ {% endif %}>
</script>
{% endblock %}
diff --git a/django/contrib/admin/templates/admin/change_list.html b/django/contrib/admin/templates/admin/change_list.html
index 0bd44d96b4..5d73d74808 100644
--- a/django/contrib/admin/templates/admin/change_list.html
+++ b/django/contrib/admin/templates/admin/change_list.html
@@ -21,15 +21,6 @@
{% block extrahead %}
{{ block.super }}
{{ media.js }}
-{% if action_form %}{% if actions_on_top or actions_on_bottom %}
-<script type="text/javascript">
-(function($) {
- $(document).ready(function($) {
- $("tr input.action-select").actions();
- });
-})(django.jQuery);
-</script>
-{% endif %}{% endif %}
{% endblock %}
{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} change-list{% endblock %}
diff --git a/django/contrib/admin/templates/admin/delete_confirmation.html b/django/contrib/admin/templates/admin/delete_confirmation.html
index bc26bca309..baaf75b2a0 100644
--- a/django/contrib/admin/templates/admin/delete_confirmation.html
+++ b/django/contrib/admin/templates/admin/delete_confirmation.html
@@ -1,5 +1,10 @@
{% extends "admin/base_site.html" %}
-{% load i18n admin_urls %}
+{% load i18n admin_urls admin_static %}
+
+{% block extrahead %}
+ {{ media }}
+ <script type="text/javascript" src="{% static 'admin/js/cancel.js' %}"></script>
+{% endblock %}
{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} delete-confirmation{% endblock %}
@@ -39,7 +44,7 @@
{% if is_popup %}<input type="hidden" name="{{ is_popup_var }}" value="1" />{% endif %}
{% if to_field %}<input type="hidden" name="{{ to_field_var }}" value="{{ to_field }}" />{% endif %}
<input type="submit" value="{% trans "Yes, I'm sure" %}" />
- <a href="#" onclick="window.history.back(); return false;" class="button cancel-link">{% trans "No, take me back" %}</a>
+ <a href="#" class="button cancel-link">{% trans "No, take me back" %}</a>
</div>
</form>
{% endif %}
diff --git a/django/contrib/admin/templates/admin/delete_selected_confirmation.html b/django/contrib/admin/templates/admin/delete_selected_confirmation.html
index 5ebd6432e1..27a888f154 100644
--- a/django/contrib/admin/templates/admin/delete_selected_confirmation.html
+++ b/django/contrib/admin/templates/admin/delete_selected_confirmation.html
@@ -1,5 +1,10 @@
{% extends "admin/base_site.html" %}
-{% load i18n l10n admin_urls %}
+{% load i18n l10n admin_urls admin_static %}
+
+{% block extrahead %}
+ {{ media }}
+ <script type="text/javascript" src="{% static 'admin/js/cancel.js' %}"></script>
+{% endblock %}
{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} delete-confirmation delete-selected-confirmation{% endblock %}
@@ -42,7 +47,7 @@
<input type="hidden" name="action" value="delete_selected" />
<input type="hidden" name="post" value="yes" />
<input type="submit" value="{% trans "Yes, I'm sure" %}" />
- <a href="#" onclick="window.history.back(); return false;" class="button cancel-link">{% trans "No, take me back" %}</a>
+ <a href="#" class="button cancel-link">{% trans "No, take me back" %}</a>
</div>
</form>
{% endif %}
diff --git a/django/contrib/admin/templates/admin/edit_inline/stacked.html b/django/contrib/admin/templates/admin/edit_inline/stacked.html
index 2a837a1686..799ea3b4de 100644
--- a/django/contrib/admin/templates/admin/edit_inline/stacked.html
+++ b/django/contrib/admin/templates/admin/edit_inline/stacked.html
@@ -1,5 +1,8 @@
{% load i18n admin_urls admin_static %}
-<div class="inline-group" id="{{ inline_admin_formset.formset.prefix }}-group">
+<div class="js-inline-admin-formset inline-group"
+ id="{{ inline_admin_formset.formset.prefix }}-group"
+ data-inline-type="stacked"
+ data-inline-formset="{{ inline_admin_formset.inline_formset_data }}">
<h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2>
{{ inline_admin_formset.formset.management_form }}
{{ inline_admin_formset.formset.non_form_errors }}
@@ -18,13 +21,3 @@
{{ inline_admin_form.fk_field.field }}
</div>{% endfor %}
</div>
-
-<script type="text/javascript">
-(function($) {
- $("#{{ inline_admin_formset.formset.prefix|escapejs }}-group .inline-related").stackedFormset({
- prefix: "{{ inline_admin_formset.formset.prefix|escapejs }}",
- deleteText: "{% filter escapejs %}{% trans "Remove" %}{% endfilter %}",
- addText: "{% filter escapejs %}{% blocktrans with verbose_name=inline_admin_formset.opts.verbose_name|capfirst %}Add another {{ verbose_name }}{% endblocktrans %}{% endfilter %}"
- });
-})(django.jQuery);
-</script>
diff --git a/django/contrib/admin/templates/admin/edit_inline/tabular.html b/django/contrib/admin/templates/admin/edit_inline/tabular.html
index a5a4eb3deb..8bb3f60c73 100644
--- a/django/contrib/admin/templates/admin/edit_inline/tabular.html
+++ b/django/contrib/admin/templates/admin/edit_inline/tabular.html
@@ -1,5 +1,7 @@
{% load i18n admin_urls admin_static admin_modify %}
-<div class="inline-group" id="{{ inline_admin_formset.formset.prefix }}-group">
+<div class="js-inline-admin-formset inline-group" id="{{ inline_admin_formset.formset.prefix }}-group"
+ data-inline-type="tabular"
+ data-inline-formset="{{ inline_admin_formset.inline_formset_data }}">
<div class="tabular inline-related {% if forloop.last %}last-related{% endif %}">
{{ inline_admin_formset.formset.management_form }}
<fieldset class="module">
@@ -71,14 +73,3 @@
</fieldset>
</div>
</div>
-
-<script type="text/javascript">
-
-(function($) {
- $("#{{ inline_admin_formset.formset.prefix|escapejs }}-group .tabular.inline-related tbody tr").tabularFormset({
- prefix: "{{ inline_admin_formset.formset.prefix|escapejs }}",
- addText: "{% filter escapejs %}{% blocktrans with inline_admin_formset.opts.verbose_name|capfirst as verbose_name %}Add another {{ verbose_name }}{% endblocktrans %}{% endfilter %}",
- deleteText: "{% filter escapejs %}{% trans 'Remove' %}{% endfilter %}"
- });
-})(django.jQuery);
-</script>
diff --git a/django/contrib/admin/templates/admin/login.html b/django/contrib/admin/templates/admin/login.html
index 7a0d650036..f8653e556e 100644
--- a/django/contrib/admin/templates/admin/login.html
+++ b/django/contrib/admin/templates/admin/login.html
@@ -62,8 +62,5 @@
</div>
</form>
-<script type="text/javascript">
-document.getElementById('id_username').focus()
-</script>
</div>
{% endblock %}
diff --git a/django/contrib/admin/templates/admin/popup_response.html b/django/contrib/admin/templates/admin/popup_response.html
index bbe8908617..6be1159cd4 100644
--- a/django/contrib/admin/templates/admin/popup_response.html
+++ b/django/contrib/admin/templates/admin/popup_response.html
@@ -1,15 +1,11 @@
-{% load i18n %}<!DOCTYPE html>
+{% load i18n admin_static %}<!DOCTYPE html>
<html>
<head><title>{% trans 'Popup closing...' %}</title></head>
<body>
- <script type="text/javascript">
- {% if action == 'change' %}
- opener.dismissChangeRelatedObjectPopup(window, "{{ value|escapejs }}", "{{ obj|escapejs }}", "{{ new_value|escapejs }}");
- {% elif action == 'delete' %}
- opener.dismissDeleteRelatedObjectPopup(window, "{{ value|escapejs }}");
- {% else %}
- opener.dismissAddRelatedObjectPopup(window, "{{ value|escapejs }}", "{{ obj|escapejs }}");
- {% endif %}
+ <script type="text/javascript"
+ id="django-admin-popup-response-constants"
+ src="{% static "admin/js/popup_response.js" %}"
+ data-popup-response="{{ popup_response_data }}">
</script>
</body>
</html>
diff --git a/django/contrib/admin/templates/admin/prepopulated_fields_js.html b/django/contrib/admin/templates/admin/prepopulated_fields_js.html
index cbb898fc50..508066a9da 100644
--- a/django/contrib/admin/templates/admin/prepopulated_fields_js.html
+++ b/django/contrib/admin/templates/admin/prepopulated_fields_js.html
@@ -1,28 +1,6 @@
-{% load l10n %}
-<script type="text/javascript">
-(function($) {
- var field;
-
-{% for field in prepopulated_fields %}
- field = {
- id: '#{{ field.field.auto_id }}',
- dependency_ids: [],
- dependency_list: [],
- maxLength: {{ field.field.field.max_length|default:"50"|unlocalize }},
- allowUnicode: {{ field.field.field.allow_unicode|default:"false"|lower }}
- };
-
- {% for dependency in field.dependencies %}
- field['dependency_ids'].push('#{{ dependency.auto_id }}');
- field['dependency_list'].push('{{ dependency.name }}');
- {% endfor %}
-
- {% comment %}
- Mark prepopulated fields in the main form and stacked inlines (.empty-form .form-row) and in tabular inlines (.empty-form.form-row)
- {% endcomment %}
- $('.empty-form .form-row .field-{{ field.field.name }}, .empty-form.form-row .field-{{ field.field.name }}').addClass('prepopulated_field');
- $(field.id).data('dependency_list', field['dependency_list'])
- .prepopulate(field['dependency_ids'], field.maxLength, field.allowUnicode);
-{% endfor %}
-})(django.jQuery);
+{% load l10n admin_static %}
+<script type="text/javascript"
+ id="django-admin-prepopulated-fields-constants"
+ src="{% static "admin/js/prepopulate_init.js" %}"
+ data-prepopulated-fields="{{ prepopulated_fields_json }}">
</script>
diff --git a/django/contrib/admin/templates/admin/search_form.html b/django/contrib/admin/templates/admin/search_form.html
index f241f14cca..be192919a9 100644
--- a/django/contrib/admin/templates/admin/search_form.html
+++ b/django/contrib/admin/templates/admin/search_form.html
@@ -3,7 +3,7 @@
<div id="toolbar"><form id="changelist-search" method="get">
<div><!-- DIV needed for valid HTML -->
<label for="searchbar"><img src="{% static "admin/img/search.svg" %}" alt="Search" /></label>
-<input type="text" size="40" name="{{ search_var }}" value="{{ cl.query }}" id="searchbar" />
+<input type="text" size="40" name="{{ search_var }}" value="{{ cl.query }}" id="searchbar" autofocus />
<input type="submit" value="{% trans 'Search' %}" />
{% if show_result_count %}
<span class="small quiet">{% blocktrans count counter=cl.result_count %}{{ counter }} result{% plural %}{{ counter }} results{% endblocktrans %} (<a href="?{% if cl.is_popup %}_popup=1{% endif %}">{% if cl.show_full_result_count %}{% blocktrans with full_result_count=cl.full_result_count %}{{ full_result_count }} total{% endblocktrans %}{% else %}{% trans "Show all" %}{% endif %}</a>)</span>
@@ -13,5 +13,4 @@
{% endfor %}
</div>
</form></div>
-<script type="text/javascript">document.getElementById("searchbar").focus();</script>
{% endif %}
diff --git a/django/contrib/admin/templates/registration/password_change_form.html b/django/contrib/admin/templates/registration/password_change_form.html
index ddb90e8f82..f005f71233 100644
--- a/django/contrib/admin/templates/registration/password_change_form.html
+++ b/django/contrib/admin/templates/registration/password_change_form.html
@@ -54,7 +54,6 @@
<input type="submit" value="{% trans 'Change my password' %}" class="default" />
</div>
-<script type="text/javascript">document.getElementById("id_old_password").focus();</script>
</div>
</form></div>
diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py
index f4033e235b..d3fcafc3af 100644
--- a/django/contrib/admin/templatetags/admin_list.py
+++ b/django/contrib/admin/templatetags/admin_list.py
@@ -19,7 +19,7 @@ from django.template.loader import get_template
from django.utils import formats
from django.utils.deprecation import RemovedInDjango20Warning
from django.utils.encoding import force_text
-from django.utils.html import escapejs, format_html
+from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.text import capfirst
from django.utils.translation import ugettext as _
@@ -254,13 +254,11 @@ def items_for_result(cl, result, form):
else:
attr = pk
value = result.serializable_value(attr)
- result_id = escapejs(value)
link_or_text = format_html(
'<a href="{}"{}>{}</a>',
url,
format_html(
- ' onclick="opener.dismissRelatedLookupPopup(window, '
- '&#39;{}&#39;); return false;"', result_id
+ ' data-popup-opener="{}"', value
) if cl.is_popup else '',
result_repr)
diff --git a/django/contrib/admin/templatetags/admin_modify.py b/django/contrib/admin/templatetags/admin_modify.py
index 30f9ac4a15..3766b0ffb9 100644
--- a/django/contrib/admin/templatetags/admin_modify.py
+++ b/django/contrib/admin/templatetags/admin_modify.py
@@ -1,3 +1,5 @@
+import json
+
from django import template
register = template.Library()
@@ -17,7 +19,22 @@ def prepopulated_fields_js(context):
for inline_admin_form in inline_admin_formset:
if inline_admin_form.original is None:
prepopulated_fields.extend(inline_admin_form.prepopulated_fields)
- context.update({'prepopulated_fields': prepopulated_fields})
+
+ prepopulated_fields_json = []
+ for field in prepopulated_fields:
+ prepopulated_fields_json.append({
+ "id": "#%s" % field["field"].auto_id,
+ "name": field["field"].name,
+ "dependency_ids": ["#%s" % dependency.auto_id for dependency in field["dependencies"]],
+ "dependency_list": [dependency.name for dependency in field["dependencies"]],
+ "maxLength": field["field"].field.max_length or 50,
+ "allowUnicode": getattr(field["field"].field, "allow_unicode", False)
+ })
+
+ context.update({
+ 'prepopulated_fields': prepopulated_fields,
+ 'prepopulated_fields_json': json.dumps(prepopulated_fields_json),
+ })
return context
diff --git a/django/contrib/admin/tests.py b/django/contrib/admin/tests.py
index 5415d5fcaa..b1846c83d2 100644
--- a/django/contrib/admin/tests.py
+++ b/django/contrib/admin/tests.py
@@ -2,10 +2,21 @@ import os
from unittest import SkipTest
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
+from django.test import modify_settings
from django.utils.module_loading import import_string
from django.utils.translation import ugettext as _
+class CSPMiddleware(object):
+ """The admin's JavaScript should be compatible with CSP."""
+ def process_response(self, request, response):
+ response['Content-Security-Policy'] = "default-src 'self'"
+ return response
+
+
+@modify_settings(
+ MIDDLEWARE_CLASSES={'append': 'django.contrib.admin.tests.CSPMiddleware'},
+)
class AdminSeleniumWebDriverTestCase(StaticLiveServerTestCase):
available_apps = [
diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py
index 3264355ed6..e6c13a53f1 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, escapejs, format_html, format_html_join, smart_urlquote,
+ escape, format_html, format_html_join, smart_urlquote,
)
from django.utils.safestring import mark_safe
from django.utils.text import Truncator
@@ -45,16 +45,11 @@ class FilteredSelectMultiple(forms.SelectMultiple):
attrs['class'] = 'selectfilter'
if self.is_stacked:
attrs['class'] += 'stacked'
- output = [
- super(FilteredSelectMultiple, self).render(name, value, attrs, choices),
- '<script type="text/javascript">addEvent(window, "load", function(e) {',
- # TODO: "id_" is hard-coded here. This should instead use the
- # correct API to determine the ID dynamically.
- 'SelectFilter.init("id_%s", "%s", %s); });</script>\n' % (
- name, escapejs(self.verbose_name), int(self.is_stacked),
- ),
- ]
- return mark_safe(''.join(output))
+
+ attrs['data-field-name'] = self.verbose_name
+ attrs['data-is-stacked'] = int(self.is_stacked)
+ output = super(FilteredSelectMultiple, self).render(name, value, attrs, choices)
+ return mark_safe(output)
class AdminDateWidget(forms.DateInput):
diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py
index b1cedc916c..1e86ece25d 100644
--- a/django/contrib/auth/forms.py
+++ b/django/contrib/auth/forms.py
@@ -78,6 +78,10 @@ class UserCreationForm(forms.ModelForm):
model = User
fields = ("username",)
+ def __init__(self, *args, **kwargs):
+ super(UserCreationForm, self).__init__(*args, **kwargs)
+ self.fields['username'].widget.attrs.update({'autofocus': ''})
+
def clean_password2(self):
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
@@ -126,7 +130,10 @@ class AuthenticationForm(forms.Form):
Base class for authenticating users. Extend this to get a form that accepts
username/password logins.
"""
- username = forms.CharField(max_length=254)
+ username = forms.CharField(
+ max_length=254,
+ widget=forms.TextInput(attrs={'autofocus': ''}),
+ )
password = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
error_messages = {
@@ -306,8 +313,10 @@ class PasswordChangeForm(SetPasswordForm):
'password_incorrect': _("Your old password was entered incorrectly. "
"Please enter it again."),
})
- old_password = forms.CharField(label=_("Old password"),
- widget=forms.PasswordInput)
+ old_password = forms.CharField(
+ label=_("Old password"),
+ widget=forms.PasswordInput(attrs={'autofocus': ''}),
+ )
field_order = ['old_password', 'new_password1', 'new_password2']
@@ -334,7 +343,7 @@ class AdminPasswordChangeForm(forms.Form):
required_css_class = 'required'
password1 = forms.CharField(
label=_("Password"),
- widget=forms.PasswordInput,
+ widget=forms.PasswordInput(attrs={'autofocus': ''}),
help_text=password_validation.password_validators_help_text_html(),
)
password2 = forms.CharField(
diff --git a/docs/ref/contrib/admin/javascript.txt b/docs/ref/contrib/admin/javascript.txt
index f953b269ad..b517aa18cc 100644
--- a/docs/ref/contrib/admin/javascript.txt
+++ b/docs/ref/contrib/admin/javascript.txt
@@ -26,10 +26,17 @@ In your custom ``change_form.html`` template, extend the
.. code-block:: html+django
{% extends 'admin/change_form.html' %}
+ {% load admin_static %}
{% block admin_change_form_document_ready %}
{{ block.super }}
- <script type="text/javascript">
+ <script type="text/javascript" src="{% static 'app/formset_handlers.js' %}></script>
+ </script>
+ {% endblock %}
+
+.. snippet:: javascript
+ :filename: app/static/app/formset_handlers.js
+
(function($) {
$(document).on('formset:added', function(event, $row, formsetName) {
if (formsetName == 'author_set') {
@@ -41,8 +48,6 @@ In your custom ``change_form.html`` template, extend the
// Row removed
});
})(django.jQuery);
- </script>
- {% endblock %}
Two points to keep in mind:
@@ -60,16 +65,20 @@ namespace, just listen to the event triggered from there. For example:
.. code-block:: html+django
{% extends 'admin/change_form.html' %}
+ {% load admin_static %}
{% block admin_change_form_document_ready %}
{{ block.super }}
- <script type="text/javascript">
- django.jQuery(document).on('formset:added', function(event, $row, formsetName) {
- // Row added
- });
-
- django.jQuery(document).on('formset:removed', function(event, $row, formsetName) {
- // Row removed
- });
- </script>
+ <script type="text/javascript" src="{% static 'app/unregistered_handlers.js' %}></script>
{% endblock %}
+
+.. snippet:: javascript
+ :filename: app/static/app/unregistered_handlers.js
+
+ django.jQuery(document).on('formset:added', function(event, $row, formsetName) {
+ // Row added
+ });
+
+ django.jQuery(document).on('formset:removed', function(event, $row, formsetName) {
+ // Row removed
+ });
diff --git a/docs/releases/1.10.txt b/docs/releases/1.10.txt
index 88ef9fd2e6..64fb7921f9 100644
--- a/docs/releases/1.10.txt
+++ b/docs/releases/1.10.txt
@@ -39,6 +39,9 @@ Minor features
* The success message that appears after adding or editing an object now
contains a link to the object's change form.
+* All inline JavaScript is removed so you can enable the
+ ``Content-Security-Policy`` HTTP header if you wish.
+
:mod:`django.contrib.admindocs`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/tests/admin_custom_urls/tests.py b/tests/admin_custom_urls/tests.py
index eff23931ef..5e9a119b6a 100644
--- a/tests/admin_custom_urls/tests.py
+++ b/tests/admin_custom_urls/tests.py
@@ -74,7 +74,6 @@ class AdminCustomUrlsTest(TestCase):
"description": "Description of added action",
}
response = self.client.post(reverse('admin_custom_urls:admin_custom_urls_action_add'), post_data)
- self.assertContains(response, 'dismissAddRelatedObjectPopup')
self.assertContains(response, 'Action added through a popup')
def test_admin_URLs_no_clash(self):
diff --git a/tests/admin_inlines/test_templates.py b/tests/admin_inlines/test_templates.py
index 1ed52c9115..236ec5df7b 100644
--- a/tests/admin_inlines/test_templates.py
+++ b/tests/admin_inlines/test_templates.py
@@ -1,5 +1,7 @@
from __future__ import unicode_literals
+import json
+
from django.template.loader import render_to_string
from django.test import SimpleTestCase
@@ -8,14 +10,16 @@ class TestTemplates(SimpleTestCase):
def test_javascript_escaping(self):
context = {
'inline_admin_formset': {
- 'formset': {'prefix': 'my-prefix'},
- 'opts': {'verbose_name': 'verbose name\\'},
+ 'inline_formset_data': json.dumps({
+ '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)
+ self.assertIn('&quot;prefix&quot;: &quot;my-prefix&quot;', output)
+ self.assertIn('&quot;verbose_name&quot;: &quot;verbose name\\\\&quot;', 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)
+ self.assertIn('&quot;prefix&quot;: &quot;my-prefix&quot;', output)
+ self.assertIn('&quot;verbose_name&quot;: &quot;verbose name\\\\&quot;', output)
diff --git a/tests/admin_inlines/tests.py b/tests/admin_inlines/tests.py
index 4f2377b2de..b2102480bc 100644
--- a/tests/admin_inlines/tests.py
+++ b/tests/admin_inlines/tests.py
@@ -76,7 +76,7 @@ class TestInline(TestDataMixin, TestCase):
# The heading for the m2m inline block uses the right text
self.assertContains(response, '<h2>Author-book relationships</h2>')
# The "add another" label is correct
- self.assertContains(response, 'Add another Author\\u002Dbook relationship')
+ self.assertContains(response, 'Add another Author-book relationship')
# The '+' is dropped from the autogenerated form prefix (Author_books+)
self.assertContains(response, 'id="id_Author_books-TOTAL_FORMS"')
@@ -133,14 +133,20 @@ class TestInline(TestDataMixin, TestCase):
response = self.client.get(reverse('admin:admin_inlines_novel_add'))
self.assertEqual(response.status_code, 200)
# View should have the child inlines section
- self.assertContains(response, '<div class="inline-group" id="chapter_set-group">')
+ self.assertContains(
+ response,
+ '<div class="js-inline-admin-formset inline-group" id="chapter_set-group"'
+ )
def test_callable_lookup(self):
"""Admin inline should invoke local callable when its name is listed in readonly_fields"""
response = self.client.get(reverse('admin:admin_inlines_poll_add'))
self.assertEqual(response.status_code, 200)
# Add parent object view should have the child inlines section
- self.assertContains(response, '<div class="inline-group" id="question_set-group">')
+ self.assertContains(
+ response,
+ '<div class="js-inline-admin-formset inline-group" id="question_set-group"'
+ )
# The right callable should be used for the inline readonly_fields
# column cells
self.assertContains(response, '<p>Callable in QuestionInline</p>')
@@ -548,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, '<h2>Author-book relationships</h2>')
- self.assertNotContains(response, 'Add another Author\\u002DBook Relationship')
+ self.assertNotContains(response, 'Add another Author-Book Relationship')
self.assertNotContains(response, 'id="id_Author_books-TOTAL_FORMS"')
def test_inline_add_fk_noperm(self):
@@ -562,7 +568,7 @@ class TestInlinePermissions(TestCase):
response = self.client.get(self.author_change_url)
# No change permission on books, so no inline
self.assertNotContains(response, '<h2>Author-book relationships</h2>')
- self.assertNotContains(response, 'Add another Author\\u002DBook Relationship')
+ self.assertNotContains(response, 'Add another Author-Book Relationship')
self.assertNotContains(response, 'id="id_Author_books-TOTAL_FORMS"')
def test_inline_change_fk_noperm(self):
@@ -578,7 +584,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, '<h2>Author-book relationships</h2>')
- self.assertNotContains(response, 'Add another Author\\u002DBook Relationship')
+ self.assertNotContains(response, 'Add another Author-Book Relationship')
self.assertNotContains(response, 'id="id_Author_books-TOTAL_FORMS"')
def test_inline_add_fk_add_perm(self):
@@ -597,7 +603,7 @@ class TestInlinePermissions(TestCase):
response = self.client.get(self.author_change_url)
# No change permission on books, so no inline
self.assertNotContains(response, '<h2>Author-book relationships</h2>')
- self.assertNotContains(response, 'Add another Author\\u002DBook Relationship')
+ self.assertNotContains(response, 'Add another Author-Book Relationship')
self.assertNotContains(response, 'id="id_Author_books-TOTAL_FORMS"')
self.assertNotContains(response, 'id="id_Author_books-0-DELETE"')
@@ -607,7 +613,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, '<h2>Author-book relationships</h2>')
- self.assertContains(response, 'Add another Author\\u002Dbook relationship')
+ self.assertContains(response, 'Add another Author-book relationship')
self.assertContains(response, '<input type="hidden" id="id_Author_books-TOTAL_FORMS" '
'value="4" name="Author_books-TOTAL_FORMS" />', html=True)
self.assertContains(
diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py
index 7ccaf75c6a..db960b34b0 100644
--- a/tests/admin_views/tests.py
+++ b/tests/admin_views/tests.py
@@ -2,6 +2,7 @@
from __future__ import unicode_literals
import datetime
+import json
import os
import re
import unittest
@@ -260,8 +261,7 @@ class AdminViewBasicTest(AdminViewBasicTestCase):
'date_1': '14:55:39',
}
response = self.client.post(reverse('admin:admin_views_article_add'), post_data)
- self.assertContains(response, 'dismissAddRelatedObjectPopup')
- self.assertContains(response, 'title with a new\\u000Aline')
+ self.assertContains(response, 'title with a new\\nline')
def test_basic_edit_POST(self):
"""
@@ -734,7 +734,7 @@ class AdminViewBasicTest(AdminViewBasicTestCase):
"""
actor = Actor.objects.create(name="Palin", age=27)
response = self.client.get("%s?%s" % (reverse('admin:admin_views_actor_changelist'), IS_POPUP_VAR))
- self.assertContains(response, "opener.dismissRelatedLookupPopup(window, &#39;%s&#39;)" % actor.pk)
+ self.assertContains(response, 'data-popup-opener="%s"' % actor.pk)
def test_hide_change_password(self):
"""
@@ -3445,27 +3445,23 @@ action)</option>
self.assertEqual(response.template_name, 'admin/popup_response.html')
def test_popup_template_escaping(self):
- context = {
+ popup_response_data = json.dumps({
'new_value': 'new_value\\',
'obj': 'obj\\',
'value': 'value\\',
+ })
+ context = {
+ 'popup_response_data': popup_response_data,
}
output = render_to_string('admin/popup_response.html', context)
self.assertIn(
- 'opener.dismissAddRelatedObjectPopup(window, "value\\u005C", "obj\\u005C");', output
+ r'&quot;value\\&quot;', output
)
-
- context['action'] = 'change'
- output = render_to_string('admin/popup_response.html', context)
self.assertIn(
- 'opener.dismissChangeRelatedObjectPopup(window, '
- '"value\\u005C", "obj\\u005C", "new_value\\u005C");', output
+ r'&quot;new_value\\&quot;', output
)
-
- context['action'] = 'delete'
- output = render_to_string('admin/popup_response.html', context)
self.assertIn(
- 'opener.dismissDeleteRelatedObjectPopup(window, "value\\u005C");', output
+ r'&quot;obj\\&quot;', output
)
@@ -4273,16 +4269,19 @@ class PrePopulatedTest(TestCase):
def test_prepopulated_on(self):
response = self.client.get(reverse('admin:admin_views_prepopulatedpost_add'))
- self.assertContains(response, "id: '#id_slug',")
- self.assertContains(response, "field['dependency_ids'].push('#id_title');")
- self.assertContains(response, "id: '#id_prepopulatedsubpost_set-0-subslug',")
+ self.assertContains(response, "&quot;id&quot;: &quot;#id_slug&quot;")
+ self.assertContains(response, "&quot;dependency_ids&quot;: [&quot;#id_title&quot;]")
+ self.assertContains(response, "&quot;id&quot;: &quot;#id_prepopulatedsubpost_set-0-subslug&quot;")
def test_prepopulated_off(self):
response = self.client.get(reverse('admin:admin_views_prepopulatedpost_change', args=(self.p1.pk,)))
self.assertContains(response, "A Long Title")
- self.assertNotContains(response, "id: '#id_slug'")
- self.assertNotContains(response, "field['dependency_ids'].push('#id_title');")
- self.assertNotContains(response, "id: '#id_prepopulatedsubpost_set-0-subslug',")
+ self.assertNotContains(response, "&quot;id&quot;: &quot;#id_slug&quot;")
+ self.assertNotContains(response, "&quot;dependency_ids&quot;: [&quot;#id_title&quot;]")
+ self.assertNotContains(
+ response,
+ "&quot;id&quot;: &quot;#id_prepopulatedsubpost_set-0-subslug&quot;"
+ )
@override_settings(USE_THOUSAND_SEPARATOR=True, USE_L10N=True)
def test_prepopulated_maxlength_localized(self):
@@ -4291,7 +4290,7 @@ class PrePopulatedTest(TestCase):
that maxLength (in the JavaScript) is rendered without separators.
"""
response = self.client.get(reverse('admin:admin_views_prepopulatedpostlargeslug_add'))
- self.assertContains(response, "maxLength: 1000") # instead of 1,000
+ self.assertContains(response, "&quot;maxLength&quot;: 1000") # instead of 1,000
@override_settings(PASSWORD_HASHERS=['django.contrib.auth.hashers.SHA1PasswordHasher'],
@@ -4933,7 +4932,7 @@ class UserAdminTest(TestCase):
'_save': '1',
}
response = self.client.post(reverse('admin:auth_user_add') + '?_popup=1', data, follow=True)
- self.assertContains(response, 'dismissAddRelatedObjectPopup')
+ self.assertContains(response, '&quot;obj&quot;: &quot;newuser&quot;')
def test_user_fk_change_popup(self):
"""User change through a FK popup should return the appropriate JavaScript response."""
@@ -4957,7 +4956,8 @@ class UserAdminTest(TestCase):
'_save': '1',
}
response = self.client.post(url, data, follow=True)
- self.assertContains(response, 'dismissChangeRelatedObjectPopup')
+ self.assertContains(response, '&quot;obj&quot;: &quot;newuser&quot;')
+ self.assertContains(response, '&quot;action&quot;: &quot;change&quot;')
def test_user_fk_delete_popup(self):
"""User deletion through a FK popup should return the appropriate JavaScript response."""
@@ -4973,7 +4973,7 @@ class UserAdminTest(TestCase):
'_popup': '1',
}
response = self.client.post(url, data, follow=True)
- self.assertContains(response, 'dismissDeleteRelatedObjectPopup')
+ self.assertContains(response, '&quot;action&quot;: &quot;delete&quot;')
def test_save_add_another_button(self):
user_count = User.objects.count()
diff --git a/tests/admin_widgets/tests.py b/tests/admin_widgets/tests.py
index 99b7b85afa..46f2d2a76e 100644
--- a/tests/admin_widgets/tests.py
+++ b/tests/admin_widgets/tests.py
@@ -271,9 +271,8 @@ class FilteredSelectMultipleWidgetTest(SimpleTestCase):
w = widgets.FilteredSelectMultiple('test\\', False)
self.assertHTMLEqual(
w.render('test', 'test'),
- '<select multiple="multiple" name="test" class="selectfilter">\n</select>'
- '<script type="text/javascript">addEvent(window, "load", function(e) '
- '{SelectFilter.init("id_test", "test\\u005C", 0); });</script>\n'
+ '<select multiple="multiple" name="test" class="selectfilter" '
+ 'data-field-name="test\\" data-is-stacked="0">\n</select>'
)
def test_stacked_render(self):
@@ -281,9 +280,8 @@ class FilteredSelectMultipleWidgetTest(SimpleTestCase):
w = widgets.FilteredSelectMultiple('test\\', True)
self.assertHTMLEqual(
w.render('test', 'test'),
- '<select multiple="multiple" name="test" class="selectfilterstacked">\n</select>'
- '<script type="text/javascript">addEvent(window, "load", function(e) '
- '{SelectFilter.init("id_test", "test\\u005C", 1); });</script>\n'
+ '<select multiple="multiple" name="test" class="selectfilterstacked" '
+ 'data-field-name="test\\" data-is-stacked="1">\n</select>'
)