Commits

za...@bcc190cf-cafb-0310-a4f2-bffc1f526a37  committed 05f5eff

[soc2009/admin-ui] M2M autocomplete, modeled much like the FK autocomplete.

  • Participants
  • Parent commits f13073d
  • Branches soc2009/admin-ui

Comments (0)

Files changed (3)

File django/contrib/admin/options.py

         if db_field.name in self.raw_id_fields:
             kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.rel)
             kwargs['help_text'] = ''
+        elif db_field.name in self.autocomplete_fields:
+            kwargs['widget'] = widgets.ManyToManySearchInput(db_field.rel, 
+                self.autocomplete_fields[db_field.name])
         elif db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)):
             kwargs['widget'] = widgets.FilteredSelectMultiple(db_field.verbose_name, (db_field.name in self.filter_vertical))
 

File django/contrib/admin/templates/widget/m2m_searchinput.html

+{% load i18n %}
+<textarea id="lookup_{{ name }}" style="display:none;">{{ label }}</textarea>
+<a href="{{ related_url }}{{ url }}" class="related-lookup" id="lookup_id_{{ name }}" onclick="return showRelatedObjectLookupPopup(this);">
+  <img src="{{ admin_media_prefix }}img/admin/selector-search.gif" width="16" height="16" alt="{% trans "Lookup" %}" />
+</a>
+<script type="text/javascript">
+$(document).ready(function() {
+    // Show lookup input
+    $("#lookup_{{ name }}").show();
+    
+    function lookup(query) {
+        $.get('{{ search_path }}', {
+            'search_fields': '{{ search_fields }}',
+            'app_label': '{{ app_label }}',
+            'model_name': '{{ model_name }}',
+            'object_pk': query
+        }, function(data){
+            $('#lookup_{{ name }}').val(data);
+        });
+    };
+    $('#lookup_{{ name }}').autocomplete('{{ search_path }}', {
+        extraParams: {
+            'search_fields': '{{ search_fields }}',
+            'app_label': '{{ app_label }}',
+            'model_name': '{{ model_name }}'
+        },
+        multiple: true,
+        mustMatch: true,
+        autoFill: true
+    }).result(function(event, data, formatted) {
+        if (data) {
+            $('#id_{{ name }}').val($('#id_{{ name }}').val() + data[1] + ",");
+        }
+    });
+});
+</script>

File django/contrib/admin/widgets.py

                 return True
         return False
 
+class ManyToManySearchInput(ManyToManyRawIdWidget):
+    """
+    A Widget for displaying M2Ms in an autocomplete search input
+    instead in a <select> box.
+    """
+    # Set in subclass to render the widget with a different template
+    widget_template = 'widget/m2m_searchinput.html'
+    # Set this to the path of the search view
+    search_path = '../../../foreignkey_autocomplete/'
+ 
+    class Media:
+        css = {
+            'all': (settings.ADMIN_MEDIA_PREFIX + 'css/jquery.autocomplete.css',)
+        }
+        js = (
+            settings.ADMIN_MEDIA_PREFIX + 'js/jquery.js',
+            settings.ADMIN_MEDIA_PREFIX + 'js/jquery.bgiframe.min.js',
+            settings.ADMIN_MEDIA_PREFIX + 'js/jquery.ajaxQueue.js',
+            settings.ADMIN_MEDIA_PREFIX + 'js/jquery.autocomplete.js',
+        )
+    
+    def __init__(self, rel, search_fields, attrs=None):
+        self.search_fields = search_fields
+        super(ManyToManySearchInput, self).__init__(rel, attrs)
+
+    def label_for_value(self, value):
+        key = self.rel.get_related_field().name
+        objs = self.rel.to._default_manager.filter(**{key + '__in': value.split(',')})
+        return ','.join([str(o) for o in objs])
+        
+    
+    def render(self, name, value, attrs=None):
+        if attrs is None:
+            attrs = {}
+        output = [super(ManyToManySearchInput, self).render(name, value, attrs)]
+        if value:
+            value = ','.join([str(v) for v in value])
+        else:
+            value = ''
+        opts = self.rel.to._meta
+        app_label = opts.app_label
+        model_name = opts.object_name.lower()
+        related_url = '../../../%s/%s/' % (app_label, model_name)
+        params = self.url_parameters()
+        if params:
+            url = '?' + '&amp;'.join(['%s=%s' % (k, v) for k, v in params.items()])
+        else:
+            url = ''
+        if not attrs.has_key('class'):
+            attrs['class'] = 'vM2MRawIdAdminField'
+        # Call the TextInput render method directly to have more control
+        output = [forms.TextInput.render(self, name, value, attrs)]
+        if value:
+            label = self.label_for_value(value)
+        else:
+            label = u''
+        context = {
+            'url': url,
+            'related_url': related_url,
+            'admin_media_prefix': settings.ADMIN_MEDIA_PREFIX,
+            'search_path': self.search_path,
+            'search_fields': ','.join(self.search_fields),
+            'model_name': model_name,
+            'app_label': app_label,
+            'label': label,
+            'name': name,
+        }
+        output.append(render_to_string(self.widget_template or (
+            'templates/widget/%s/%s/m2m_searchinput.html' % (app_label, model_name),
+            'templates/widget/%s/m2m_searchinput.html' % app_label,
+            'templates/widget/m2m_searchinput.html',
+        ), context))
+        output.reverse()
+        return mark_safe(u''.join(output))
+
 class RelatedFieldWidgetWrapper(forms.Widget):
     """
     This class is a wrapper to a given widget to add the add icon for the