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.
This commit is contained in:
Thomas Grainger 2015-11-23 10:46:19 +00:00 committed by Tim Graham
parent 105028eec6
commit d638cdc42a
42 changed files with 455 additions and 275 deletions

View File

@ -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"],

View File

@ -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

View File

@ -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:

View File

@ -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,

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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();

View File

@ -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);

View File

@ -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++;
}

View File

@ -0,0 +1,9 @@
(function($) {
'use strict';
$(function() {
$('.cancel-link').click(function(e) {
e.preventDefault();
window.history.back();
});
});
})(django.jQuery);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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;
}
})();

View File

@ -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);

View File

@ -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>

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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>

View File

@ -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>

View File

@ -62,8 +62,5 @@
</div>
</form>
<script type="text/javascript">
document.getElementById('id_username').focus()
</script>
</div>
{% endblock %}

View File

@ -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>

View File

@ -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>

View File

@ -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 %}

View File

@ -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>

View File

@ -19,7 +19,7 @@ from django.template.loader import get_template
from django.utils import formats
from django.utils.deprecation import RemovedInDjango20Warning
from django.utils.encoding import force_text
from django.utils.html import escapejs, format_html
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.text import capfirst
from django.utils.translation import ugettext as _
@ -254,13 +254,11 @@ def items_for_result(cl, result, form):
else:
attr = pk
value = result.serializable_value(attr)
result_id = escapejs(value)
link_or_text = format_html(
'<a href="{}"{}>{}</a>',
url,
format_html(
' onclick="opener.dismissRelatedLookupPopup(window, '
'&#39;{}&#39;); return false;"', result_id
' data-popup-opener="{}"', value
) if cl.is_popup else '',
result_repr)

View File

@ -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

View File

@ -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 = [

View File

@ -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):

View File

@ -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(

View File

@ -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
});

View File

@ -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`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -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):

View File

@ -1,5 +1,7 @@
from __future__ import unicode_literals
import json
from django.template.loader import render_to_string
from django.test import SimpleTestCase
@ -8,14 +10,16 @@ class TestTemplates(SimpleTestCase):
def test_javascript_escaping(self):
context = {
'inline_admin_formset': {
'formset': {'prefix': 'my-prefix'},
'opts': {'verbose_name': 'verbose name\\'},
'inline_formset_data': json.dumps({
'formset': {'prefix': 'my-prefix'},
'opts': {'verbose_name': 'verbose name\\'},
}),
},
}
output = render_to_string('admin/edit_inline/stacked.html', context)
self.assertIn('prefix: "my\\u002Dprefix",', output)
self.assertIn('addText: "Add another Verbose name\\u005C"', output)
self.assertIn('&quot;prefix&quot;: &quot;my-prefix&quot;', output)
self.assertIn('&quot;verbose_name&quot;: &quot;verbose name\\\\&quot;', output)
output = render_to_string('admin/edit_inline/tabular.html', context)
self.assertIn('prefix: "my\\u002Dprefix",', output)
self.assertIn('addText: "Add another Verbose name\\u005C"', output)
self.assertIn('&quot;prefix&quot;: &quot;my-prefix&quot;', output)
self.assertIn('&quot;verbose_name&quot;: &quot;verbose name\\\\&quot;', output)

View File

@ -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(

View File

@ -2,6 +2,7 @@
from __future__ import unicode_literals
import datetime
import json
import os
import re
import unittest
@ -260,8 +261,7 @@ class AdminViewBasicTest(AdminViewBasicTestCase):
'date_1': '14:55:39',
}
response = self.client.post(reverse('admin:admin_views_article_add'), post_data)
self.assertContains(response, 'dismissAddRelatedObjectPopup')
self.assertContains(response, 'title with a new\\u000Aline')
self.assertContains(response, 'title with a new\\nline')
def test_basic_edit_POST(self):
"""
@ -734,7 +734,7 @@ class AdminViewBasicTest(AdminViewBasicTestCase):
"""
actor = Actor.objects.create(name="Palin", age=27)
response = self.client.get("%s?%s" % (reverse('admin:admin_views_actor_changelist'), IS_POPUP_VAR))
self.assertContains(response, "opener.dismissRelatedLookupPopup(window, &#39;%s&#39;)" % actor.pk)
self.assertContains(response, 'data-popup-opener="%s"' % actor.pk)
def test_hide_change_password(self):
"""
@ -3445,27 +3445,23 @@ action)</option>
self.assertEqual(response.template_name, 'admin/popup_response.html')
def test_popup_template_escaping(self):
context = {
popup_response_data = json.dumps({
'new_value': 'new_value\\',
'obj': 'obj\\',
'value': 'value\\',
})
context = {
'popup_response_data': popup_response_data,
}
output = render_to_string('admin/popup_response.html', context)
self.assertIn(
'opener.dismissAddRelatedObjectPopup(window, "value\\u005C", "obj\\u005C");', output
r'&quot;value\\&quot;', output
)
context['action'] = 'change'
output = render_to_string('admin/popup_response.html', context)
self.assertIn(
'opener.dismissChangeRelatedObjectPopup(window, '
'"value\\u005C", "obj\\u005C", "new_value\\u005C");', output
r'&quot;new_value\\&quot;', output
)
context['action'] = 'delete'
output = render_to_string('admin/popup_response.html', context)
self.assertIn(
'opener.dismissDeleteRelatedObjectPopup(window, "value\\u005C");', output
r'&quot;obj\\&quot;', output
)
@ -4273,16 +4269,19 @@ class PrePopulatedTest(TestCase):
def test_prepopulated_on(self):
response = self.client.get(reverse('admin:admin_views_prepopulatedpost_add'))
self.assertContains(response, "id: '#id_slug',")
self.assertContains(response, "field['dependency_ids'].push('#id_title');")
self.assertContains(response, "id: '#id_prepopulatedsubpost_set-0-subslug',")
self.assertContains(response, "&quot;id&quot;: &quot;#id_slug&quot;")
self.assertContains(response, "&quot;dependency_ids&quot;: [&quot;#id_title&quot;]")
self.assertContains(response, "&quot;id&quot;: &quot;#id_prepopulatedsubpost_set-0-subslug&quot;")
def test_prepopulated_off(self):
response = self.client.get(reverse('admin:admin_views_prepopulatedpost_change', args=(self.p1.pk,)))
self.assertContains(response, "A Long Title")
self.assertNotContains(response, "id: '#id_slug'")
self.assertNotContains(response, "field['dependency_ids'].push('#id_title');")
self.assertNotContains(response, "id: '#id_prepopulatedsubpost_set-0-subslug',")
self.assertNotContains(response, "&quot;id&quot;: &quot;#id_slug&quot;")
self.assertNotContains(response, "&quot;dependency_ids&quot;: [&quot;#id_title&quot;]")
self.assertNotContains(
response,
"&quot;id&quot;: &quot;#id_prepopulatedsubpost_set-0-subslug&quot;"
)
@override_settings(USE_THOUSAND_SEPARATOR=True, USE_L10N=True)
def test_prepopulated_maxlength_localized(self):
@ -4291,7 +4290,7 @@ class PrePopulatedTest(TestCase):
that maxLength (in the JavaScript) is rendered without separators.
"""
response = self.client.get(reverse('admin:admin_views_prepopulatedpostlargeslug_add'))
self.assertContains(response, "maxLength: 1000") # instead of 1,000
self.assertContains(response, "&quot;maxLength&quot;: 1000") # instead of 1,000
@override_settings(PASSWORD_HASHERS=['django.contrib.auth.hashers.SHA1PasswordHasher'],
@ -4933,7 +4932,7 @@ class UserAdminTest(TestCase):
'_save': '1',
}
response = self.client.post(reverse('admin:auth_user_add') + '?_popup=1', data, follow=True)
self.assertContains(response, 'dismissAddRelatedObjectPopup')
self.assertContains(response, '&quot;obj&quot;: &quot;newuser&quot;')
def test_user_fk_change_popup(self):
"""User change through a FK popup should return the appropriate JavaScript response."""
@ -4957,7 +4956,8 @@ class UserAdminTest(TestCase):
'_save': '1',
}
response = self.client.post(url, data, follow=True)
self.assertContains(response, 'dismissChangeRelatedObjectPopup')
self.assertContains(response, '&quot;obj&quot;: &quot;newuser&quot;')
self.assertContains(response, '&quot;action&quot;: &quot;change&quot;')
def test_user_fk_delete_popup(self):
"""User deletion through a FK popup should return the appropriate JavaScript response."""
@ -4973,7 +4973,7 @@ class UserAdminTest(TestCase):
'_popup': '1',
}
response = self.client.post(url, data, follow=True)
self.assertContains(response, 'dismissDeleteRelatedObjectPopup')
self.assertContains(response, '&quot;action&quot;: &quot;delete&quot;')
def test_save_add_another_button(self):
user_count = User.objects.count()

View File

@ -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>'
)