Commits

tyrion  committed a350592

Autocomplete is fully customizable and can be used with any form field.

  • Participants
  • Parent commits 628a81c

Comments (0)

Files changed (3)

File django/contrib/admin/media/js/admin/autocomplete.js

 $.widget( "ui.djangoautocomplete", {
     options: {
         source: "../autocomplete/$name/",
-        m2m: false,
+        multiple: false,
+        force_selection: true,
+        renderItem: function( ul, item) {
+            return $( "<li></li>" )
+                .data( "item.autocomplete", item )
+                .append( $( "<a></a>" ).append( item.label ) )
+                .appendTo( ul );
+        },
+
     },
     _create: function() {
         var self = this;
         this.hidden_input = this.element.prev( "input[type=hidden]" );
         this.name = this.hidden_input.attr( "name" );
         this.element.autocomplete({
+            appendTo: this.element.parent(),
             select: function( event, ui ) {
                 self.lastSelected = ui.item;
-                if ( self.options.m2m ) {
+                if ( self.options.multiple ) {
                     if ( $.inArray( ui.item.id, self.values ) < 0 ) {
-                        $('<li />')
+                        $('<li></li>')
                             .addClass( "ui-autocomplete-value" )
                             .data( "value.autocomplete", ui.item.id )
                             .append( ui.item.label+'<a href="#">x</a>' )
                     return false;
                 }
             },
-        });
+        }).data( "autocomplete" )._renderItem = this.options.renderItem;
         this._initSource();
-        if ( this.options.m2m ) {
+        if ( this.options.multiple ) {
             this._initManyToMany();
         } else {
             this.lastSelected = {
                 value: this.element.val(),
             };
         }
-        // Implements "force selection".
-        this.element.focusout(function() {
-            if ( self.element.val() !== self.lastSelected.value ) {
-                self.element.val( "" );
-            }
-        });
+        if (this.options.force_selection) {
+            this.element.focusout(function() {
+                if ( self.element.val() != self.lastSelected.value ) {
+                    self.element.val( "" );
+                }
+            });
+        }
         this.element.closest( "form" ).submit(function() {
-            if ( self.options.m2m ) {
+            if ( self.options.multiple ) {
                 self.hidden_input.val( self.values.join(",") );
+            } else if ( self.options.force_selection ) {
+                self.hidden_input.val( self.element.val() ? self.lastSelected.id : "" );
             } else {
-                self.hidden_input.val( self.element.val() ? self.lastSelected.id : "" );
+                self.hidden_input.val( self.element.val() );
             }
         });
     },
     
     destroy: function() {
         this.element.autocomplete( "destroy" );
-        if ( this.options.m2m ) this.values_ul.remove();
+        if ( this.options.multiple ) this.values_ul.remove();
 		$.Widget.prototype.destroy.call( this );
     },
 
                 self.values.push( parseInt(id) );
             });
         }
-        this.values_ul = this.element.next( "ul" );
+        this.values_ul = this.element.nextAll( "ul.ui-autocomplete-values" );
         this.lastSelected = { id: null, value: null };
         if ( this.values.length && this.values_ul[0] ) {
             this.values_ul.children().each(function(i) {
                     .append( '<a href="#">x</a>' );
             });
         } else {
-            this.values_ul = $( "<ul />" ).insertAfter( this.element );
+            this.values_ul = $( "<ul></ul>" ).insertAfter( this.element );
         }
         this.values_ul.addClass( "ui-autocomplete-values" );
         $( ".ui-autocomplete-value a", this.values_ul[0] ).live( "click", function() {
     },
 });
 
-$(function() {
-    $('.vForeignKeyAutocompleteField').djangoautocomplete();
-    $('.vManyToManyAutocompleteField').djangoautocomplete({m2m: true});
-});
-
 })(django.jQuery);

File django/contrib/admin/options.py

         overrides.update(self.formfield_overrides)
         self.formfield_overrides = overrides
 
-        def build_setting(value, field):
-            rel_model = getattr(self.model, field).field.rel.to
-            if value in rel_model._meta.get_all_field_names():
+        def build_setting(value):
+            if value in settings['queryset'].model._meta.get_all_field_names():
                 return lambda m: getattr(m, value)
             return lambda m: value % vars(m)
 
         for (field, values) in self.autocomplete_fields.items():
             settings = autocomplete_fields[field] = AUTOCOMPLETE_FIELDS_DEFAULTS.copy()
             settings.update(values)
+            if hasattr(self.model, field):
+                rel = getattr(self.model, field).field.rel
+                settings['id'] = settings.get('id', rel.get_related_field().name)
+                if not settings.get('queryset'):
+                    settings['queryset'] = rel.to._default_manager.complex_filter(rel.limit_choices_to)
             for option in ('value', 'label'):
                 if isinstance(settings[option], (str, unicode)):
-                    settings[option] = build_setting(settings[option], field)
+                    settings[option] = build_setting(settings[option])
 
         self.autocomplete_fields = autocomplete_fields
 
 
             return formfield
 
+        elif db_field.name in self.autocomplete_fields:
+            kwargs['widget'] = widgets.AutocompleteWidget(
+                self.autocomplete_fields[db_field.name],
+                using=kwargs.get('using'),
+                force_selection=False)
+
         # If we've got overrides for the formfield defined, use 'em. **kwargs
         # passed to formfield_for_dbfield override the defaults.
         for klass in db_field.__class__.mro():
         if db_field.name in self.raw_id_fields:
             kwargs['widget'] = widgets.ForeignKeyRawIdWidget(db_field.rel, using=db)
         elif db_field.name in self.autocomplete_fields:
-            kwargs['widget'] = widgets.ForeignKeyAutocompleteWidget(db_field.rel,
+            kwargs['widget'] = widgets.AutocompleteWidget(
                 self.autocomplete_fields[db_field.name], using=db)
         elif db_field.name in self.radio_fields:
             kwargs['widget'] = widgets.AdminRadioSelect(attrs={
             kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.rel, using=db)
             kwargs['help_text'] = ''
         elif db_field.name in self.autocomplete_fields:
-            kwargs['widget'] = widgets.ManyToManyAutocompleteWidget(db_field.rel,
+            kwargs['widget'] = widgets.MultipleAutocompleteWidget(
                 self.autocomplete_fields[db_field.name], using=db)
             kwargs['help_text'] = ''
         elif db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)):
         
         if field not in self.autocomplete_fields or query is None:
             raise Http404
-        
-        relation = getattr(self.model, field).field.rel
-        queryset = relation.to._default_manager.complex_filter(relation.limit_choices_to)
+
         settings = self.autocomplete_fields[field]
+        queryset = settings['queryset']
 
         def construct_search(field_name):
             # use different lookup methods depending on the notation
         data = []
         for o in queryset[:settings['limit']]:
             data.append(dict(
-                id = o.pk,
+                id = getattr(o, settings['id']),
                 value = settings['value'](o),
                 label = settings['label'](o),
             ))

File django/contrib/admin/widgets.py

 from django import forms
 from django.forms.widgets import RadioFieldRenderer
 from django.forms.util import flatatt
+from django.utils import simplejson
 from django.utils.html import escape
 from django.utils.text import truncate_words
 from django.utils.translation import ugettext as _
         except (ValueError, self.rel.to.DoesNotExist):
             return ''
 
-class ForeignKeyAutocompleteWidget(forms.HiddenInput):
+class AutocompleteWidget(forms.Widget):
     
     class Media:
         js = (
             settings.ADMIN_MEDIA_PREFIX + 'js/admin/autocomplete.js',
         )
 
-    def __init__(self, rel, settings, attrs=None, using=None):
-        self.rel = rel
+    def __init__(self, settings, attrs=None, using=None, **js_options):
         self.settings = settings
         self.db = using
-        super(ForeignKeyAutocompleteWidget, self).__init__(attrs)
+        self.js_options = dict(
+            source = settings.get('source'),
+            multiple = settings.get('multiple', False),
+            force_selection = settings.get('force_selection', True),
+        )
+        self.js_options.update(js_options)
+        super(AutocompleteWidget, self).__init__(attrs)
 
     def get_autocomplete_url(self, name):
         return '../autocomplete/%s/' % name
 
-    def render(self, name, value, attrs=None):
-        output = [super(ForeignKeyAutocompleteWidget, self).render(name, value, {'id': 'id_hidden_%s' % name})]
-        final_attrs = self.build_attrs(attrs, type='text')
+    def render(self, name, value, attrs=None, hattrs=None, initial_objects=u''):
+        if value is None:
+            value = ''
+        hidden_id = 'id_hidden_%s' % name
+        hidden_attrs = self.build_attrs(type='hidden', name=name, value=value, id=hidden_id)
+        normal_attrs = self.build_attrs(attrs, type='text')
         if value:
-            final_attrs['value'] = self.label_for_value(value)
-        if not 'class' in final_attrs:
-            final_attrs['class'] = 'vForeignKeyAutocompleteField'
-        output.append(u'<input%s />' % flatatt(final_attrs))
-        return mark_safe(u''.join(output))
+            normal_attrs['value'] = self.label_for_value(value)
+        if not self.js_options.get('source'):
+            self.js_options['source'] = self.get_autocomplete_url(name)
+        options = simplejson.dumps(self.js_options)
+        return mark_safe(u''.join((
+            u'<input%s />\n' % flatatt(hidden_attrs),
+            u'<input%s />\n' % flatatt(normal_attrs),
+            initial_objects,
+            u'<script type="text/javascript">',
+            u'django.jQuery("#id_%s").djangoautocomplete(%s);' % (name, options),
+            u'</script>\n',
+        )))
 
     def label_for_value(self, value):
-        key = self.rel.get_related_field().name
+        qs, key, value_fmt = [self.settings[k] for k in ('queryset','id','value')]
         try:
-            obj = self.rel.to._default_manager.using(self.db).get(**{key: value})
-            return self.settings['value'](obj)
+            obj = qs.get(**{key: value})
+            return value_fmt(obj)
         # XXX this shouldn't happen.
-        except self.rel.to.DoesNotExist:
+        except qs.model.DoesNotExist:
             return value
 
 
                 return True
         return False
 
-class ManyToManyAutocompleteWidget(ForeignKeyAutocompleteWidget):
+class MultipleAutocompleteWidget(AutocompleteWidget):
 
-    def render(self, name, value, attrs=None):
-        attrs = attrs or {}
-        attrs['class'] = 'vManyToManyAutocompleteField'
-        if not value:
-            return super(ManyToManyAutocompleteWidget, self).render(name, value, attrs)
-        key = '%s__in' % self.rel.get_related_field().name
-        objs = self.rel.to._default_manager.using(self.db).filter(**{key: value})
-        value = ','.join([str(v) for v in value])
-        output = [super(ManyToManyAutocompleteWidget, self).render(name, value, attrs)]
-        output.append('<ul class="ui-autocomplete-values">')
-        for o in objs:
-            output.append('<li>%s</li>' % self.settings['label'](o))
-        output.append('</ul>')
-        return mark_safe(u''.join(output))
+    def __init__(self, settings, attrs=None, using=None, **js_options):
+        js_options['multiple'] = True
+        super(MultipleAutocompleteWidget, self).__init__(settings, attrs,
+            using, **js_options)
+
+    def render(self, name, value, attrs=None, hattrs=None):
+        if value:
+            initial_objects = self.initial_objects(value)
+            value = ','.join([str(v) for v in value])
+        else:
+            initial_objects = u''
+        return super(MultipleAutocompleteWidget, self).render(
+            name, value, attrs, hattrs, initial_objects)
 
     def label_for_value(self, value):
         return ''
         if value:
             return value.split(',')
         return value
+    
+    def initial_objects(self, value):
+        qs, key, label_fmt = [self.settings[k] for k in ('queryset','id','label')]
+        output = [u'<ul class="ui-autocomplete-values">']
+        for obj in qs.filter(**{'%s__in' % key: value}):
+            output.append(u'<li>%s</li>' % label_fmt(obj))
+        output.append(u'</ul>\n')
+        return mark_safe(u'\n'.join(output))
 
 
 class RelatedFieldWidgetWrapper(forms.Widget):