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)
Change History (17)
by , 18 years ago
Attachment: | selector.PNG added |
---|
comment:1 by , 18 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: | Unreviewed → Design decision needed |
comment:2 by , 18 years ago
Cc: | added |
---|
comment:3 by , 17 years ago
Triage Stage: | Design decision needed → Someday/Maybe |
---|
comment:4 by , 16 years ago
Keywords: | nfa added |
---|
Admin interface is reworked (NFA). Is it possible to get this nice feature into next release?
comment:5 by , 16 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 , 14 years ago
Severity: | normal → Normal |
---|---|
Type: | enhancement → New feature |
comment:7 by , 14 years ago
UI/UX: | set |
---|
comment:11 by , 10 years ago
Cc: | 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 , 9 years ago
Triage Stage: | Someday/Maybe → Accepted |
---|
comment:13 by , 9 years ago
Cc: | 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:15 by , 9 years ago
Cc: | added |
---|
comment:16 by , 8 years ago
Resolution: | → duplicate |
---|---|
Status: | new → closed |
Closing as a duplicate of #14370 which has is more active and has a patch.
At some point, we'll be reworking the admin interface, and this will be one of the things we examine at that time.