Kaydet (Commit) d638cdc4 authored tarafından Thomas Grainger's avatar Thomas Grainger Kaydeden (comit) Tim Graham

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.
üst 105028ee
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
"no-octal-escape": [2], "no-octal-escape": [2],
"no-underscore-dangle": [2], "no-underscore-dangle": [2],
"no-unused-vars": [2, {"vars": "local", "args": "none"}], "no-unused-vars": [2, {"vars": "local", "args": "none"}],
"no-script-url": [1], "no-script-url": [2],
"no-shadow": [2, {"hoist": "functions"}], "no-shadow": [2, {"hoist": "functions"}],
"quotes": [0, "single"], "quotes": [0, "single"],
"linebreak-style": [2, "unix"], "linebreak-style": [2, "unix"],
......
...@@ -74,6 +74,7 @@ def delete_selected(modeladmin, request, queryset): ...@@ -74,6 +74,7 @@ def delete_selected(modeladmin, request, queryset):
protected=protected, protected=protected,
opts=opts, opts=opts,
action_checkbox_name=helpers.ACTION_CHECKBOX_NAME, action_checkbox_name=helpers.ACTION_CHECKBOX_NAME,
media=modeladmin.media,
) )
request.current_app = modeladmin.admin_site.name request.current_app = modeladmin.admin_site.name
......
from __future__ import unicode_literals from __future__ import unicode_literals
import json
import warnings import warnings
from django import forms from django import forms
...@@ -18,7 +19,7 @@ from django.utils.deprecation import RemovedInDjango20Warning ...@@ -18,7 +19,7 @@ from django.utils.deprecation import RemovedInDjango20Warning
from django.utils.encoding import force_text, smart_text from django.utils.encoding import force_text, smart_text
from django.utils.html import conditional_escape, format_html from django.utils.html import conditional_escape, format_html
from django.utils.safestring import mark_safe 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' ACTION_CHECKBOX_NAME = '_selected_action'
...@@ -276,6 +277,19 @@ class InlineAdminFormSet(object): ...@@ -276,6 +277,19 @@ class InlineAdminFormSet(object):
'help_text': form_field.help_text, '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): def _media(self):
media = self.opts.media + self.formset.media media = self.opts.media + self.formset.media
for fs in self: for fs in self:
......
from __future__ import unicode_literals from __future__ import unicode_literals
import copy import copy
import json
import operator import operator
from collections import OrderedDict from collections import OrderedDict
from functools import partial, reduce, update_wrapper from functools import partial, reduce, update_wrapper
...@@ -40,7 +41,7 @@ from django.template.response import SimpleTemplateResponse, TemplateResponse ...@@ -40,7 +41,7 @@ from django.template.response import SimpleTemplateResponse, TemplateResponse
from django.utils import six from django.utils import six
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.utils.encoding import force_text, python_2_unicode_compatible 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.http import urlencode, urlquote
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.text import capfirst, get_text_list from django.utils.text import capfirst, get_text_list
...@@ -568,9 +569,9 @@ class ModelAdmin(BaseModelAdmin): ...@@ -568,9 +569,9 @@ class ModelAdmin(BaseModelAdmin):
extra = '' if settings.DEBUG else '.min' extra = '' if settings.DEBUG else '.min'
js = [ js = [
'core.js', 'core.js',
'admin/RelatedObjectLookups.js',
'vendor/jquery/jquery%s.js' % extra, 'vendor/jquery/jquery%s.js' % extra,
'jquery.init.js', 'jquery.init.js',
'admin/RelatedObjectLookups.js',
'actions%s.js' % extra, 'actions%s.js' % extra,
'urlify.js', 'urlify.js',
'prepopulate%s.js' % extra, 'prepopulate%s.js' % extra,
...@@ -1084,9 +1085,12 @@ class ModelAdmin(BaseModelAdmin): ...@@ -1084,9 +1085,12 @@ class ModelAdmin(BaseModelAdmin):
else: else:
attr = obj._meta.pk.attname attr = obj._meta.pk.attname
value = obj.serializable_value(attr) value = obj.serializable_value(attr)
return SimpleTemplateResponse('admin/popup_response.html', { popup_response_data = json.dumps({
'value': value, '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: elif "_continue" in request.POST:
...@@ -1132,11 +1136,14 @@ class ModelAdmin(BaseModelAdmin): ...@@ -1132,11 +1136,14 @@ class ModelAdmin(BaseModelAdmin):
# Retrieve the `object_id` from the resolved pattern arguments. # Retrieve the `object_id` from the resolved pattern arguments.
value = request.resolver_match.args[0] value = request.resolver_match.args[0]
new_value = obj.serializable_value(attr) new_value = obj.serializable_value(attr)
return SimpleTemplateResponse('admin/popup_response.html', { popup_response_data = json.dumps({
'action': 'change', 'action': 'change',
'value': escape(value), 'value': value,
'obj': escapejs(obj), 'obj': six.text_type(obj),
'new_value': escape(new_value), 'new_value': new_value,
})
return SimpleTemplateResponse('admin/popup_response.html', {
'popup_response_data': popup_response_data,
}) })
opts = self.model._meta opts = self.model._meta
...@@ -1300,9 +1307,12 @@ class ModelAdmin(BaseModelAdmin): ...@@ -1300,9 +1307,12 @@ class ModelAdmin(BaseModelAdmin):
opts = self.model._meta opts = self.model._meta
if IS_POPUP_VAR in request.POST: if IS_POPUP_VAR in request.POST:
return SimpleTemplateResponse('admin/popup_response.html', { popup_response_data = json.dumps({
'action': 'delete', '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, self.message_user(request,
...@@ -1332,6 +1342,7 @@ class ModelAdmin(BaseModelAdmin): ...@@ -1332,6 +1342,7 @@ class ModelAdmin(BaseModelAdmin):
context.update( context.update(
to_field_var=TO_FIELD_VAR, to_field_var=TO_FIELD_VAR,
is_popup_var=IS_POPUP_VAR, is_popup_var=IS_POPUP_VAR,
media=self.media,
) )
return TemplateResponse(request, return TemplateResponse(request,
......
...@@ -75,15 +75,15 @@ Requires core.js, SelectBox.js and addevent.js. ...@@ -75,15 +75,15 @@ Requires core.js, SelectBox.js and addevent.js.
filter_input.id = field_id + '_input'; filter_input.id = field_id + '_input';
selector_available.appendChild(from_box); 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'; choose_all.className = 'selector-chooseall';
// <ul class="selector-chooser"> // <ul class="selector-chooser">
var selector_chooser = quickElement('ul', selector_div); var selector_chooser = quickElement('ul', selector_div);
selector_chooser.className = 'selector-chooser'; 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'; 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'; remove_link.className = 'selector-remove';
// <div class="selector-chosen"> // <div class="selector-chosen">
...@@ -105,7 +105,7 @@ Requires core.js, SelectBox.js and addevent.js. ...@@ -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')); 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'; 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'; clear_all.className = 'selector-clearall';
from_box.setAttribute('name', from_box.getAttribute('name') + '_old'); from_box.setAttribute('name', from_box.getAttribute('name') + '_old');
...@@ -195,4 +195,12 @@ Requires core.js, SelectBox.js and addevent.js. ...@@ -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); })(django.jQuery);
/*global _actions_icnt, gettext, interpolate, ngettext*/ /*global gettext, interpolate, ngettext*/
(function($) { (function($) {
'use strict'; 'use strict';
var lastChecked; var lastChecked;
...@@ -41,12 +41,13 @@ ...@@ -41,12 +41,13 @@
}, },
updateCounter = function() { updateCounter = function() {
var sel = $(actionCheckboxes).filter(":checked").length; 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 // and contains the total amount of objects in the queryset
var actions_icnt = $('.action-counter').data('actionsIcnt');
$(options.counterContainer).html(interpolate( $(options.counterContainer).html(interpolate(
ngettext('%(sel)s of %(cnt)s selected', '%(sel)s of %(cnt)s selected', sel), { ngettext('%(sel)s of %(cnt)s selected', '%(sel)s of %(cnt)s selected', sel), {
sel: sel, sel: sel,
cnt: _actions_icnt cnt: actions_icnt
}, true)); }, true));
$(options.allToggle).prop("checked", function() { $(options.allToggle).prop("checked", function() {
var value; var value;
...@@ -143,4 +144,10 @@ ...@@ -143,4 +144,10 @@
allToggle: "#action-toggle", allToggle: "#action-toggle",
selectedClass: "selected" selectedClass: "selected"
}; };
$(document).ready(function() {
var $actionsEls = $('tr input.action-select');
if ($actionsEls.length > 0) {
$actionsEls.actions();
}
});
})(django.jQuery); })(django.jQuery);
(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(); (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)},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); 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();
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, 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&&
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.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 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('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"}})(django.jQuery); 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);
...@@ -105,11 +105,22 @@ ...@@ -105,11 +105,22 @@
shortcuts_span.className = DateTimeShortcuts.shortCutsClass; shortcuts_span.className = DateTimeShortcuts.shortCutsClass;
inp.parentNode.insertBefore(shortcuts_span, inp.nextSibling); inp.parentNode.insertBefore(shortcuts_span, inp.nextSibling);
var now_link = document.createElement('a'); 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'))); 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'); var clock_link = document.createElement('a');
clock_link.setAttribute('href', 'javascript:DateTimeShortcuts.openClock(' + num + ');'); clock_link.setAttribute('href', '#');
clock_link.id = DateTimeShortcuts.clockLinkName + num; 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( quickElement(
'span', clock_link, '', 'span', clock_link, '',
'class', 'clock-icon', 'class', 'clock-icon',
...@@ -146,15 +157,40 @@ ...@@ -146,15 +157,40 @@
quickElement('h2', clock_box, gettext('Choose a time')); quickElement('h2', clock_box, gettext('Choose a time'));
var time_list = quickElement('ul', clock_box); var time_list = quickElement('ul', clock_box);
time_list.className = 'timelist'; time_list.className = 'timelist';
quickElement("a", quickElement("li", time_list), gettext("Now"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", -1);"); var time_link = quickElement("a", quickElement("li", time_list), gettext("Now"), "href", "#");
quickElement("a", quickElement("li", time_list), gettext("Midnight"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", 0);"); addEvent(time_link, 'click', function(e) {
quickElement("a", quickElement("li", time_list), gettext("6 a.m."), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", 6);"); e.preventDefault();
quickElement("a", quickElement("li", time_list), gettext("Noon"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", 12);"); DateTimeShortcuts.handleClockQuicklink(num, -1);
quickElement("a", quickElement("li", time_list), gettext("6 p.m."), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", 18);"); });
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); var cancel_p = quickElement('p', clock_box);
cancel_p.className = 'calendar-cancel'; 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) { django.jQuery(document).bind('keyup', function(event) {
if (event.which === 27) { if (event.which === 27) {
// ESC key closes popup // ESC key closes popup
...@@ -213,11 +249,21 @@ ...@@ -213,11 +249,21 @@
shortcuts_span.className = DateTimeShortcuts.shortCutsClass; shortcuts_span.className = DateTimeShortcuts.shortCutsClass;
inp.parentNode.insertBefore(shortcuts_span, inp.nextSibling); inp.parentNode.insertBefore(shortcuts_span, inp.nextSibling);
var today_link = document.createElement('a'); 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'))); 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'); var cal_link = document.createElement('a');
cal_link.setAttribute('href', 'javascript:DateTimeShortcuts.openCalendar(' + num + ');'); cal_link.setAttribute('href', '#');
cal_link.id = DateTimeShortcuts.calendarLinkName + num; 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( quickElement(
'span', cal_link, '', 'span', cal_link, '',
'class', 'date-icon', 'class', 'date-icon',
...@@ -255,10 +301,19 @@ ...@@ -255,10 +301,19 @@
// next-prev links // next-prev links
var cal_nav = quickElement('div', cal_box); 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'; 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'; cal_nav_next.className = 'calendarnav-next';
addEvent(cal_nav_next, 'click', function(e) {
e.preventDefault();
DateTimeShortcuts.drawNext(num);
});
// main box // main box
var cal_main = quickElement('div', cal_box, '', 'id', DateTimeShortcuts.calendarDivName2 + num); var cal_main = quickElement('div', cal_box, '', 'id', DateTimeShortcuts.calendarDivName2 + num);
...@@ -269,16 +324,32 @@ ...@@ -269,16 +324,32 @@
// calendar shortcuts // calendar shortcuts
var shortcuts = quickElement('div', cal_box); var shortcuts = quickElement('div', cal_box);
shortcuts.className = 'calendar-shortcuts'; 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')); 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')); 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 // cancel bar
var cancel_p = quickElement('p', cal_box); var cancel_p = quickElement('p', cal_box);
cancel_p.className = 'calendar-cancel'; 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) { django.jQuery(document).bind('keyup', function(event) {
if (event.which === 27) { if (event.which === 27) {
// ESC key closes popup // ESC key closes popup
...@@ -340,15 +411,11 @@ ...@@ -340,15 +411,11 @@
format = format.replace('\n', '\\n'); format = format.replace('\n', '\\n');
format = format.replace('\t', '\\t'); format = format.replace('\t', '\\t');
format = format.replace("'", "\\'"); format = format.replace("'", "\\'");
return ["function(y, m, d) { DateTimeShortcuts.calendarInputs[", return function(y, m, d) {
num, DateTimeShortcuts.calendarInputs[num].value = new Date(y, m - 1, d).strftime(format);
"].value = new Date(y, m-1, d).strftime('", DateTimeShortcuts.calendarInputs[num].focus();
format, document.getElementById(DateTimeShortcuts.calendarDivName1 + num).style.display = 'none';
"');DateTimeShortcuts.calendarInputs[", };
num,
"].focus();document.getElementById(DateTimeShortcuts.calendarDivName1+",
num,
").style.display='none';}"].join('');
}, },
handleCalendarQuickLink: function(num, offset) { handleCalendarQuickLink: function(num, offset) {
var d = DateTimeShortcuts.now(); var d = DateTimeShortcuts.now();
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
// Handles related-objects functionality: lookup link for raw_id_fields // Handles related-objects functionality: lookup link for raw_id_fields
// and Add Another links. // and Add Another links.
(function() { (function($) {
'use strict'; 'use strict';
function html_unescape(text) { function html_unescape(text) {
...@@ -157,4 +157,11 @@ ...@@ -157,4 +157,11 @@
window.showAddAnotherPopup = showRelatedObjectPopup; window.showAddAnotherPopup = showRelatedObjectPopup;
window.dismissAddAnotherPopup = dismissAddRelatedObjectPopup; window.dismissAddAnotherPopup = dismissAddRelatedObjectPopup;
})(); $(document).ready(function() {
$("a[data-popup-opener]").click(function(event) {
event.preventDefault();
opener.dismissRelatedLookupPopup(window, $(this).data("popup-opener"));
});
});
})(django.jQuery);
/*global gettext, pgettext, get_format, quickElement, removeChildren*/ /*global gettext, pgettext, get_format, quickElement, removeChildren, addEvent*/
/* /*
calendar.js - Calendar functions by Adrian Holovaty calendar.js - Calendar functions by Adrian Holovaty
depends on core.js for utility functions like removeChildren or quickElement 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 ...@@ -100,6 +100,14 @@ depends on core.js for utility functions like removeChildren or quickElement
nonDayCell.className = "nonday"; 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 // Draw days of month
var currentDay = 1; var currentDay = 1;
for (i = startingPos; currentDay <= days; i++) { for (i = startingPos; currentDay <= days; i++) {
...@@ -121,8 +129,8 @@ depends on core.js for utility functions like removeChildren or quickElement ...@@ -121,8 +129,8 @@ depends on core.js for utility functions like removeChildren or quickElement
} }
var cell = quickElement('td', tableRow, '', 'class', todayClass); var cell = quickElement('td', tableRow, '', 'class', todayClass);
var link = quickElement('a', cell, currentDay, 'href', '#');
quickElement('a', cell, currentDay, 'href', 'javascript:void(' + callback + '(' + year + ',' + month + ',' + currentDay + '));'); addEvent(link, 'click', calendarMonth(year, month));
currentDay++; currentDay++;
} }
......
(function($) {
'use strict';
$(function() {
$('.cancel-link').click(function(e) {
e.preventDefault();
window.history.back();
});
});
})(django.jQuery);
/*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);
...@@ -49,11 +49,11 @@ ...@@ -49,11 +49,11 @@
// If forms are laid out as table rows, insert the // If forms are laid out as table rows, insert the
// "add" button in a new table row: // "add" button in a new table row:
var numCols = this.eq(-1).children().length; 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"); addButton = $parent.find("tr:last a");
} else { } else {
// Otherwise, insert it immediately after the last form: // 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 = $this.filter(":last").next().find("a");
} }
addButton.click(function(e) { addButton.click(function(e) {
...@@ -66,15 +66,15 @@ ...@@ -66,15 +66,15 @@
if (row.is("tr")) { if (row.is("tr")) {
// If the forms are laid out in table rows, insert // If the forms are laid out in table rows, insert
// the remove button into the last table cell: // 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")) { } else if (row.is("ul") || row.is("ol")) {
// If they're laid out as an ordered/unordered list, // If they're laid out as an ordered/unordered list,
// insert an <li> after the last list item: // 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 { } else {
// Otherwise, just insert the remove button as the // Otherwise, just insert the remove button as the
// last child element of the form's container: // 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() { row.find("*").each(function() {
updateElementIndex(this, options.prefix, totalForms.val()); updateElementIndex(this, options.prefix, totalForms.val());
...@@ -272,4 +272,19 @@ ...@@ -272,4 +272,19 @@
return $rows; 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); })(django.jQuery);
(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(); (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();
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+ 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",
"-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+"-"+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();
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, 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=
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")}, {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,
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"))}); 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,
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, 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=
"#"+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"))}); 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,
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); 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);
/*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;
}
})();
(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);
...@@ -3,14 +3,13 @@ ...@@ -3,14 +3,13 @@
{% for field in action_form %}{% if field.label %}<label>{{ field.label }} {% endif %}{{ field }}{% if field.label %}</label>{% endif %}{% endfor %} {% 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> <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 %} {% if actions_selection_counter %}
<script type="text/javascript">var _actions_icnt="{{ cl.result_list|length|default:"0" }}";</script> <span class="action-counter" data-actions-icnt="{{ cl.result_list|length }}">{{ selection_note }}</span>
<span class="action-counter">{{ selection_note }}</span>
{% if cl.result_count != cl.result_list|length %} {% if cl.result_count != cl.result_list|length %}
<span class="all">{{ selection_note_all }}</span> <span class="all">{{ selection_note_all }}</span>
<span class="question"> <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>
<span class="clear"><a href="javascript:;">{% trans "Clear selection" %}</a></span> <span class="clear"><a href="#">{% trans "Clear selection" %}</a></span>
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>
...@@ -8,7 +8,3 @@ ...@@ -8,7 +8,3 @@
<p>{% trans "Enter a username and password." %}</p> <p>{% trans "Enter a username and password." %}</p>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block after_field_sets %}
<script type="text/javascript">document.getElementById("id_username").focus();</script>
{% endblock %}
...@@ -54,7 +54,6 @@ ...@@ -54,7 +54,6 @@
<input type="submit" value="{% trans 'Change password' %}" class="default" /> <input type="submit" value="{% trans 'Change password' %}" class="default" />
</div> </div>
<script type="text/javascript">document.getElementById("id_password1").focus();</script>
</div> </div>
</form></div> </form></div>
{% endblock %} {% endblock %}
...@@ -68,49 +68,12 @@ ...@@ -68,49 +68,12 @@
{% block submit_buttons_bottom %}{% submit_row %}{% endblock %} {% block submit_buttons_bottom %}{% submit_row %}{% endblock %}
{% block admin_change_form_document_ready %} {% block admin_change_form_document_ready %}
<script type="text/javascript"> <script type="text/javascript"
(function($) { id="django-admin-form-add-constants"
$(document).ready(function() { src="{% static 'admin/js/change_form.js' %}"
$('.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 adminform and add %} {% if adminform and add %}
$('form#{{ opts.model_name }}_form :input:visible:enabled:first').focus() data-model-name="{{ opts.model_name }}"
{% endif %} {% endif %}>
});
})(django.jQuery);
</script> </script>
{% endblock %} {% endblock %}
......
...@@ -21,15 +21,6 @@ ...@@ -21,15 +21,6 @@
{% block extrahead %} {% block extrahead %}
{{ block.super }} {{ block.super }}
{{ media.js }} {{ 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 %} {% endblock %}
{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} change-list{% endblock %} {% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} change-list{% endblock %}
......
{% extends "admin/base_site.html" %} {% 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 %} {% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} delete-confirmation{% endblock %}
...@@ -39,7 +44,7 @@ ...@@ -39,7 +44,7 @@
{% if is_popup %}<input type="hidden" name="{{ is_popup_var }}" value="1" />{% endif %} {% 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 %} {% if to_field %}<input type="hidden" name="{{ to_field_var }}" value="{{ to_field }}" />{% endif %}
<input type="submit" value="{% trans "Yes, I'm sure" %}" /> <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> </div>
</form> </form>
{% endif %} {% endif %}
......
{% extends "admin/base_site.html" %} {% 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 %} {% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} delete-confirmation delete-selected-confirmation{% endblock %}
...@@ -42,7 +47,7 @@ ...@@ -42,7 +47,7 @@
<input type="hidden" name="action" value="delete_selected" /> <input type="hidden" name="action" value="delete_selected" />
<input type="hidden" name="post" value="yes" /> <input type="hidden" name="post" value="yes" />
<input type="submit" value="{% trans "Yes, I'm sure" %}" /> <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> </div>
</form> </form>
{% endif %} {% endif %}
......
{% load i18n admin_urls admin_static %} {% 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> <h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2>
{{ inline_admin_formset.formset.management_form }} {{ inline_admin_formset.formset.management_form }}
{{ inline_admin_formset.formset.non_form_errors }} {{ inline_admin_formset.formset.non_form_errors }}
...@@ -18,13 +21,3 @@ ...@@ -18,13 +21,3 @@
{{ inline_admin_form.fk_field.field }} {{ inline_admin_form.fk_field.field }}
</div>{% endfor %} </div>{% endfor %}
</div> </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>
{% load i18n admin_urls admin_static admin_modify %} {% 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 %}"> <div class="tabular inline-related {% if forloop.last %}last-related{% endif %}">
{{ inline_admin_formset.formset.management_form }} {{ inline_admin_formset.formset.management_form }}
<fieldset class="module"> <fieldset class="module">
...@@ -71,14 +73,3 @@ ...@@ -71,14 +73,3 @@
</fieldset> </fieldset>
</div> </div>
</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>
...@@ -62,8 +62,5 @@ ...@@ -62,8 +62,5 @@
</div> </div>
</form> </form>
<script type="text/javascript">
document.getElementById('id_username').focus()
</script>
</div> </div>
{% endblock %} {% endblock %}
{% load i18n %}<!DOCTYPE html> {% load i18n admin_static %}<!DOCTYPE html>
<html> <html>
<head><title>{% trans 'Popup closing...' %}</title></head> <head><title>{% trans 'Popup closing...' %}</title></head>
<body> <body>
<script type="text/javascript"> <script type="text/javascript"
{% if action == 'change' %} id="django-admin-popup-response-constants"
opener.dismissChangeRelatedObjectPopup(window, "{{ value|escapejs }}", "{{ obj|escapejs }}", "{{ new_value|escapejs }}"); src="{% static "admin/js/popup_response.js" %}"
{% elif action == 'delete' %} data-popup-response="{{ popup_response_data }}">
opener.dismissDeleteRelatedObjectPopup(window, "{{ value|escapejs }}");
{% else %}
opener.dismissAddRelatedObjectPopup(window, "{{ value|escapejs }}", "{{ obj|escapejs }}");
{% endif %}
</script> </script>
</body> </body>
</html> </html>
{% load l10n %} {% load l10n admin_static %}
<script type="text/javascript"> <script type="text/javascript"
(function($) { id="django-admin-prepopulated-fields-constants"
var field; src="{% static "admin/js/prepopulate_init.js" %}"
data-prepopulated-fields="{{ prepopulated_fields_json }}">
{% 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);
</script> </script>
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
<div id="toolbar"><form id="changelist-search" method="get"> <div id="toolbar"><form id="changelist-search" method="get">
<div><!-- DIV needed for valid HTML --> <div><!-- DIV needed for valid HTML -->
<label for="searchbar"><img src="{% static "admin/img/search.svg" %}" alt="Search" /></label> <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' %}" /> <input type="submit" value="{% trans 'Search' %}" />
{% if show_result_count %} {% 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> <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 @@ ...@@ -13,5 +13,4 @@
{% endfor %} {% endfor %}
</div> </div>
</form></div> </form></div>
<script type="text/javascript">document.getElementById("searchbar").focus();</script>
{% endif %} {% endif %}
...@@ -54,7 +54,6 @@ ...@@ -54,7 +54,6 @@
<input type="submit" value="{% trans 'Change my password' %}" class="default" /> <input type="submit" value="{% trans 'Change my password' %}" class="default" />
</div> </div>
<script type="text/javascript">document.getElementById("id_old_password").focus();</script>
</div> </div>
</form></div> </form></div>
......
...@@ -19,7 +19,7 @@ from django.template.loader import get_template ...@@ -19,7 +19,7 @@ from django.template.loader import get_template
from django.utils import formats from django.utils import formats
from django.utils.deprecation import RemovedInDjango20Warning from django.utils.deprecation import RemovedInDjango20Warning
from django.utils.encoding import force_text 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.safestring import mark_safe
from django.utils.text import capfirst from django.utils.text import capfirst
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
...@@ -254,13 +254,11 @@ def items_for_result(cl, result, form): ...@@ -254,13 +254,11 @@ def items_for_result(cl, result, form):
else: else:
attr = pk attr = pk
value = result.serializable_value(attr) value = result.serializable_value(attr)
result_id = escapejs(value)
link_or_text = format_html( link_or_text = format_html(
'<a href="{}"{}>{}</a>', '<a href="{}"{}>{}</a>',
url, url,
format_html( format_html(
' onclick="opener.dismissRelatedLookupPopup(window, ' ' data-popup-opener="{}"', value
'&#39;{}&#39;); return false;"', result_id
) if cl.is_popup else '', ) if cl.is_popup else '',
result_repr) result_repr)
......
import json
from django import template from django import template
register = template.Library() register = template.Library()
...@@ -17,7 +19,22 @@ def prepopulated_fields_js(context): ...@@ -17,7 +19,22 @@ def prepopulated_fields_js(context):
for inline_admin_form in inline_admin_formset: for inline_admin_form in inline_admin_formset:
if inline_admin_form.original is None: if inline_admin_form.original is None:
prepopulated_fields.extend(inline_admin_form.prepopulated_fields) 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 return context
......
...@@ -2,10 +2,21 @@ import os ...@@ -2,10 +2,21 @@ import os
from unittest import SkipTest from unittest import SkipTest
from django.contrib.staticfiles.testing import StaticLiveServerTestCase from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from django.test import modify_settings
from django.utils.module_loading import import_string from django.utils.module_loading import import_string
from django.utils.translation import ugettext as _ 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): class AdminSeleniumWebDriverTestCase(StaticLiveServerTestCase):
available_apps = [ available_apps = [
......
...@@ -15,7 +15,7 @@ from django.template.loader import render_to_string ...@@ -15,7 +15,7 @@ from django.template.loader import render_to_string
from django.utils import six from django.utils import six
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.html import ( 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.safestring import mark_safe
from django.utils.text import Truncator from django.utils.text import Truncator
...@@ -45,16 +45,11 @@ class FilteredSelectMultiple(forms.SelectMultiple): ...@@ -45,16 +45,11 @@ class FilteredSelectMultiple(forms.SelectMultiple):
attrs['class'] = 'selectfilter' attrs['class'] = 'selectfilter'
if self.is_stacked: if self.is_stacked:
attrs['class'] += 'stacked' attrs['class'] += 'stacked'
output = [
super(FilteredSelectMultiple, self).render(name, value, attrs, choices), attrs['data-field-name'] = self.verbose_name
'<script type="text/javascript">addEvent(window, "load", function(e) {', attrs['data-is-stacked'] = int(self.is_stacked)
# TODO: "id_" is hard-coded here. This should instead use the output = super(FilteredSelectMultiple, self).render(name, value, attrs, choices)
# correct API to determine the ID dynamically. return mark_safe(output)
'SelectFilter.init("id_%s", "%s", %s); });</script>\n' % (
name, escapejs(self.verbose_name), int(self.is_stacked),
),
]
return mark_safe(''.join(output))
class AdminDateWidget(forms.DateInput): class AdminDateWidget(forms.DateInput):
......
...@@ -78,6 +78,10 @@ class UserCreationForm(forms.ModelForm): ...@@ -78,6 +78,10 @@ class UserCreationForm(forms.ModelForm):
model = User model = User
fields = ("username",) fields = ("username",)
def __init__(self, *args, **kwargs):
super(UserCreationForm, self).__init__(*args, **kwargs)
self.fields['username'].widget.attrs.update({'autofocus': ''})
def clean_password2(self): def clean_password2(self):
password1 = self.cleaned_data.get("password1") password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2") password2 = self.cleaned_data.get("password2")
...@@ -126,7 +130,10 @@ class AuthenticationForm(forms.Form): ...@@ -126,7 +130,10 @@ class AuthenticationForm(forms.Form):
Base class for authenticating users. Extend this to get a form that accepts Base class for authenticating users. Extend this to get a form that accepts
username/password logins. 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) password = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
error_messages = { error_messages = {
...@@ -306,8 +313,10 @@ class PasswordChangeForm(SetPasswordForm): ...@@ -306,8 +313,10 @@ class PasswordChangeForm(SetPasswordForm):
'password_incorrect': _("Your old password was entered incorrectly. " 'password_incorrect': _("Your old password was entered incorrectly. "
"Please enter it again."), "Please enter it again."),
}) })
old_password = forms.CharField(label=_("Old password"), old_password = forms.CharField(
widget=forms.PasswordInput) label=_("Old password"),
widget=forms.PasswordInput(attrs={'autofocus': ''}),
)
field_order = ['old_password', 'new_password1', 'new_password2'] field_order = ['old_password', 'new_password1', 'new_password2']
...@@ -334,7 +343,7 @@ class AdminPasswordChangeForm(forms.Form): ...@@ -334,7 +343,7 @@ class AdminPasswordChangeForm(forms.Form):
required_css_class = 'required' required_css_class = 'required'
password1 = forms.CharField( password1 = forms.CharField(
label=_("Password"), label=_("Password"),
widget=forms.PasswordInput, widget=forms.PasswordInput(attrs={'autofocus': ''}),
help_text=password_validation.password_validators_help_text_html(), help_text=password_validation.password_validators_help_text_html(),
) )
password2 = forms.CharField( password2 = forms.CharField(
......
...@@ -26,10 +26,17 @@ In your custom ``change_form.html`` template, extend the ...@@ -26,10 +26,17 @@ In your custom ``change_form.html`` template, extend the
.. code-block:: html+django .. code-block:: html+django
{% extends 'admin/change_form.html' %} {% extends 'admin/change_form.html' %}
{% load admin_static %}
{% block admin_change_form_document_ready %} {% block admin_change_form_document_ready %}
{{ block.super }} {{ 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($) { (function($) {
$(document).on('formset:added', function(event, $row, formsetName) { $(document).on('formset:added', function(event, $row, formsetName) {
if (formsetName == 'author_set') { if (formsetName == 'author_set') {
...@@ -41,8 +48,6 @@ In your custom ``change_form.html`` template, extend the ...@@ -41,8 +48,6 @@ In your custom ``change_form.html`` template, extend the
// Row removed // Row removed
}); });
})(django.jQuery); })(django.jQuery);
</script>
{% endblock %}
Two points to keep in mind: Two points to keep in mind:
...@@ -60,16 +65,20 @@ namespace, just listen to the event triggered from there. For example: ...@@ -60,16 +65,20 @@ namespace, just listen to the event triggered from there. For example:
.. code-block:: html+django .. code-block:: html+django
{% extends 'admin/change_form.html' %} {% extends 'admin/change_form.html' %}
{% load admin_static %}
{% block admin_change_form_document_ready %} {% block admin_change_form_document_ready %}
{{ block.super }} {{ block.super }}
<script type="text/javascript"> <script type="text/javascript" src="{% static 'app/unregistered_handlers.js' %}></script>
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>
{% endblock %} {% 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
});
...@@ -39,6 +39,9 @@ Minor features ...@@ -39,6 +39,9 @@ Minor features
* The success message that appears after adding or editing an object now * The success message that appears after adding or editing an object now
contains a link to the object's change form. 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` :mod:`django.contrib.admindocs`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
......
...@@ -74,7 +74,6 @@ class AdminCustomUrlsTest(TestCase): ...@@ -74,7 +74,6 @@ class AdminCustomUrlsTest(TestCase):
"description": "Description of added action", "description": "Description of added action",
} }
response = self.client.post(reverse('admin_custom_urls:admin_custom_urls_action_add'), post_data) 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') self.assertContains(response, 'Action added through a popup')
def test_admin_URLs_no_clash(self): def test_admin_URLs_no_clash(self):
......
from __future__ import unicode_literals from __future__ import unicode_literals
import json
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.test import SimpleTestCase from django.test import SimpleTestCase
...@@ -8,14 +10,16 @@ class TestTemplates(SimpleTestCase): ...@@ -8,14 +10,16 @@ class TestTemplates(SimpleTestCase):
def test_javascript_escaping(self): def test_javascript_escaping(self):
context = { context = {
'inline_admin_formset': { 'inline_admin_formset': {
'formset': {'prefix': 'my-prefix'}, 'inline_formset_data': json.dumps({
'opts': {'verbose_name': 'verbose name\\'}, 'formset': {'prefix': 'my-prefix'},
'opts': {'verbose_name': 'verbose name\\'},
}),
}, },
} }
output = render_to_string('admin/edit_inline/stacked.html', context) output = render_to_string('admin/edit_inline/stacked.html', context)
self.assertIn('prefix: "my\\u002Dprefix",', output) self.assertIn('&quot;prefix&quot;: &quot;my-prefix&quot;', output)
self.assertIn('addText: "Add another Verbose name\\u005C"', output) self.assertIn('&quot;verbose_name&quot;: &quot;verbose name\\\\&quot;', output)
output = render_to_string('admin/edit_inline/tabular.html', context) output = render_to_string('admin/edit_inline/tabular.html', context)
self.assertIn('prefix: "my\\u002Dprefix",', output) self.assertIn('&quot;prefix&quot;: &quot;my-prefix&quot;', output)
self.assertIn('addText: "Add another Verbose name\\u005C"', output) self.assertIn('&quot;verbose_name&quot;: &quot;verbose name\\\\&quot;', output)
...@@ -76,7 +76,7 @@ class TestInline(TestDataMixin, TestCase): ...@@ -76,7 +76,7 @@ class TestInline(TestDataMixin, TestCase):
# The heading for the m2m inline block uses the right text # The heading for the m2m inline block uses the right text
self.assertContains(response, '<h2>Author-book relationships</h2>') self.assertContains(response, '<h2>Author-book relationships</h2>')
# The "add another" label is correct # 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+) # The '+' is dropped from the autogenerated form prefix (Author_books+)
self.assertContains(response, 'id="id_Author_books-TOTAL_FORMS"') self.assertContains(response, 'id="id_Author_books-TOTAL_FORMS"')
...@@ -133,14 +133,20 @@ class TestInline(TestDataMixin, TestCase): ...@@ -133,14 +133,20 @@ class TestInline(TestDataMixin, TestCase):
response = self.client.get(reverse('admin:admin_inlines_novel_add')) response = self.client.get(reverse('admin:admin_inlines_novel_add'))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# View should have the child inlines section # 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): def test_callable_lookup(self):
"""Admin inline should invoke local callable when its name is listed in readonly_fields""" """Admin inline should invoke local callable when its name is listed in readonly_fields"""
response = self.client.get(reverse('admin:admin_inlines_poll_add')) response = self.client.get(reverse('admin:admin_inlines_poll_add'))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# Add parent object view should have the child inlines section # 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 # The right callable should be used for the inline readonly_fields
# column cells # column cells
self.assertContains(response, '<p>Callable in QuestionInline</p>') self.assertContains(response, '<p>Callable in QuestionInline</p>')
...@@ -548,7 +554,7 @@ class TestInlinePermissions(TestCase): ...@@ -548,7 +554,7 @@ class TestInlinePermissions(TestCase):
response = self.client.get(reverse('admin:admin_inlines_author_add')) response = self.client.get(reverse('admin:admin_inlines_author_add'))
# No change permission on books, so no inline # No change permission on books, so no inline
self.assertNotContains(response, '<h2>Author-book relationships</h2>') 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-TOTAL_FORMS"')
def test_inline_add_fk_noperm(self): def test_inline_add_fk_noperm(self):
...@@ -562,7 +568,7 @@ class TestInlinePermissions(TestCase): ...@@ -562,7 +568,7 @@ class TestInlinePermissions(TestCase):
response = self.client.get(self.author_change_url) response = self.client.get(self.author_change_url)
# No change permission on books, so no inline # No change permission on books, so no inline
self.assertNotContains(response, '<h2>Author-book relationships</h2>') 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-TOTAL_FORMS"')
def test_inline_change_fk_noperm(self): def test_inline_change_fk_noperm(self):
...@@ -578,7 +584,7 @@ class TestInlinePermissions(TestCase): ...@@ -578,7 +584,7 @@ class TestInlinePermissions(TestCase):
response = self.client.get(reverse('admin:admin_inlines_author_add')) response = self.client.get(reverse('admin:admin_inlines_author_add'))
# No change permission on Books, so no inline # No change permission on Books, so no inline
self.assertNotContains(response, '<h2>Author-book relationships</h2>') 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-TOTAL_FORMS"')
def test_inline_add_fk_add_perm(self): def test_inline_add_fk_add_perm(self):
...@@ -597,7 +603,7 @@ class TestInlinePermissions(TestCase): ...@@ -597,7 +603,7 @@ class TestInlinePermissions(TestCase):
response = self.client.get(self.author_change_url) response = self.client.get(self.author_change_url)
# No change permission on books, so no inline # No change permission on books, so no inline
self.assertNotContains(response, '<h2>Author-book relationships</h2>') 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-TOTAL_FORMS"')
self.assertNotContains(response, 'id="id_Author_books-0-DELETE"') self.assertNotContains(response, 'id="id_Author_books-0-DELETE"')
...@@ -607,7 +613,7 @@ class TestInlinePermissions(TestCase): ...@@ -607,7 +613,7 @@ class TestInlinePermissions(TestCase):
response = self.client.get(self.author_change_url) response = self.client.get(self.author_change_url)
# We have change perm on books, so we can add/change/delete inlines # We have change perm on books, so we can add/change/delete inlines
self.assertContains(response, '<h2>Author-book relationships</h2>') 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" ' self.assertContains(response, '<input type="hidden" id="id_Author_books-TOTAL_FORMS" '
'value="4" name="Author_books-TOTAL_FORMS" />', html=True) 'value="4" name="Author_books-TOTAL_FORMS" />', html=True)
self.assertContains( self.assertContains(
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import datetime import datetime
import json
import os import os
import re import re
import unittest import unittest
...@@ -260,8 +261,7 @@ class AdminViewBasicTest(AdminViewBasicTestCase): ...@@ -260,8 +261,7 @@ class AdminViewBasicTest(AdminViewBasicTestCase):
'date_1': '14:55:39', 'date_1': '14:55:39',
} }
response = self.client.post(reverse('admin:admin_views_article_add'), post_data) response = self.client.post(reverse('admin:admin_views_article_add'), post_data)
self.assertContains(response, 'dismissAddRelatedObjectPopup') self.assertContains(response, 'title with a new\\nline')
self.assertContains(response, 'title with a new\\u000Aline')
def test_basic_edit_POST(self): def test_basic_edit_POST(self):
""" """
...@@ -734,7 +734,7 @@ class AdminViewBasicTest(AdminViewBasicTestCase): ...@@ -734,7 +734,7 @@ class AdminViewBasicTest(AdminViewBasicTestCase):
""" """
actor = Actor.objects.create(name="Palin", age=27) actor = Actor.objects.create(name="Palin", age=27)
response = self.client.get("%s?%s" % (reverse('admin:admin_views_actor_changelist'), IS_POPUP_VAR)) 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): def test_hide_change_password(self):
""" """
...@@ -3445,27 +3445,23 @@ action)</option> ...@@ -3445,27 +3445,23 @@ action)</option>
self.assertEqual(response.template_name, 'admin/popup_response.html') self.assertEqual(response.template_name, 'admin/popup_response.html')
def test_popup_template_escaping(self): def test_popup_template_escaping(self):
context = { popup_response_data = json.dumps({
'new_value': 'new_value\\', 'new_value': 'new_value\\',
'obj': 'obj\\', 'obj': 'obj\\',
'value': 'value\\', 'value': 'value\\',
})
context = {
'popup_response_data': popup_response_data,
} }
output = render_to_string('admin/popup_response.html', context) output = render_to_string('admin/popup_response.html', context)
self.assertIn( 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( self.assertIn(
'opener.dismissChangeRelatedObjectPopup(window, ' r'&quot;new_value\\&quot;', output
'"value\\u005C", "obj\\u005C", "new_value\\u005C");', output
) )
context['action'] = 'delete'
output = render_to_string('admin/popup_response.html', context)
self.assertIn( self.assertIn(
'opener.dismissDeleteRelatedObjectPopup(window, "value\\u005C");', output r'&quot;obj\\&quot;', output
) )
...@@ -4273,16 +4269,19 @@ class PrePopulatedTest(TestCase): ...@@ -4273,16 +4269,19 @@ class PrePopulatedTest(TestCase):
def test_prepopulated_on(self): def test_prepopulated_on(self):
response = self.client.get(reverse('admin:admin_views_prepopulatedpost_add')) response = self.client.get(reverse('admin:admin_views_prepopulatedpost_add'))
self.assertContains(response, "id: '#id_slug',") self.assertContains(response, "&quot;id&quot;: &quot;#id_slug&quot;")
self.assertContains(response, "field['dependency_ids'].push('#id_title');") self.assertContains(response, "&quot;dependency_ids&quot;: [&quot;#id_title&quot;]")
self.assertContains(response, "id: '#id_prepopulatedsubpost_set-0-subslug',") self.assertContains(response, "&quot;id&quot;: &quot;#id_prepopulatedsubpost_set-0-subslug&quot;")
def test_prepopulated_off(self): def test_prepopulated_off(self):
response = self.client.get(reverse('admin:admin_views_prepopulatedpost_change', args=(self.p1.pk,))) response = self.client.get(reverse('admin:admin_views_prepopulatedpost_change', args=(self.p1.pk,)))
self.assertContains(response, "A Long Title") self.assertContains(response, "A Long Title")
self.assertNotContains(response, "id: '#id_slug'") self.assertNotContains(response, "&quot;id&quot;: &quot;#id_slug&quot;")
self.assertNotContains(response, "field['dependency_ids'].push('#id_title');") self.assertNotContains(response, "&quot;dependency_ids&quot;: [&quot;#id_title&quot;]")
self.assertNotContains(response, "id: '#id_prepopulatedsubpost_set-0-subslug',") self.assertNotContains(
response,
"&quot;id&quot;: &quot;#id_prepopulatedsubpost_set-0-subslug&quot;"
)
@override_settings(USE_THOUSAND_SEPARATOR=True, USE_L10N=True) @override_settings(USE_THOUSAND_SEPARATOR=True, USE_L10N=True)
def test_prepopulated_maxlength_localized(self): def test_prepopulated_maxlength_localized(self):
...@@ -4291,7 +4290,7 @@ class PrePopulatedTest(TestCase): ...@@ -4291,7 +4290,7 @@ class PrePopulatedTest(TestCase):
that maxLength (in the JavaScript) is rendered without separators. that maxLength (in the JavaScript) is rendered without separators.
""" """
response = self.client.get(reverse('admin:admin_views_prepopulatedpostlargeslug_add')) 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'], @override_settings(PASSWORD_HASHERS=['django.contrib.auth.hashers.SHA1PasswordHasher'],
...@@ -4933,7 +4932,7 @@ class UserAdminTest(TestCase): ...@@ -4933,7 +4932,7 @@ class UserAdminTest(TestCase):
'_save': '1', '_save': '1',
} }
response = self.client.post(reverse('admin:auth_user_add') + '?_popup=1', data, follow=True) 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): def test_user_fk_change_popup(self):
"""User change through a FK popup should return the appropriate JavaScript response.""" """User change through a FK popup should return the appropriate JavaScript response."""
...@@ -4957,7 +4956,8 @@ class UserAdminTest(TestCase): ...@@ -4957,7 +4956,8 @@ class UserAdminTest(TestCase):
'_save': '1', '_save': '1',
} }
response = self.client.post(url, data, follow=True) 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): def test_user_fk_delete_popup(self):
"""User deletion through a FK popup should return the appropriate JavaScript response.""" """User deletion through a FK popup should return the appropriate JavaScript response."""
...@@ -4973,7 +4973,7 @@ class UserAdminTest(TestCase): ...@@ -4973,7 +4973,7 @@ class UserAdminTest(TestCase):
'_popup': '1', '_popup': '1',
} }
response = self.client.post(url, data, follow=True) 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): def test_save_add_another_button(self):
user_count = User.objects.count() user_count = User.objects.count()
......
...@@ -271,9 +271,8 @@ class FilteredSelectMultipleWidgetTest(SimpleTestCase): ...@@ -271,9 +271,8 @@ class FilteredSelectMultipleWidgetTest(SimpleTestCase):
w = widgets.FilteredSelectMultiple('test\\', False) w = widgets.FilteredSelectMultiple('test\\', False)
self.assertHTMLEqual( self.assertHTMLEqual(
w.render('test', 'test'), w.render('test', 'test'),
'<select multiple="multiple" name="test" class="selectfilter">\n</select>' '<select multiple="multiple" name="test" class="selectfilter" '
'<script type="text/javascript">addEvent(window, "load", function(e) ' 'data-field-name="test\\" data-is-stacked="0">\n</select>'
'{SelectFilter.init("id_test", "test\\u005C", 0); });</script>\n'
) )
def test_stacked_render(self): def test_stacked_render(self):
...@@ -281,9 +280,8 @@ class FilteredSelectMultipleWidgetTest(SimpleTestCase): ...@@ -281,9 +280,8 @@ class FilteredSelectMultipleWidgetTest(SimpleTestCase):
w = widgets.FilteredSelectMultiple('test\\', True) w = widgets.FilteredSelectMultiple('test\\', True)
self.assertHTMLEqual( self.assertHTMLEqual(
w.render('test', 'test'), w.render('test', 'test'),
'<select multiple="multiple" name="test" class="selectfilterstacked">\n</select>' '<select multiple="multiple" name="test" class="selectfilterstacked" '
'<script type="text/javascript">addEvent(window, "load", function(e) ' 'data-field-name="test\\" data-is-stacked="1">\n</select>'
'{SelectFilter.init("id_test", "test\\u005C", 1); });</script>\n'
) )
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment