Ticket #13614: 13614.filterselect-bug.5.diff

File 13614.filterselect-bug.5.diff, 41.8 KB (added by Ramiro Morales, 12 years ago)

Patch updated to ignore ordering of options when checking contents of vertical/horizontal select widgets

  • django/contrib/admin/options.py

    diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
    a b  
    10131013    @transaction.commit_on_success
    10141014    def change_view(self, request, object_id, form_url='', extra_context=None):
    10151015        "The 'change' admin view for this model."
     1016
    10161017        model = self.model
    10171018        opts = model._meta
    10181019
     
    13781379        if self.prepopulated_fields:
    13791380            js.extend(['urlify.js', 'prepopulate%s.js' % extra])
    13801381        if self.filter_vertical or self.filter_horizontal:
    1381             js.extend(['SelectBox.js', 'SelectFilter2.js'])
     1382            js.extend(['selectfilter.js'])
    13821383        return forms.Media(js=[static('admin/js/%s' % url) for url in js])
    13831384
    13841385    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
    + -  
    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 node
    9     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 ID
    23         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 select
    32                 // boxes or wrapped off on the side to the right of the add
    33                 // 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 interface
    86         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_box
    96         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 refresh
    112         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 selected
    120         $('#' + 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 empty
    123         $('#' + 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 Enter
    129         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 across
    143         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 around
    150         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 around
    154         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
    - +  
     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('&nbsp;')
     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('&nbsp;')
     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/tests.py

    diff --git a/django/contrib/admin/tests.py b/django/contrib/admin/tests.py
    a b  
    8585                return option
    8686        raise NoSuchElementException('Option "%s" not found in "%s"' % (value, selector))
    8787
    88     def assertSelectOptions(self, selector, values):
     88    def assertSelectOptions(self, selector, values, order_matters=True):
    8989        """
    9090        Asserts that the <SELECT> widget identified by `selector` has the
    9191        options with the given `values`.
     92        :param order_matters: controls if the order of the options is taken in
     93        account to decide if the expected widget contents is correct.
    9294        """
    9395        options = self.selenium.find_elements_by_css_selector('%s option' % selector)
    9496        actual_values = []
    9597        for option in options:
    9698            actual_values.append(option.get_attribute('value'))
    97         self.assertEqual(values, actual_values)
     99        if order_matters:
     100            self.assertEqual(values, actual_values)
     101        else:
     102            self.assertEqual(set(values), set(actual_values))
    98103
    99104    def has_css_class(self, selector, klass):
    100105        """
     
    102107        `klass`.
    103108        """
    104109        return (self.selenium.find_element_by_css_selector(selector)
    105                 .get_attribute('class').find(klass) != -1)
    106  No newline at end of file
     110                .get_attribute('class').find(klass) != -1)
  • django/contrib/admin/widgets.py

    diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py
    a b  
    2424    """
    2525    @property
    2626    def media(self):
    27         js = ["core.js", "SelectBox.js", "SelectFilter2.js"]
     27        js = ["SelectBox.js", "selectfilter.js"]
    2828        return forms.Media(js=[static("admin/js/%s" % path) for path in js])
    2929
    3030    def __init__(self, verbose_name, is_stacked, attrs=None, choices=()):
     
    3535    def render(self, name, value, attrs=None, choices=()):
    3636        if attrs is None:
    3737            attrs = {}
    38         attrs['class'] = 'selectfilter'
    3938        if self.is_stacked:
    40             attrs['class'] += 'stacked'
     39            attrs['class'] = 'stacked'
     40        id_ = attrs.get('id', 'id_%s' % name)
    4141        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
    4762        return mark_safe(u''.join(output))
    4863
    4964class AdminDateWidget(forms.DateInput):
     
    153168                url = u''
    154169            if "class" not in attrs:
    155170                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_))
    160174            extra.append(u'<img src="%s" width="16" height="16" alt="%s" /></a>'
    161175                            % (static('admin/img/selector-search.gif'), _('Lookup')))
    162176        output = [super(ForeignKeyRawIdWidget, self).render(name, value, attrs)] + extra
     
    259273        output = [self.widget.render(name, value, *args, **kwargs)]
    260274        if self.can_add_related:
    261275            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 the
    263             # 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))
    266280            output.append(u'<img src="%s" width="10" height="10" alt="%s"/></a>'
    267281                          % (static('admin/img/icon_addlink.gif'), _('Add Another')))
    268282        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
    a b  
    196196        w = widgets.FilteredSelectMultiple('test', False)
    197197        self.assertHTMLEqual(
    198198            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()
    200200        )
    201201
    202202    def test_stacked_render(self):
    203203        w = widgets.FilteredSelectMultiple('test', True)
    204204        self.assertHTMLEqual(
    205205            conditional_escape(w.render('test', 'test')),
    206             '<select multiple="multiple" name="test" class="selectfilterstacked">\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()
    207207        )
    208208
    209209class AdminDateWidgetTest(DjangoTestCase):
     
    478478        self.cliff = models.Student.objects.create(name='Cliff')
    479479        self.arthur = models.Student.objects.create(name='Arthur')
    480480        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()
    481484        super(HorizontalVerticalFilterSeleniumFirefoxTests, self).setUp()
    482485
    483486    def assertActiveButtons(self, mode, field_name, choose, remove,
    484487                             choose_all=None, remove_all=None):
    485         choose_link = '#id_%s_add_link' % field_name
    486         choose_all_link = '#id_%s_add_all_link' % field_name
    487         remove_link = '#id_%s_remove_link' % field_name
    488         remove_all_link = '#id_%s_remove_all_link' % field_name
     488        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
    489492        self.assertEqual(self.has_css_class(choose_link, 'active'), choose)
    490493        self.assertEqual(self.has_css_class(remove_link, 'active'), remove)
    491494        if mode == 'horizontal':
     
    495498    def execute_basic_operations(self, mode, field_name):
    496499        from_box = '#id_%s_from' % field_name
    497500        to_box = '#id_%s_to' % field_name
    498         choose_link = 'id_%s_add_link' % field_name
    499         choose_all_link = 'id_%s_add_all_link' % field_name
    500         remove_link = 'id_%s_remove_link' % field_name
    501         remove_all_link = 'id_%s_remove_all_link' % field_name
     501        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
    502505
    503506        # Initial positions ---------------------------------------------------
    504507        self.assertSelectOptions(from_box,
     
    511514
    512515        # Click 'Choose all' --------------------------------------------------
    513516        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()
    515518        elif mode == 'vertical':
    516519            # There 's no 'Choose all' button in vertical mode, so individually
    517520            # select all options and click 'Choose'.
    518521            for option in self.selenium.find_elements_by_css_selector(from_box + ' option'):
    519522                option.click()
    520             self.selenium.find_element_by_id(choose_link).click()
     523            self.selenium.find_element_by_css_selector(choose_link).click()
    521524        self.assertSelectOptions(from_box, [])
    522525        self.assertSelectOptions(to_box,
    523526                        [str(self.lisa.id), str(self.peter.id),
     
    528531
    529532        # Click 'Remove all' --------------------------------------------------
    530533        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()
    532535        elif mode == 'vertical':
    533536            # There 's no 'Remove all' button in vertical mode, so individually
    534537            # select all options and click 'Remove'.
    535538            for option in self.selenium.find_elements_by_css_selector(to_box + ' option'):
    536539                option.click()
    537             self.selenium.find_element_by_id(remove_link).click()
     540            self.selenium.find_element_by_css_selector(remove_link).click()
    538541        self.assertSelectOptions(from_box,
    539542                        [str(self.lisa.id), str(self.peter.id),
    540543                         str(self.arthur.id), str(self.bob.id),
     
    549552        self.get_select_option(from_box, str(self.bob.id)).click()
    550553        self.get_select_option(from_box, str(self.john.id)).click()
    551554        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()
    553556        self.assertActiveButtons(mode, field_name, False, False, True, True)
    554557
    555558        self.assertSelectOptions(from_box,
     
    563566        self.get_select_option(to_box, str(self.lisa.id)).click()
    564567        self.get_select_option(to_box, str(self.bob.id)).click()
    565568        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()
    567570        self.assertActiveButtons(mode, field_name, False, False, True, True)
    568571
    569572        self.assertSelectOptions(from_box,
     
    576579        # Choose some more options --------------------------------------------
    577580        self.get_select_option(from_box, str(self.arthur.id)).click()
    578581        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()
    580583
    581584        self.assertSelectOptions(from_box,
    582585                        [str(self.peter.id), str(self.jenny.id),
     
    586589                         str(self.arthur.id), str(self.cliff.id)])
    587590
    588591    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 
    593592        self.admin_login(username='super', password='secret', login_url='/')
    594593        self.selenium.get(
    595594            '%s%s' % (self.live_server_url, '/admin_widgets/school/%s/' % self.school.id))
     
    605604        self.assertEqual(list(self.school.alumni.all()),
    606605                         [self.arthur, self.cliff, self.jason, self.john])
    607606
     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        # Don't take in account the order of options in the select controls
     621        # to accommodate behavior divergence by some browsers (i.e. Firefox)
     622        from_box = '#id_students_from'
     623        to_box = '#id_students_to'
     624        self.assertSelectOptions(from_box,
     625                        [str(self.bob.id), str(self.jenny.id),
     626                         str(self.lisa.id), str(self.peter.id)], order_matters=False)
     627        self.assertSelectOptions(to_box,
     628                        [str(self.arthur.id), str(self.cliff.id),
     629                         str(self.jason.id), str(self.john.id)], order_matters=False)
     630
     631        from_box = '#id_alumni_from'
     632        to_box = '#id_alumni_to'
     633        self.assertSelectOptions(from_box,
     634                        [str(self.bob.id), str(self.jenny.id),
     635                         str(self.lisa.id), str(self.peter.id)], order_matters=False)
     636        self.assertSelectOptions(to_box,
     637                        [str(self.arthur.id), str(self.cliff.id),
     638                         str(self.jason.id), str(self.john.id)], order_matters=False)
     639
     640        # Save and check that everything is properly stored in the database ---
     641        self.selenium.find_element_by_xpath('//input[@value="Save"]').click()
     642        self.school = models.School.objects.get(id=self.school.id) # Reload from database
     643        self.assertEqual(list(self.school.students.all()),
     644                         [self.arthur, self.cliff, self.jason, self.john])
     645        self.assertEqual(list(self.school.alumni.all()),
     646                         [self.arthur, self.cliff, self.jason, self.john])
     647
    608648    def test_filter(self):
    609649        """
    610650        Ensure that typing in the search box filters out options displayed in
     
    624664        for field_name in ['students', 'alumni']:
    625665            from_box = '#id_%s_from' % field_name
    626666            to_box = '#id_%s_to' % field_name
    627             choose_link = '#id_%s_add_link' % field_name
    628             remove_link = '#id_%s_remove_link' % field_name
     667            choose_link = '#id_%s_selector .selector-add' % field_name
     668            remove_link = '#id_%s_selector .selector-remove' % field_name
    629669            input = self.selenium.find_element_by_css_selector('#id_%s_input' % field_name)
    630670
    631671            # Initial values
     
    685725                         [self.jason, self.peter])
    686726
    687727class HorizontalVerticalFilterSeleniumChromeTests(HorizontalVerticalFilterSeleniumFirefoxTests):
    688     webdriver_class = 'selenium.webdriver.chrome.webdriver.WebDriver'
    689  No newline at end of file
     728    webdriver_class = 'selenium.webdriver.chrome.webdriver.WebDriver'
Back to Top