Opened 9 years ago

Last modified 5 months ago

#2651 new New feature

ForeignKey search/filter/autocomplete widget

Reported by: zaur Owned by: nobody
Component: contrib.admin Version: master
Severity: Normal Keywords: FilterSelector filter_interface nfa
Cc: portland@…, cmawebsite@… Triage Stage: Someday/Maybe
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: yes

Description

This is an adoptation of nice SelectFilter2.js for single item selecting with search in change forms
in case of relatively large lists and addition of filter_interface parameter for ForegnKeyField.
Here are changes against sources (0.96-pre) (24.08.2006).
It's seems work for me.

# add file SelectFilter3.js to ..\admin\media\js (adaptation of SelectFilter2.js)

/*
SelectFilter3 - Turns a multiple-select box into a filter interface.

Different than SelectFilter because this is coupled to the admin framework.

Requires core.js, SelectBox.js and addevent.js.
*/

function findForm(node) {
    // returns the node of the form containing the given node
    if (node.tagName.toLowerCase() != 'form') {
        return findForm(node.parentNode);
    }
    return node;
}

var SelectFilter3 = {
    init: function(field_id, field_name, is_stacked, admin_media_prefix) {
        var from_box = document.getElementById(field_id);
        from_box.id += '_from'; // change its ID
        from_box.className = 'filtered';
        from_box.size = 5

        // Remove <p class="info">, because it just gets in the way.
        var ps = from_box.parentNode.getElementsByTagName('p');
        for (var i=0; i<ps.length; i++) {
            from_box.parentNode.removeChild(ps[i]);
        }

        // <div class="selector"> or <div class="selector stacked">
        var selector_div = quickElement('div', from_box.parentNode);
        selector_div.className = is_stacked ? 'selector stacked' : 'selector';

        // <div class="selector-available">
        var selector_available = quickElement('div', selector_div, '');
        selector_available.className = 'selector-available';
        quickElement('h2', selector_available, interpolate(gettext('Select'), [field_name]));
        var filter_p = quickElement('p', selector_available, '');
        filter_p.className = 'selector-filter';
        quickElement('img', filter_p, '', 'src', admin_media_prefix + 'img/admin/selector-search.gif');
        filter_p.appendChild(document.createTextNode(' '));
        var filter_input = quickElement('input', filter_p, '', 'type', 'text');
        filter_input.id = field_id + '_input';
        selector_available.appendChild(from_box);
        //var choose_all = quickElement('a', selector_available, gettext('Choose all'), 'href', 'javascript: (function(){ SelectBox.move_all("' + field_id + '_from", "' + field_id + '_to"); })()');
        //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('Add'), 'href', 'javascript: (function(){ SelectBox.move("' + field_id + '_from","' + field_id + '_to");})()');
        //add_link.className = 'selector-add';
        //var remove_link = quickElement('a', quickElement('li', selector_chooser, ''), gettext('Remove'), 'href', 'javascript: (function(){ SelectBox.move("' + field_id + '_to","' + field_id + '_from");})()');
        //remove_link.className = 'selector-remove';

        // <div class="selector-chosen">
        //var selector_chosen = quickElement('div', selector_div, '');
        //selector_chosen.className = 'selector-chosen';
        //quickElement('h2', selector_chosen, interpolate(gettext('Chosen %s'), [field_name]));
        //var selector_filter = quickElement('p', selector_chosen, gettext('Select your choice(s) and click '));
        //selector_filter.className = 'selector-filter';
        //quickElement('img', selector_filter, '', 'src', admin_media_prefix + 'img/admin/selector-add.gif', 'alt', 'Add');
        //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('Clear all'), 'href', 'javascript: (function() { SelectBox.move_all("' + field_id + '_to", "' + field_id + '_from");})()');
        //clear_all.className = 'selector-clearall';

        //from_box.setAttribute('name', from_box.getAttribute('name') + '_old');

        // Set up the JavaScript event handlers for the select box filter interface
        addEvent(filter_input, 'keyup', function(e) { SelectFilter.filter_key_up(e, field_id); });
        addEvent(filter_input, 'keydown', function(e) { SelectFilter.filter_key_down(e, field_id); });
        //addEvent(from_box, 'dblclick', function() { SelectBox.move(field_id + '_from', field_id + '_to'); });
        //addEvent(to_box, 'dblclick', function() { SelectBox.move(field_id + '_to', field_id + '_from'); });
        addEvent(from_box, 'submit', function() { SelectBox.select_all(field_id + '_from'); });
        SelectBox.init(field_id + '_from');
        //SelectBox.init(field_id + '_to');
        // Move selected from_box options to to_box
        //SelectBox.move(field_id + '_from', field_id + '_to');
    },
    filter_key_up: function(event, field_id) {
        from = document.getElementById(field_id + '_from');
        // don't submit form if user pressed Enter
        if ((event.which && event.which == 13) || (event.keyCode && event.keyCode == 13)) {
            from.selectedIndex = 0;
            //SelectBox.move(field_id + '_from', field_id + '_to');
            from.selectedIndex = 0;
            return false;
        }
        var temp = from.selectedIndex;
        SelectBox.filter(field_id + '_from', document.getElementById(field_id + '_input').value);
        from.selectedIndex = temp;
        return true;
    },
    filter_key_down: function(event, field_id) {
        from = document.getElementById(field_id + '_from');
        // right arrow -- move across
        if ((event.which && event.which == 39) || (event.keyCode && event.keyCode == 39)) {
            var old_index = from.selectedIndex;
            //SelectBox.move(field_id + '_from', field_id + '_to');
            from.selectedIndex = (old_index == from.length) ? from.length - 1 : old_index;
            return false;
        }
        // down arrow -- wrap around
        if ((event.which && event.which == 40) || (event.keyCode && event.keyCode == 40)) {
            from.selectedIndex = (from.length == from.selectedIndex + 1) ? 0 : from.selectedIndex + 1;
        }
        // up arrow -- wrap around
        if ((event.which && event.which == 38) || (event.keyCode && event.keyCode == 38)) {
            from.selectedIndex = (from.selectedIndex == 0) ? from.length - 1 : from.selectedIndex - 1;
        }
        return true;
    }
}

# in ..\admin\views\main.py
# add import of 'js/SelectFilter3.js'

def get_javascript_imports(opts, auto_populated_fields, field_sets):
        # ...
        for field_line in field_set:
            try:
                for f in field_line:
                    if f.rel: # and isinstance(f, models.ManyToManyField) and f.rel.filter_interface:
                        js.extend(['js/SelectBox.js' , 'js/SelectFilter2.js', 'js/SelectFilter3.js'])
                        raise StopIteration
            except StopIteration:
                break
        # ....

# in ..\admin\templatestag\admin_modify.py

def filter_interface_script_maybe(bound_field):
    f = bound_field.field
    if f.rel and isinstance(f.rel, models.ManyToManyRel) and f.rel.filter_interface:
        return '<script type="text/javascript">addEvent(window, "load", function(e) {' \
              ' SelectFilter.init("id_%s", %r, %s, "%s"); });</script>\n' % (
              f.name, f.verbose_name, 
              f.rel.filter_interface-1, 
              settings.ADMIN_MEDIA_PREFIX)
    elif f.rel and isinstance(f.rel, models.ManyToOneRel) and f.rel.filter_interface:
        return '<script type="text/javascript">addEvent(window, "load", function(e) {' \
              ' SelectFilter3.init("id_%s", %r, %s, "%s"); });</script>\n' % (
              f.name, f.verbose_name, 
              #f.rel.filter_interface-1, 
              2, 
              settings.ADMIN_MEDIA_PREFIX)
    else:
        return ''
filter_interface_script_maybe = register.simple_tag(filter_interface_script_maybe)

# in ..\db\models\fields\related.py
# add filter_interface=kwargs.pop('filter_interface', None)

class ForeignKey(RelatedField, Field):
        #......
        kwargs['rel'] = ManyToOneRel(to, to_field,
            num_in_admin=kwargs.pop('num_in_admin', 3),
            min_num_in_admin=kwargs.pop('min_num_in_admin', None),
            max_num_in_admin=kwargs.pop('max_num_in_admin', None),
            num_extra_on_change=kwargs.pop('num_extra_on_change', 1),
            filter_interface=kwargs.pop('filter_interface', None),
            edit_inline=kwargs.pop('edit_inline', False),
            related_name=kwargs.pop('related_name', None),
            limit_choices_to=kwargs.pop('limit_choices_to', None),
            lookup_overrides=kwargs.pop('lookup_overrides', None),
            raw_id_admin=kwargs.pop('raw_id_admin', False))
        # ...

# add filter_interface keyword argument and assignment

class ManyToOneRel(object):
    def __init__(self, to, field_name, num_in_admin=3, min_num_in_admin=None,
        max_num_in_admin=None, num_extra_on_change=1, edit_inline=False, filter_interface=None, 
        related_name=None, limit_choices_to=None, lookup_overrides=None, raw_id_admin=False):
        try:
            to._meta
        except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT
            assert isinstance(to, basestring), "'to' must be either a model, a model name or the string %r" % RECURSIVE_RELATIONSHIP_CONSTANT
        self.to, self.field_name = to, field_name
        self.num_in_admin, self.edit_inline = num_in_admin, edit_inline
        self.min_num_in_admin, self.max_num_in_admin = min_num_in_admin, max_num_in_admin
        self.num_extra_on_change, self.related_name = num_extra_on_change, related_name
        if limit_choices_to is None:
            limit_choices_to = {}
        self.limit_choices_to = limit_choices_to
        # it's here
        self.filter_interface = filter_interface and 1 or 0
        self.lookup_overrides = lookup_overrides or {}
        self.raw_id_admin = raw_id_admin
        self.multiple = True

Attachments (1)

selector.PNG (4.4 KB) - added by szport@… 9 years ago.

Download all attachments as: .zip

Change History (12)

Changed 9 years ago by szport@…

comment:1 Changed 8 years ago by Simon G. <dev@…>

  • Summary changed from Addingg to admin nice selector for editing ForeignKey fields in case of relatively large number of items (adopted from SelectFilter2.js) to Adding to admin nice selector for editing ForeignKey fields in case of relatively large number of items (adopted from SelectFilter2.js)
  • Triage Stage changed from Unreviewed to Design decision needed

comment:2 Changed 8 years ago by Wesley Fok <portland@…>

  • Cc portland@… added

comment:3 Changed 8 years ago by jacob

  • Triage Stage changed from Design decision needed to Someday/Maybe

At some point, we'll be reworking the admin interface, and this will be one of the things we examine at that time.

comment:4 Changed 6 years ago by anonymous

  • Keywords nfa added

Admin interface is reworked (NFA). Is it possible to get this nice feature into next release?

comment:5 Changed 6 years ago by kmtracey

It isn't clear to me from the one sentence at the beginning of the description what problem this ticket is solving, so a more fleshed out description of the problem and proposed enhancement would help in assessing the ticket.

Also, a proper patch against current code would have a better chance of getting looked at. Personally I'm not inclined to try integrating the inline code from this ticket into the current code tree to see what it is that it does.

Finally, something called !SelectFilter3 rubs me the wrong way -- if it needs to be entirely separate from SelectFilter give it a proper name that distinguishes it based on how it differs. (I don't know why !SelectFilter2.js has the 2 tacked on the end, since in fact inside it defines SelectFilter, not !SelectFilter2. This odd naming seems to be an historical oddity; I don't see any reason to propagate it even further moving forward.)

comment:6 Changed 4 years ago by lrekucki

  • Severity changed from normal to Normal
  • Type changed from enhancement to New feature

comment:7 Changed 4 years ago by julien

  • UI/UX set

comment:8 Changed 3 years ago by aaugustin

  • UI/UX unset

Change UI/UX from NULL to False.

comment:9 Changed 3 years ago by aaugustin

  • Easy pickings unset

Change Easy pickings from NULL to False.

comment:10 Changed 3 years ago by aaugustin

  • UI/UX set

Revert accidental batch modification.

comment:11 Changed 5 months ago by collinanderson

  • Cc cmawebsite@… added
  • Summary changed from Adding to admin nice selector for editing ForeignKey fields in case of relatively large number of items (adopted from SelectFilter2.js) to ForeignKey search/filter/autocomplete widget
Note: See TracTickets for help on using tickets.
Back to Top