Opened 18 years ago

Closed 8 years ago

#2651 closed New feature (duplicate)

ForeignKey search/filter/autocomplete widget

Reported by: zaur Owned by: nobody
Component: contrib.admin Version: dev
Severity: Normal Keywords: FilterSelector filter_interface nfa
Cc: portland@…, cmawebsite@…, info@…, tymoteusz.jankowski@…, tymoteusz.jankowski@… Triage Stage: Accepted
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@… 18 years ago.

Download all attachments as: .zip

Change History (17)

by szport@…, 18 years ago

Attachment: selector.PNG added

comment:1 by Simon G. <dev@…>, 17 years ago

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

comment:2 by Wesley Fok <portland@…>, 17 years ago

Cc: portland@… added

comment:3 by Jacob, 16 years ago

Triage Stage: Design decision neededSomeday/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 by anonymous, 15 years ago

Keywords: nfa added

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

comment:5 by Karen Tracey, 15 years ago

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 by Łukasz Rekucki, 13 years ago

Severity: normalNormal
Type: enhancementNew feature

comment:7 by Julien Phalip, 13 years ago

UI/UX: set

comment:8 by Aymeric Augustin, 12 years ago

UI/UX: unset

Change UI/UX from NULL to False.

comment:9 by Aymeric Augustin, 12 years ago

Easy pickings: unset

Change Easy pickings from NULL to False.

comment:10 by Aymeric Augustin, 12 years ago

UI/UX: set

Revert accidental batch modification.

comment:11 by Collin Anderson, 9 years ago

Cc: cmawebsite@… added
Summary: Adding to admin nice selector for editing ForeignKey fields in case of relatively large number of items (adopted from SelectFilter2.js)ForeignKey search/filter/autocomplete widget

comment:12 by Tim Graham, 9 years ago

Triage Stage: Someday/MaybeAccepted

comment:13 by Johannes Maron, 8 years ago

Cc: info@… added

Is there any particular plan on how to implement that?
I'm maintaining django-select2 which does a wonderful job of solving that problem. I wonder if it would be over the to to rely on a 3rd party package like jquery.select2 or twitter's typeahead to solve this issue.

comment:14 by Collin Anderson, 8 years ago

comment:15 by xliiv, 8 years ago

Cc: tymoteusz.jankowski@… tymoteusz.jankowski@… added

comment:16 by Tim Graham, 8 years ago

Resolution: duplicate
Status: newclosed

Closing as a duplicate of #14370 which has is more active and has a patch.

Note: See TracTickets for help on using tickets.
Back to Top