diff options
| author | Thomas Grainger <tom@yplanapp.com> | 2015-11-23 10:46:19 +0000 |
|---|---|---|
| committer | Tim Graham <timograham@gmail.com> | 2015-12-05 15:51:57 -0500 |
| commit | d638cdc42acec608c1967f44af6be32a477c239f (patch) | |
| tree | 44e102c555476dae8f2f339b6fa11bb57f982386 | |
| parent | 105028eec64064c11dfd3d5415317ba1fab81214 (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.
42 files changed, 455 insertions, 275 deletions
@@ -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, ' - ''{}'); 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('"prefix": "my-prefix"', output) + self.assertIn('"verbose_name": "verbose name\\\\"', 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('"prefix": "my-prefix"', output) + self.assertIn('"verbose_name": "verbose name\\\\"', 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, '%s')" % 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'"value\\"', 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'"new_value\\"', output ) - - context['action'] = 'delete' - output = render_to_string('admin/popup_response.html', context) self.assertIn( - 'opener.dismissDeleteRelatedObjectPopup(window, "value\\u005C");', output + r'"obj\\"', 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, ""id": "#id_slug"") + self.assertContains(response, ""dependency_ids": ["#id_title"]") + self.assertContains(response, ""id": "#id_prepopulatedsubpost_set-0-subslug"") 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, ""id": "#id_slug"") + self.assertNotContains(response, ""dependency_ids": ["#id_title"]") + self.assertNotContains( + response, + ""id": "#id_prepopulatedsubpost_set-0-subslug"" + ) @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, ""maxLength": 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, '"obj": "newuser"') 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, '"obj": "newuser"') + self.assertContains(response, '"action": "change"') 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, '"action": "delete"') 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>' ) |
