Ticket #13614: 13614.filterselect-bug.4.diff
File 13614.filterselect-bug.4.diff, 41.0 KB (added by , 13 years ago) |
---|
-
django/contrib/admin/options.py
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index f5f6256..8fadd88 100644
a b class ModelAdmin(BaseModelAdmin): 1013 1013 @transaction.commit_on_success 1014 1014 def change_view(self, request, object_id, form_url='', extra_context=None): 1015 1015 "The 'change' admin view for this model." 1016 1016 1017 model = self.model 1017 1018 opts = model._meta 1018 1019 … … class InlineModelAdmin(BaseModelAdmin): 1378 1379 if self.prepopulated_fields: 1379 1380 js.extend(['urlify.js', 'prepopulate%s.js' % extra]) 1380 1381 if self.filter_vertical or self.filter_horizontal: 1381 js.extend([' SelectBox.js', 'SelectFilter2.js'])1382 js.extend(['selectfilter.js']) 1382 1383 return forms.Media(js=[static('admin/js/%s' % url) for url in js]) 1383 1384 1384 1385 def get_formset(self, request, obj=None, **kwargs): -
deleted file django/contrib/admin/static/admin/js/SelectFilter2.js
diff --git a/django/contrib/admin/static/admin/js/SelectFilter2.js b/django/contrib/admin/static/admin/js/SelectFilter2.js deleted file mode 100644 index 0accd08..0000000
+ - 1 /*2 SelectFilter2 - Turns a multiple-select box into a filter interface.3 4 Requires core.js, SelectBox.js and addevent.js.5 */6 (function($) {7 function findForm(node) {8 // returns the node of the form containing the given node9 if (node.tagName.toLowerCase() != 'form') {10 return findForm(node.parentNode);11 }12 return node;13 }14 15 window.SelectFilter = {16 init: function(field_id, field_name, is_stacked, admin_media_prefix) {17 if (field_id.match(/__prefix__/)){18 // Don't intialize on empty forms.19 return;20 }21 var from_box = document.getElementById(field_id);22 from_box.id += '_from'; // change its ID23 from_box.className = 'filtered';24 25 var ps = from_box.parentNode.getElementsByTagName('p');26 for (var i=0; i<ps.length; i++) {27 if (ps[i].className.indexOf("info") != -1) {28 // Remove <p class="info">, because it just gets in the way.29 from_box.parentNode.removeChild(ps[i]);30 } else if (ps[i].className.indexOf("help") != -1) {31 // Move help text up to the top so it isn't below the select32 // boxes or wrapped off on the side to the right of the add33 // button:34 from_box.parentNode.insertBefore(ps[i], from_box.parentNode.firstChild);35 }36 }37 38 // <div class="selector"> or <div class="selector stacked">39 var selector_div = quickElement('div', from_box.parentNode);40 selector_div.className = is_stacked ? 'selector stacked' : 'selector';41 42 // <div class="selector-available">43 var selector_available = quickElement('div', selector_div, '');44 selector_available.className = 'selector-available';45 var title_available = quickElement('h2', selector_available, interpolate(gettext('Available %s') + ' ', [field_name]));46 quickElement('img', title_available, '', 'src', admin_media_prefix + 'img/icon-unknown.gif', 'width', '10', 'height', '10', 'class', 'help help-tooltip', 'title', interpolate(gettext('This is the list of available %s. You may choose some by selecting them in the box below and then clicking the "Choose" arrow between the two boxes.'), [field_name]));47 48 var filter_p = quickElement('p', selector_available, '', 'id', field_id + '_filter');49 filter_p.className = 'selector-filter';50 51 var search_filter_label = quickElement('label', filter_p, '', 'for', field_id + "_input");52 53 var search_selector_img = quickElement('img', search_filter_label, '', 'src', admin_media_prefix + 'img/selector-search.gif', 'class', 'help-tooltip', 'alt', '', 'title', interpolate(gettext("Type into this box to filter down the list of available %s."), [field_name]));54 55 filter_p.appendChild(document.createTextNode(' '));56 57 var filter_input = quickElement('input', filter_p, '', 'type', 'text', 'placeholder', gettext("Filter"));58 filter_input.id = field_id + '_input';59 60 selector_available.appendChild(from_box);61 var choose_all = quickElement('a', selector_available, gettext('Choose all'), 'title', interpolate(gettext('Click to choose all %s at once.'), [field_name]), 'href', 'javascript: (function(){ SelectBox.move_all("' + field_id + '_from", "' + field_id + '_to"); SelectFilter.refresh_icons("' + field_id + '");})()', 'id', field_id + '_add_all_link');62 choose_all.className = 'selector-chooseall';63 64 // <ul class="selector-chooser">65 var selector_chooser = quickElement('ul', selector_div, '');66 selector_chooser.className = 'selector-chooser';67 var add_link = quickElement('a', quickElement('li', selector_chooser, ''), gettext('Choose'), 'title', gettext('Choose'), 'href', 'javascript: (function(){ SelectBox.move("' + field_id + '_from","' + field_id + '_to"); SelectFilter.refresh_icons("' + field_id + '");})()', 'id', field_id + '_add_link');68 add_link.className = 'selector-add';69 var remove_link = quickElement('a', quickElement('li', selector_chooser, ''), gettext('Remove'), 'title', gettext('Remove'), 'href', 'javascript: (function(){ SelectBox.move("' + field_id + '_to","' + field_id + '_from"); SelectFilter.refresh_icons("' + field_id + '");})()', 'id', field_id + '_remove_link');70 remove_link.className = 'selector-remove';71 72 // <div class="selector-chosen">73 var selector_chosen = quickElement('div', selector_div, '');74 selector_chosen.className = 'selector-chosen';75 var title_chosen = quickElement('h2', selector_chosen, interpolate(gettext('Chosen %s') + ' ', [field_name]));76 quickElement('img', title_chosen, '', 'src', admin_media_prefix + 'img/icon-unknown.gif', 'width', '10', 'height', '10', 'class', 'help help-tooltip', 'title', interpolate(gettext('This is the list of chosen %s. You may remove some by selecting them in the box below and then clicking the "Remove" arrow between the two boxes.'), [field_name]));77 78 var to_box = quickElement('select', selector_chosen, '', 'id', field_id + '_to', 'multiple', 'multiple', 'size', from_box.size, 'name', from_box.getAttribute('name'));79 to_box.className = 'filtered';80 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: (function() { SelectBox.move_all("' + field_id + '_to", "' + field_id + '_from"); SelectFilter.refresh_icons("' + field_id + '");})()', 'id', field_id + '_remove_all_link');81 clear_all.className = 'selector-clearall';82 83 from_box.setAttribute('name', from_box.getAttribute('name') + '_old');84 85 // Set up the JavaScript event handlers for the select box filter interface86 addEvent(filter_input, 'keyup', function(e) { SelectFilter.filter_key_up(e, field_id); });87 addEvent(filter_input, 'keydown', function(e) { SelectFilter.filter_key_down(e, field_id); });88 addEvent(from_box, 'change', function(e) { SelectFilter.refresh_icons(field_id) });89 addEvent(to_box, 'change', function(e) { SelectFilter.refresh_icons(field_id) });90 addEvent(from_box, 'dblclick', function() { SelectBox.move(field_id + '_from', field_id + '_to'); SelectFilter.refresh_icons(field_id); });91 addEvent(to_box, 'dblclick', function() { SelectBox.move(field_id + '_to', field_id + '_from'); SelectFilter.refresh_icons(field_id); });92 addEvent(findForm(from_box), 'submit', function() { SelectBox.select_all(field_id + '_to'); });93 SelectBox.init(field_id + '_from');94 SelectBox.init(field_id + '_to');95 // Move selected from_box options to to_box96 SelectBox.move(field_id + '_from', field_id + '_to');97 98 if (!is_stacked) {99 // In horizontal mode, give the same height to the two boxes.100 var j_from_box = $(from_box);101 var j_to_box = $(to_box);102 var resize_filters = function() { j_to_box.height($(filter_p).outerHeight() + j_from_box.outerHeight()); }103 if (j_from_box.outerHeight() > 0) {104 resize_filters(); // This fieldset is already open. Resize now.105 } else {106 // This fieldset is probably collapsed. Wait for its 'show' event.107 j_to_box.closest('fieldset').one('show.fieldset', resize_filters);108 }109 }110 111 // Initial icon refresh112 SelectFilter.refresh_icons(field_id);113 },114 refresh_icons: function(field_id) {115 var from = $('#' + field_id + '_from');116 var to = $('#' + field_id + '_to');117 var is_from_selected = from.find('option:selected').length > 0;118 var is_to_selected = to.find('option:selected').length > 0;119 // Active if at least one item is selected120 $('#' + field_id + '_add_link').toggleClass('active', is_from_selected);121 $('#' + field_id + '_remove_link').toggleClass('active', is_to_selected);122 // Active if the corresponding box isn't empty123 $('#' + field_id + '_add_all_link').toggleClass('active', from.find('option').length > 0);124 $('#' + field_id + '_remove_all_link').toggleClass('active', to.find('option').length > 0);125 },126 filter_key_up: function(event, field_id) {127 var from = document.getElementById(field_id + '_from');128 // don't submit form if user pressed Enter129 if ((event.which && event.which == 13) || (event.keyCode && event.keyCode == 13)) {130 from.selectedIndex = 0;131 SelectBox.move(field_id + '_from', field_id + '_to');132 from.selectedIndex = 0;133 return false;134 }135 var temp = from.selectedIndex;136 SelectBox.filter(field_id + '_from', document.getElementById(field_id + '_input').value);137 from.selectedIndex = temp;138 return true;139 },140 filter_key_down: function(event, field_id) {141 var from = document.getElementById(field_id + '_from');142 // right arrow -- move across143 if ((event.which && event.which == 39) || (event.keyCode && event.keyCode == 39)) {144 var old_index = from.selectedIndex;145 SelectBox.move(field_id + '_from', field_id + '_to');146 from.selectedIndex = (old_index == from.length) ? from.length - 1 : old_index;147 return false;148 }149 // down arrow -- wrap around150 if ((event.which && event.which == 40) || (event.keyCode && event.keyCode == 40)) {151 from.selectedIndex = (from.length == from.selectedIndex + 1) ? 0 : from.selectedIndex + 1;152 }153 // up arrow -- wrap around154 if ((event.which && event.which == 38) || (event.keyCode && event.keyCode == 38)) {155 from.selectedIndex = (from.selectedIndex == 0) ? from.length - 1 : from.selectedIndex - 1;156 }157 return true;158 }159 }160 161 })(django.jQuery); -
new file django/contrib/admin/static/admin/js/selectfilter.js
diff --git a/django/contrib/admin/static/admin/js/selectfilter.js b/django/contrib/admin/static/admin/js/selectfilter.js new file mode 100644 index 0000000..7b7d74a
- + 1 (function($) { 2 var settings = { 3 'name': '', 4 'verboseName': '', 5 'stacked': 0, 6 'adminStaticPrefix': '', 7 refresh_icons: function(selector) { 8 var available = selector.data('available'); 9 var chosen = selector.data('chosen'); 10 var is_from_selected = available.children().filter(':selected').length > 0; 11 var is_to_selected = chosen.children().filter(':selected').length > 0; 12 // Active if at least one item is selected 13 selector.find('.selector-add').toggleClass('active', is_from_selected); 14 selector.find('.selector-remove').toggleClass('active', is_to_selected); 15 // Active if the corresponding box isn't empty 16 selector.find('.selector-chooseall').toggleClass('active', available.find('option').length > 0); 17 selector.find('.selector-clearall').toggleClass('active', chosen.find('option').length > 0); 18 }, 19 refresh_state: function(selector) { 20 settings.refresh_icons(selector); 21 var available = selector.data('available'); 22 var chosen = selector.data('chosen'); 23 24 // Re-generate the content of the actual, hidden, box. 25 var actualBox = selector.data('actualBox'); 26 var actualOptions = actualBox.children(); 27 28 // De-select all options. 29 available.children().each(function(){ 30 var option = actualOptions[$(this).attr('data-index')]; 31 option.selected = false; 32 }); 33 34 // Select all the same options as those from the 'to' box. 35 chosen.children().each(function(){ 36 var option = actualOptions[$(this).attr('data-index')]; 37 option.selected = true; 38 }); 39 }, 40 move: function(from, to, all) { 41 var selector = from.data('selector'); 42 var available = selector.data('available'); 43 44 var options; 45 if (all) { 46 options = from.children(); 47 } 48 else { 49 options = from.children().filter(':selected'); 50 } 51 52 if (from == available) { 53 options.each(function() { 54 var moved_option = $(this); 55 available.data('cache').each(function(index, option) { 56 var cached_option = $(option); 57 if (moved_option.val() == cached_option.val()) { 58 available.data('cache').splice(index, 1); 59 } 60 }); 61 }); 62 } 63 else { 64 options.each(function() { 65 available.data('cache').push($(this).clone()[0]); 66 }); 67 } 68 69 to.append(options); 70 to.children().removeAttr('selected'); 71 settings.refresh_state(from.data('selector')); 72 } 73 }; 74 75 var methods = { 76 init: function(options) { 77 $.extend(settings, options); 78 79 var actualBox = this; 80 var stack_class = settings.stacked ? 'selector stacked' : 'selector'; 81 var selector = $('<div>').addClass(stack_class) 82 .attr('id', actualBox.attr('id') + '_selector') 83 .appendTo(actualBox.parent()); 84 85 actualBox.parent().find('p').each(function(){ 86 if ($(this).hasClass('info')) { 87 // Remove <p class="info">, because it just gets in the way. 88 $(this).remove(); 89 } 90 else if ($(this).hasClass('help')) { 91 // Move help text up to the top so it isn't below the select 92 // boxes or wrapped off on the side to the right of the add 93 // button. 94 actualBox.parent().prepend($(this)); 95 } 96 }); 97 98 var available = $('<select>').addClass('filtered').attr({ 99 'id': 'id_' + settings.name + '_from', 100 'multiple': 'multiple', 101 'name': settings.name + '_from' 102 }); 103 104 actualBox.children().each(function(index, original){ 105 var copy = $('<option>').attr({ 106 'value': $(original).val(), 107 'data-index': index 108 }); 109 copy.text($(original).text()); 110 copy[0].selected = original.selected; 111 available.append(copy); 112 }); 113 actualBox.hide(); 114 available.insertBefore(actualBox); 115 available.show(); 116 117 var search = $('<input>').attr({ 118 'id': 'id_' + settings.name + '_input', 119 'type': 'text' 120 }); 121 122 var searchContainer = $('<p>').addClass('selector-filter') 123 .append( 124 $('<label>').attr('for', search.attr('id')).css({'width': "16px", 'padding': "2px"}) 125 .append( 126 $('<img>').attr('src', settings.adminStaticPrefix + 'img/selector-search.gif') 127 ) 128 ) 129 .append(search); 130 131 var availableHeader = $('<h2>').text(gettext('Available ') + settings.verboseName) 132 .append(' ') 133 .append( 134 $('<img>').attr({ 135 src: settings.adminStaticPrefix + 'img/icon-unknown.gif', 136 width: 10, 137 height: 10, 138 title: interpolate(gettext('This is the list of available %s. You may choose some by selecting them in the box below and then clicking the "Choose" arrow between the two boxes.'), [settings.verboseName]) 139 }).addClass('help help-tooltip') 140 ); 141 var chosenHeader = $('<h2>').text(gettext('Chosen ') + settings.verboseName) 142 .append(' ') 143 .append( 144 $('<img>').attr({ 145 src: settings.adminStaticPrefix + 'img/icon-unknown.gif', 146 width: 10, 147 height: 10, 148 title: interpolate(gettext('This is the list of chosen %s. You may remove some by selecting them in the box below and then clicking the "Remove" arrow between the two boxes.'), [settings.verboseName]) 149 }).addClass('help help-tooltip') 150 ); 151 152 $('<div>').addClass('selector-available') 153 .append(availableHeader) 154 .append(searchContainer) 155 .append(available) 156 .append($('<a>').addClass('selector-chooseall').text(gettext('Choose all')).attr('href', '#')) 157 .appendTo(selector); 158 159 $('<ul>').addClass('selector-chooser') 160 .append($('<li>') 161 .append($('<a>').addClass('selector-add').text(gettext('Choose')).attr('title', gettext('Choose'))) 162 ) 163 .append($('<li>') 164 .append($('<a>').addClass('selector-remove').text(gettext('Remove')).attr('title', gettext('Remove'))) 165 ) 166 .appendTo(selector); 167 168 var chosen = $('<select>').addClass('filtered').attr({ 169 'id': 'id_' + settings.name + '_to', 170 'multiple': 'multiple', 171 'name': settings.name + '_to' 172 }); 173 174 $('<div>').addClass('selector-chosen') 175 .append(chosenHeader) 176 .append(chosen) 177 .append( 178 $('<a>').addClass('selector-clearall') 179 .text(gettext('Clear all')) 180 .attr('href', '#') 181 ).appendTo(selector); 182 183 // Cross-referencing 184 available.data('selector', selector); 185 chosen.data('selector', selector); 186 selector.data('available', available); 187 selector.data('chosen', chosen); 188 selector.data('actualBox', actualBox); 189 190 // Initialize the filter cache 191 available.data('cache', available.children()); 192 193 // If this is a saved instance, move the already selected options across 194 // to the chosen list. 195 settings.move(available, chosen); 196 197 // Resizing 198 if (!settings.stacked) { 199 // In horizontal mode, give the same height to the two boxes. 200 var resize_filters = function() { 201 chosen.height($(searchContainer).outerHeight() + available.outerHeight()); 202 }; 203 if (available.outerHeight() > 0) { 204 resize_filters(); // This fieldset is already open. Resize now. 205 } else { 206 // This fieldset is probably collapsed. Wait for its 'show' event. 207 chosen.closest('fieldset').one('show.fieldset', resize_filters); 208 } 209 } 210 211 // Hook up selection events. 212 $(search).keyup(function(event) { 213 if ((event.which && event.which == 13) || (event.keyCode && event.keyCode == 13)) { 214 settings.move(available, chosen); 215 return false; 216 } 217 available.children().removeAttr('selected'); 218 var text = $(this).val(); 219 if ($.trim(text)) { 220 var tokens = text.toLowerCase().split(/\s+/); 221 var token; 222 available.empty(); 223 available.data('cache').each(function() { 224 // Redisplay the HTML select box, displaying only the choices 225 // containing ALL the words in text. (It's an AND search.) 226 for (var j = 0; (token = tokens[j]); j++) { 227 if (this.text.toLowerCase().indexOf(token) != -1) { 228 available.append($(this).clone()); 229 } 230 } 231 }); 232 } 233 else { 234 available.html(available.data('cache').clone()); 235 } 236 return true; 237 }); 238 239 selector.find('.selector-chooseall').click(function() { 240 settings.move(available, chosen, true); 241 return false; 242 }); 243 244 selector.find('.selector-clearall').click(function() { 245 settings.move(chosen, available, true); 246 return false; 247 }); 248 249 selector.find('.selector-add').click(function() { 250 settings.move(available, chosen); 251 return false; 252 }); 253 254 selector.find('.selector-remove').click(function() { 255 settings.move(chosen, available); 256 return false; 257 }); 258 259 available.dblclick(function() { 260 settings.move(available, chosen); 261 }); 262 263 available.change(function() { 264 settings.refresh_icons(selector); 265 }); 266 267 chosen.change(function() { 268 settings.refresh_icons(selector); 269 }); 270 271 chosen.dblclick(function() { 272 settings.move(chosen, available); 273 }); 274 275 available.keydown(function(event) { 276 event.which = event.which ? event.which : event.keyCode; 277 switch(event.which) { 278 case 13: 279 // Enter pressed - don't submit the form but move the current selection. 280 settings.move(available, chosen); 281 this.selectedIndex = (this.selectedIndex == this.length) ? this.length - 1 : this.selectedIndex; 282 return false; 283 case 39: 284 // Right arrow - move across (only when horizontal) 285 if (!settings.stacked) { 286 settings.move(available, chosen); 287 this.selectedIndex = (this.selectedIndex == this.length) ? this.length - 1 : this.selectedIndex; 288 return false; 289 } 290 } 291 return true; 292 }); 293 294 chosen.keydown(function(event) { 295 event.which = event.which ? event.which : event.keyCode; 296 switch(event.which) { 297 case 13: 298 // Enter pressed - don't submit the form but move the current selection. 299 settings.move(chosen, available); 300 this.selectedIndex = (this.selectedIndex == this.length) ? this.length - 1 : this.selectedIndex; 301 return false; 302 case 37: 303 // Left arrow - move across (only when horizontal) 304 if (!settings.stacked) { 305 settings.move(chosen, available); 306 this.selectedIndex = (this.selectedIndex == this.length) ? this.length - 1 : this.selectedIndex; 307 return false; 308 } 309 } 310 return true; 311 }); 312 } 313 }; 314 315 $.fn.selectFilter = function(method) { 316 if (methods[method]) { 317 return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); 318 } else if (typeof method === 'object' || !method ) { 319 return methods.init.apply(this, arguments); 320 } else { 321 $.error('Method ' + method + ' does not exist on jQuery.selectFilter'); 322 } 323 }; 324 })(django.jQuery); -
django/contrib/admin/widgets.py
diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py index 29958b2..dc8ae2c 100644
a b class FilteredSelectMultiple(forms.SelectMultiple): 24 24 """ 25 25 @property 26 26 def media(self): 27 js = [" core.js", "SelectBox.js", "SelectFilter2.js"]27 js = ["SelectBox.js", "selectfilter.js"] 28 28 return forms.Media(js=[static("admin/js/%s" % path) for path in js]) 29 29 30 30 def __init__(self, verbose_name, is_stacked, attrs=None, choices=()): … … class FilteredSelectMultiple(forms.SelectMultiple): 35 35 def render(self, name, value, attrs=None, choices=()): 36 36 if attrs is None: 37 37 attrs = {} 38 attrs['class'] = 'selectfilter'39 38 if self.is_stacked: 40 attrs['class'] += 'stacked' 39 attrs['class'] = 'stacked' 40 id_ = attrs.get('id', 'id_%s' % name) 41 41 output = [super(FilteredSelectMultiple, self).render(name, value, attrs, choices)] 42 output.append(u'<script type="text/javascript">addEvent(window, "load", function(e) {') 43 # TODO: "id_" is hard-coded here. This should instead use the correct 44 # API to determine the ID dynamically. 45 output.append(u'SelectFilter.init("id_%s", "%s", %s, "%s"); });</script>\n' 46 % (name, self.verbose_name.replace('"', '\\"'), int(self.is_stacked), static('admin/'))) 42 output.append(u''' 43 <script type="text/javascript"> 44 (function($) { 45 $(document).ready(function() { 46 $("#%(id)s").selectFilter({ 47 "name": "%(name)s", 48 "verboseName": "%(verbose_name)s", 49 "stacked": %(is_stacked)s, 50 "adminStaticPrefix": "%(admin_static_prefix)s" 51 }); 52 }); 53 })(django.jQuery); 54 </script>''' % { 55 'id': id_, 56 'name': name, 57 'verbose_name': self.verbose_name.replace('"', '\\"'), 58 'is_stacked': int(self.is_stacked), 59 'admin_static_prefix': static('admin/') 60 }) 61 47 62 return mark_safe(u''.join(output)) 48 63 49 64 class AdminDateWidget(forms.DateInput): … … class ForeignKeyRawIdWidget(forms.TextInput): 153 168 url = u'' 154 169 if "class" not in attrs: 155 170 attrs['class'] = 'vForeignKeyRawIdAdminField' # The JavaScript code looks for this hook. 156 # TODO: "lookup_id_" is hard-coded here. This should instead use 157 # the correct API to determine the ID dynamically. 158 extra.append(u'<a href="%s%s" class="related-lookup" id="lookup_id_%s" onclick="return showRelatedObjectLookupPopup(this);"> ' 159 % (related_url, url, name)) 171 id_ = attrs.get('id', 'id_%s' % name) 172 extra.append(u'<a href="%s%s" class="related-lookup" id="lookup_%s" onclick="return showRelatedObjectLookupPopup(this);"> ' % \ 173 (related_url, url, id_)) 160 174 extra.append(u'<img src="%s" width="16" height="16" alt="%s" /></a>' 161 175 % (static('admin/img/selector-search.gif'), _('Lookup'))) 162 176 output = [super(ForeignKeyRawIdWidget, self).render(name, value, attrs)] + extra … … class RelatedFieldWidgetWrapper(forms.Widget): 259 273 output = [self.widget.render(name, value, *args, **kwargs)] 260 274 if self.can_add_related: 261 275 related_url = reverse('admin:%s_%s_add' % info, current_app=self.admin_site.name) 262 # TODO: "add_id_" is hard-coded here. This should instead use the263 # correct API to determine the ID dynamically.264 output.append(u'<a href="%s" class="add-another" id="add_id_%s" onclick="return showAddAnotherPopup(this);"> '265 % (related_url, name))276 id = self.attrs.get('id', 'id_%s' % name) 277 output.append( 278 u'<a href="%s" class="add-another" id="add_%s" onclick="return showAddAnotherPopup(this);"> ' 279 % (related_url, id)) 266 280 output.append(u'<img src="%s" width="10" height="10" alt="%s"/></a>' 267 281 % (static('admin/img/icon_addlink.gif'), _('Add Another'))) 268 282 return mark_safe(u''.join(output)) -
tests/regressiontests/admin_widgets/tests.py
diff --git a/tests/regressiontests/admin_widgets/tests.py b/tests/regressiontests/admin_widgets/tests.py index 94ee5a9..f3d6e0c 100644
a b class FilteredSelectMultipleWidgetTest(DjangoTestCase): 196 196 w = widgets.FilteredSelectMultiple('test', False) 197 197 self.assertHTMLEqual( 198 198 conditional_escape(w.render('test', 'test')), 199 '<select multiple="multiple" name="test" class="selectfilter">\n</select><script type="text/javascript">addEvent(window, "load", function(e) {SelectFilter.init("id_test", "test", 0, "%(ADMIN_MEDIA_PREFIX)s"); });</script>\n' % admin_media_prefix()199 '<select multiple="multiple" name="test">\n</select><script type="text/javascript">(function($) { $(document).ready(function() { $("#id_test").selectFilter({ "name": "test", "verboseName": "test", "stacked": 0, "adminStaticPrefix": "%(ADMIN_MEDIA_PREFIX)s" }); }); })(django.jQuery);</script>\n' % admin_media_prefix() 200 200 ) 201 201 202 202 def test_stacked_render(self): 203 203 w = widgets.FilteredSelectMultiple('test', True) 204 204 self.assertHTMLEqual( 205 205 conditional_escape(w.render('test', 'test')), 206 '<select multiple="multiple" name="test" class="s electfilterstacked">\n</select><script type="text/javascript">addEvent(window, "load", function(e) {SelectFilter.init("id_test", "test", 1, "%(ADMIN_MEDIA_PREFIX)s"); });</script>\n' % admin_media_prefix()206 '<select multiple="multiple" name="test" class="stacked">\n</select><script type="text/javascript">(function($) { $(document).ready(function() { $("#id_test").selectFilter({ "name": "test", "verboseName": "test", "stacked": 1, "adminStaticPrefix": "%(ADMIN_MEDIA_PREFIX)s" }); }); })(django.jQuery);</script>\n' % admin_media_prefix() 207 207 ) 208 208 209 209 class AdminDateWidgetTest(DjangoTestCase): … … class HorizontalVerticalFilterSeleniumFirefoxTests(AdminSeleniumWebDriverTestCas 478 478 self.cliff = models.Student.objects.create(name='Cliff') 479 479 self.arthur = models.Student.objects.create(name='Arthur') 480 480 self.school = models.School.objects.create(name='School of Awesome') 481 self.school.students = [self.lisa, self.peter] 482 self.school.alumni = [self.lisa, self.peter] 483 self.school.save() 481 484 super(HorizontalVerticalFilterSeleniumFirefoxTests, self).setUp() 482 485 483 486 def assertActiveButtons(self, mode, field_name, choose, remove, 484 487 choose_all=None, remove_all=None): 485 choose_link = '#id_%s_ add_link' % field_name486 choose_all_link = '#id_%s_ add_all_link' % field_name487 remove_link = '#id_%s_ remove_link' % field_name488 remove_all_link = '#id_%s_ remove_all_link' % field_name488 choose_link = '#id_%s_selector .selector-add' % field_name 489 choose_all_link = '#id_%s_selector .selector-chooseall' % field_name 490 remove_link = '#id_%s_selector .selector-remove' % field_name 491 remove_all_link = '#id_%s_selector .selector-clearall' % field_name 489 492 self.assertEqual(self.has_css_class(choose_link, 'active'), choose) 490 493 self.assertEqual(self.has_css_class(remove_link, 'active'), remove) 491 494 if mode == 'horizontal': … … class HorizontalVerticalFilterSeleniumFirefoxTests(AdminSeleniumWebDriverTestCas 495 498 def execute_basic_operations(self, mode, field_name): 496 499 from_box = '#id_%s_from' % field_name 497 500 to_box = '#id_%s_to' % field_name 498 choose_link = ' id_%s_add_link' % field_name499 choose_all_link = ' id_%s_add_all_link' % field_name500 remove_link = ' id_%s_remove_link' % field_name501 remove_all_link = ' id_%s_remove_all_link' % field_name501 choose_link = '#id_%s_selector .selector-add' % field_name 502 choose_all_link = '#id_%s_selector .selector-chooseall' % field_name 503 remove_link = '#id_%s_selector .selector-remove' % field_name 504 remove_all_link = '#id_%s_selector .selector-clearall' % field_name 502 505 503 506 # Initial positions --------------------------------------------------- 504 507 self.assertSelectOptions(from_box, … … class HorizontalVerticalFilterSeleniumFirefoxTests(AdminSeleniumWebDriverTestCas 511 514 512 515 # Click 'Choose all' -------------------------------------------------- 513 516 if mode == 'horizontal': 514 self.selenium.find_element_by_ id(choose_all_link).click()517 self.selenium.find_element_by_css_selector(choose_all_link).click() 515 518 elif mode == 'vertical': 516 519 # There 's no 'Choose all' button in vertical mode, so individually 517 520 # select all options and click 'Choose'. 518 521 for option in self.selenium.find_elements_by_css_selector(from_box + ' option'): 519 522 option.click() 520 self.selenium.find_element_by_ id(choose_link).click()523 self.selenium.find_element_by_css_selector(choose_link).click() 521 524 self.assertSelectOptions(from_box, []) 522 525 self.assertSelectOptions(to_box, 523 526 [str(self.lisa.id), str(self.peter.id), … … class HorizontalVerticalFilterSeleniumFirefoxTests(AdminSeleniumWebDriverTestCas 528 531 529 532 # Click 'Remove all' -------------------------------------------------- 530 533 if mode == 'horizontal': 531 self.selenium.find_element_by_ id(remove_all_link).click()534 self.selenium.find_element_by_css_selector(remove_all_link).click() 532 535 elif mode == 'vertical': 533 536 # There 's no 'Remove all' button in vertical mode, so individually 534 537 # select all options and click 'Remove'. 535 538 for option in self.selenium.find_elements_by_css_selector(to_box + ' option'): 536 539 option.click() 537 self.selenium.find_element_by_ id(remove_link).click()540 self.selenium.find_element_by_css_selector(remove_link).click() 538 541 self.assertSelectOptions(from_box, 539 542 [str(self.lisa.id), str(self.peter.id), 540 543 str(self.arthur.id), str(self.bob.id), … … class HorizontalVerticalFilterSeleniumFirefoxTests(AdminSeleniumWebDriverTestCas 549 552 self.get_select_option(from_box, str(self.bob.id)).click() 550 553 self.get_select_option(from_box, str(self.john.id)).click() 551 554 self.assertActiveButtons(mode, field_name, True, False, True, False) 552 self.selenium.find_element_by_ id(choose_link).click()555 self.selenium.find_element_by_css_selector(choose_link).click() 553 556 self.assertActiveButtons(mode, field_name, False, False, True, True) 554 557 555 558 self.assertSelectOptions(from_box, … … class HorizontalVerticalFilterSeleniumFirefoxTests(AdminSeleniumWebDriverTestCas 563 566 self.get_select_option(to_box, str(self.lisa.id)).click() 564 567 self.get_select_option(to_box, str(self.bob.id)).click() 565 568 self.assertActiveButtons(mode, field_name, False, True, True, True) 566 self.selenium.find_element_by_ id(remove_link).click()569 self.selenium.find_element_by_css_selector(remove_link).click() 567 570 self.assertActiveButtons(mode, field_name, False, False, True, True) 568 571 569 572 self.assertSelectOptions(from_box, … … class HorizontalVerticalFilterSeleniumFirefoxTests(AdminSeleniumWebDriverTestCas 576 579 # Choose some more options -------------------------------------------- 577 580 self.get_select_option(from_box, str(self.arthur.id)).click() 578 581 self.get_select_option(from_box, str(self.cliff.id)).click() 579 self.selenium.find_element_by_ id(choose_link).click()582 self.selenium.find_element_by_css_selector(choose_link).click() 580 583 581 584 self.assertSelectOptions(from_box, 582 585 [str(self.peter.id), str(self.jenny.id), … … class HorizontalVerticalFilterSeleniumFirefoxTests(AdminSeleniumWebDriverTestCas 586 589 str(self.arthur.id), str(self.cliff.id)]) 587 590 588 591 def test_basic(self): 589 self.school.students = [self.lisa, self.peter]590 self.school.alumni = [self.lisa, self.peter]591 self.school.save()592 593 592 self.admin_login(username='super', password='secret', login_url='/') 594 593 self.selenium.get( 595 594 '%s%s' % (self.live_server_url, '/admin_widgets/school/%s/' % self.school.id)) … … class HorizontalVerticalFilterSeleniumFirefoxTests(AdminSeleniumWebDriverTestCas 605 604 self.assertEqual(list(self.school.alumni.all()), 606 605 [self.arthur, self.cliff, self.jason, self.john]) 607 606 607 def test_back_button_bug_13614(self): 608 self.admin_login(username='super', password='secret', login_url='/') 609 self.selenium.get( 610 '%s%s' % (self.live_server_url, '/admin_widgets/school/%s/' % self.school.id)) 611 612 self.execute_basic_operations('vertical', 'students') 613 self.execute_basic_operations('horizontal', 'alumni') 614 615 # Navigate away and go back to the change form page ------------------- 616 self.selenium.find_element_by_link_text('Home').click() 617 self.selenium.back() 618 619 # Check that everything is still in place ----------------------------- 620 from_box = '#id_students_from' 621 to_box = '#id_students_to' 622 self.assertSelectOptions(from_box, 623 [str(self.bob.id), str(self.jenny.id), 624 str(self.lisa.id), str(self.peter.id)]) 625 self.assertSelectOptions(to_box, 626 [str(self.arthur.id), str(self.cliff.id), 627 str(self.jason.id), str(self.john.id)]) 628 629 from_box = '#id_alumni_from' 630 to_box = '#id_alumni_to' 631 self.assertSelectOptions(from_box, 632 [str(self.bob.id), str(self.jenny.id), 633 str(self.lisa.id), str(self.peter.id)]) 634 self.assertSelectOptions(to_box, 635 [str(self.arthur.id), str(self.cliff.id), 636 str(self.jason.id), str(self.john.id)]) 637 638 # Save and check that everything is properly stored in the database --- 639 self.selenium.find_element_by_xpath('//input[@value="Save"]').click() 640 self.school = models.School.objects.get(id=self.school.id) # Reload from database 641 self.assertEqual(list(self.school.students.all()), 642 [self.arthur, self.cliff, self.jason, self.john]) 643 self.assertEqual(list(self.school.alumni.all()), 644 [self.arthur, self.cliff, self.jason, self.john]) 645 608 646 def test_filter(self): 609 647 """ 610 648 Ensure that typing in the search box filters out options displayed in … … class HorizontalVerticalFilterSeleniumFirefoxTests(AdminSeleniumWebDriverTestCas 624 662 for field_name in ['students', 'alumni']: 625 663 from_box = '#id_%s_from' % field_name 626 664 to_box = '#id_%s_to' % field_name 627 choose_link = '#id_%s_ add_link' % field_name628 remove_link = '#id_%s_ remove_link' % field_name665 choose_link = '#id_%s_selector .selector-add' % field_name 666 remove_link = '#id_%s_selector .selector-remove' % field_name 629 667 input = self.selenium.find_element_by_css_selector('#id_%s_input' % field_name) 630 668 631 669 # Initial values