Ian Struble avatar Ian Struble committed 5e1e20f

Adding search-icon next to autocomplete fields.

Also fixed up the way that the dismiss* functions were recording the selected item.

The search-icon can be disabled by setting show_search = False

class BookAdmin(admin.ModelAdmin):
autocomplete_fields = {
'author': { 'fields': ('name',), 'show_search': False },
}

Comments (0)

Files changed (6)

django/contrib/admin/media/js/admin/RelatedObjectLookups.js

 function dismissRelatedLookupPopup(win, chosenId) {
     var name = windowname_to_id(win.name);
     var elem = document.getElementById(name);
-    if (elem.className.indexOf('vManyToManyRawIdAdminField') != -1 && elem.value) {
+    var $ = django && django.jQuery;
+    var autocomplete_elem = !!$ && $(elem);
+    if (!!autocomplete_elem && !!autocomplete_elem.data('djangoautocomplete')) {
+        $.getJSON(
+                autocomplete_elem.data('djangoautocomplete').options.source,
+                {term: chosenId, by_id: 1},
+                function (data) {
+                    // Pass the returned item to the normal
+                    // autocomplete onSelect handler.
+                    autocomplete_elem.data('autocomplete')
+                        .options.select({}, {item: data[0]});
+                });
+    }
+    else if (elem.className.indexOf('vManyToManyRawIdAdminField') != -1 && elem.value) {
         elem.value += ',' + chosenId;
     } else {
-        document.getElementById(name).value = chosenId;
+        elem.value = chosenId;
     }
     win.close();
 }

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

                 .append( $( "<a></a>" ).append( item.label ) )
                 .appendTo( ul );
         },
-
+      is_djangoautocomplete: true
     },
     _create: function() {
         var self = this;
         this.element.autocomplete({
             appendTo: this.element.parent(),
             select: function( event, ui ) {
-                self.lastSelected = ui.item;
-                if ( self.options.multiple ) {
-                    if ( $.inArray( ui.item.id, self.values ) < 0 ) {
-                        $('<li></li>')
-                            .addClass( "ui-autocomplete-value" )
-                            .data( "value.autocomplete", ui.item.id )
-                            .append( ui.item.label+'<a href="#">x</a>' )
-                            .appendTo( self.values_ul );
-                        self.values.push( ui.item.id );
+                var item = ui.item.data ? ui.item.data( "item.autocomplete" ) : ui.item;
+                self.lastSelected = item
+                if (self.options.is_djangoautocomplete === true) {
+                    if ( self.options.multiple ) {
+                        if ( $.inArray( item.id, self.values ) < 0 ) {
+                            $('<li></li>')
+                                .addClass( "ui-autocomplete-value" )
+                                .data( "value.autocomplete", item.id )
+                                .append( item.label+'<a href="#">x</a>' )
+                                .appendTo( self.values_ul );
+                            self.values.push( item.id );
+                        }
+                    } else {
+                        self.term = item.value;
+                        self.element.val(item.value);
                     }
                     return false;
                 }

django/contrib/admin/options.py

     limit = 5,
     value = lambda o: unicode(o),
     label = lambda o: unicode(o),
+    show_search = True,
 )
 
 csrf_protect_m = method_decorator(csrf_protect)
 
     def autocomplete_view(self, request, field, extra_content=None):
         query = request.GET.get('term', None)
-        query_by_id = request.GET.get('by_id', None) is not None
         
         if field not in self.autocomplete_fields or query is None:
             raise Http404
 
         settings = self.autocomplete_fields[field]
         queryset = settings['queryset']
+        search_fields = settings['fields']
+        if request.GET.get('by_id', None) is not None:
+            # lookup only via an exact match on id
+            search_fields = ('=id',)
 
         def construct_search(field_name):
             # use different lookup methods depending on the notation
                 return "%s__search" % field_name[1:]
             else:
                 return "%s__icontains" % field_name
-        
-        if query_by_id:
-            # lookup only via an exact match on id
-            settings['fields'] = ('=id',)
 
         for bit in query.split():
             or_queries = [models.Q(**{construct_search(
                 smart_str(field_name)): bit})
-                    for field_name in settings['fields']]
+                    for field_name in search_fields]
 
             queryset = queryset.filter(reduce(operator.or_, or_queries))
         

django/contrib/admin/widgets.py

     template_with_clear = (u'<span class="clearable-file-input">%s</span>'
                            % forms.ClearableFileInput.template_with_clear)
 
+def _get_search_icon(model, name, value, params, attrs):
+    related_url = '../../../%s/%s/' % (model._meta.app_label, model._meta.object_name.lower())
+    if params:
+        url = '?' + '&amp;'.join(['%s=%s' % (k, v) for k, v in params.items()])
+    else:
+        url = ''
+    # TODO: "id_" is hard-coded here. This should instead use the correct
+    # API to determine the ID dynamically.
+    output = []
+    output.append('<a href="%s%s" class="related-lookup" id="lookup_id_%s" onclick="return showRelatedObjectLookupPopup(this);"> ' % \
+        (related_url, url, name))
+    output.append('<img src="%simg/admin/selector-search.gif" width="16" height="16" alt="%s" /></a>' % (settings.ADMIN_MEDIA_PREFIX, _('Lookup')))
+    return output
 
 class ForeignKeyRawIdWidget(forms.TextInput):
     """
     def render(self, name, value, attrs=None):
         if attrs is None:
             attrs = {}
-        related_url = '../../../%s/%s/' % (self.rel.to._meta.app_label, self.rel.to._meta.object_name.lower())
-        params = self.url_parameters()
-        if params:
-            url = '?' + '&amp;'.join(['%s=%s' % (k, v) for k, v in params.items()])
-        else:
-            url = ''
         if "class" not in attrs:
             attrs['class'] = 'vForeignKeyRawIdAdminField' # The JavaScript looks for this hook.
         output = [super(ForeignKeyRawIdWidget, self).render(name, value, attrs)]
-        # TODO: "id_" is hard-coded here. This should instead use the correct
-        # API to determine the ID dynamically.
-        output.append('<a href="%s%s" class="related-lookup" id="lookup_id_%s" onclick="return showRelatedObjectLookupPopup(this);"> ' % \
-            (related_url, url, name))
-        output.append('<img src="%simg/admin/selector-search.gif" width="16" height="16" alt="%s" /></a>' % (settings.ADMIN_MEDIA_PREFIX, _('Lookup')))
+
+        output += _get_search_icon(self.rel.to, name, value, self.url_parameters(), attrs)
         if value:
             output.append(self.label_for_value(value))
         return mark_safe(u''.join(output))
         if not self.js_options.get('source'):
             self.js_options['source'] = self.get_autocomplete_url(name)
         options = simplejson.dumps(self.js_options)
+        if self.settings.get('show_search'):
+            target_key = self.settings.get('id')
+            search_icon = _get_search_icon(self.settings.get('queryset').model, 
+                                           name, value, {'t': target_key}, attrs)
+            search_icon = u''.join(search_icon) + '\n'
+        else:
+            search_icon = u''
         return mark_safe(u''.join((
             u'<input%s />\n' % flatatt(hidden_attrs),
             u'<input%s />\n' % flatatt(normal_attrs),
+            search_icon,
             initial_objects,
             u'<script type="text/javascript">',
             u'django.jQuery("#id_%s").djangoautocomplete(%s);' % (name, options),
 
     def label_for_value(self, value):
         qs, key, value_fmt = [self.settings[k] for k in ('queryset','id','value')]
+        if not value:
+            return value
         try:
             obj = qs.get(**{key: value})
             return value_fmt(obj)

tests/regressiontests/admin_widgets/tests.py

 
         widget_settings = {
             'fields': ('name',),
-            'id': 'pk',
+            'id': 'id',
             'limit': 5,
             'value': lambda o: unicode(o),
             'label': lambda o: unicode(o),
         )
         self.bands = (band, band2)
     
-    def _get_expected(self, name, bands):
+    def _get_expected(self, name, bands, show_search=False):
         bands = [] if bands is None else bands
         expected = [
             '<input type="hidden" name="%(field_name)s" value="%(band_pks)s" id="id_hidden_%(field_name)s" />',
             '<input type="text" value="" />',  # input is for data entry only
             ]
+        if show_search:
+            expected.append('<a href="../../../admin_widgets/band/?t=id" class="related-lookup" id="lookup_id_%s" onclick="return showRelatedObjectLookupPopup(this);"> <img src="%simg/admin/selector-search.gif" width="16" height="16" alt="Lookup" /></a>' % (name, settings.ADMIN_MEDIA_PREFIX))
         if bands:                
                 expected += ['<ul class="ui-autocomplete-values">']
                 expected += ['<li>%s</li>' % b.name for b in bands]
 
         widget_settings = {
             'fields': ('name',),
-            'id': 'pk',
+            'id': 'id',
             'limit': 5,
             'value': lambda o: unicode(o),
             'label': lambda o: unicode(o),
             'queryset': models.Band.objects.all(),
+            'show_search': False,
             }
+        with_search_settings = widget_settings.copy()
+        with_search_settings['show_search'] = True
 
         w = MultipleAutocompleteWidget(widget_settings)
         expected_multiple = self._get_expected('test_multiple', bands)
             expected_none,
         )
 
+        w = MultipleAutocompleteWidget(with_search_settings)
+        bands_none = None
+        expected_none = self._get_expected('test_none', bands_none, True)
+        self.assertEqual(
+            conditional_escape(w.render('test_none', bands_none, attrs={})),
+            expected_none,
+        )
+        
 
     def test_autocomplete_lookup(self):
         band = self.bands[1]

tests/regressiontests/admin_widgets/widgetadmin.py

 
 class AlbumAdmin(admin.ModelAdmin):
     autocomplete_fields = {
-        'band': { 'fields': ('name',), 'id': 'pk' }
+        'band': { 'fields': ('name',) }
             }
 
 site = WidgetAdmin(name='widget-admin')
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.