Skip to content

Commit c4dd344

Browse files
committed
[soc2009/admin-ui] M2M autocomplete, modeled much like the FK autocomplete.
git-svn-id: http://code.djangoproject.com/svn/django/branches/soc2009/admin-ui@11427 bcc190cf-cafb-0310-a4f2-bffc1f526a37
1 parent 2919305 commit c4dd344

File tree

3 files changed

+114
-0
lines changed

3 files changed

+114
-0
lines changed

django/contrib/admin/options.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,9 @@ def formfield_for_manytomany(self, db_field, request=None, **kwargs):
163163
if db_field.name in self.raw_id_fields:
164164
kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.rel)
165165
kwargs['help_text'] = ''
166+
elif db_field.name in self.autocomplete_fields:
167+
kwargs['widget'] = widgets.ManyToManySearchInput(db_field.rel,
168+
self.autocomplete_fields[db_field.name])
166169
elif db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)):
167170
kwargs['widget'] = widgets.FilteredSelectMultiple(db_field.verbose_name, (db_field.name in self.filter_vertical))
168171

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{% load i18n %}
2+
<textarea id="lookup_{{ name }}" style="display:none;">{{ label }}</textarea>
3+
<a href="{{ related_url }}{{ url }}" class="related-lookup" id="lookup_id_{{ name }}" onclick="return showRelatedObjectLookupPopup(this);">
4+
<img src="{{ admin_media_prefix }}img/admin/selector-search.gif" width="16" height="16" alt="{% trans "Lookup" %}" />
5+
</a>
6+
<script type="text/javascript">
7+
$(document).ready(function() {
8+
// Show lookup input
9+
$("#lookup_{{ name }}").show();
10+
11+
function lookup(query) {
12+
$.get('{{ search_path }}', {
13+
'search_fields': '{{ search_fields }}',
14+
'app_label': '{{ app_label }}',
15+
'model_name': '{{ model_name }}',
16+
'object_pk': query
17+
}, function(data){
18+
$('#lookup_{{ name }}').val(data);
19+
});
20+
};
21+
$('#lookup_{{ name }}').autocomplete('{{ search_path }}', {
22+
extraParams: {
23+
'search_fields': '{{ search_fields }}',
24+
'app_label': '{{ app_label }}',
25+
'model_name': '{{ model_name }}'
26+
},
27+
multiple: true,
28+
mustMatch: true,
29+
autoFill: true
30+
}).result(function(event, data, formatted) {
31+
if (data) {
32+
$('#id_{{ name }}').val($('#id_{{ name }}').val() + data[1] + ",");
33+
}
34+
});
35+
});
36+
</script>

django/contrib/admin/widgets.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,81 @@ def _has_changed(self, initial, data):
263263
return True
264264
return False
265265

266+
class ManyToManySearchInput(ManyToManyRawIdWidget):
267+
"""
268+
A Widget for displaying M2Ms in an autocomplete search input
269+
instead in a <select> box.
270+
"""
271+
# Set in subclass to render the widget with a different template
272+
widget_template = 'widget/m2m_searchinput.html'
273+
# Set this to the path of the search view
274+
search_path = '../../../foreignkey_autocomplete/'
275+
276+
class Media:
277+
css = {
278+
'all': (settings.ADMIN_MEDIA_PREFIX + 'css/jquery.autocomplete.css',)
279+
}
280+
js = (
281+
settings.ADMIN_MEDIA_PREFIX + 'js/jquery.js',
282+
settings.ADMIN_MEDIA_PREFIX + 'js/jquery.bgiframe.min.js',
283+
settings.ADMIN_MEDIA_PREFIX + 'js/jquery.ajaxQueue.js',
284+
settings.ADMIN_MEDIA_PREFIX + 'js/jquery.autocomplete.js',
285+
)
286+
287+
def __init__(self, rel, search_fields, attrs=None):
288+
self.search_fields = search_fields
289+
super(ManyToManySearchInput, self).__init__(rel, attrs)
290+
291+
def label_for_value(self, value):
292+
key = self.rel.get_related_field().name
293+
objs = self.rel.to._default_manager.filter(**{key + '__in': value.split(',')})
294+
return ','.join([str(o) for o in objs])
295+
296+
297+
def render(self, name, value, attrs=None):
298+
if attrs is None:
299+
attrs = {}
300+
output = [super(ManyToManySearchInput, self).render(name, value, attrs)]
301+
if value:
302+
value = ','.join([str(v) for v in value])
303+
else:
304+
value = ''
305+
opts = self.rel.to._meta
306+
app_label = opts.app_label
307+
model_name = opts.object_name.lower()
308+
related_url = '../../../%s/%s/' % (app_label, model_name)
309+
params = self.url_parameters()
310+
if params:
311+
url = '?' + '&amp;'.join(['%s=%s' % (k, v) for k, v in params.items()])
312+
else:
313+
url = ''
314+
if not attrs.has_key('class'):
315+
attrs['class'] = 'vM2MRawIdAdminField'
316+
# Call the TextInput render method directly to have more control
317+
output = [forms.TextInput.render(self, name, value, attrs)]
318+
if value:
319+
label = self.label_for_value(value)
320+
else:
321+
label = u''
322+
context = {
323+
'url': url,
324+
'related_url': related_url,
325+
'admin_media_prefix': settings.ADMIN_MEDIA_PREFIX,
326+
'search_path': self.search_path,
327+
'search_fields': ','.join(self.search_fields),
328+
'model_name': model_name,
329+
'app_label': app_label,
330+
'label': label,
331+
'name': name,
332+
}
333+
output.append(render_to_string(self.widget_template or (
334+
'templates/widget/%s/%s/m2m_searchinput.html' % (app_label, model_name),
335+
'templates/widget/%s/m2m_searchinput.html' % app_label,
336+
'templates/widget/m2m_searchinput.html',
337+
), context))
338+
output.reverse()
339+
return mark_safe(u''.join(output))
340+
266341
class RelatedFieldWidgetWrapper(forms.Widget):
267342
"""
268343
This class is a wrapper to a given widget to add the add icon for the

0 commit comments

Comments
 (0)