In my last post I showed you how to write your own widget for django-tagging form fields that uses the nifty jQuery autocomplete plugin to simplify entering tags. This time I’d like to take on a suggestion PatricK made in the comments:
my question is, if this could be done on a more abstract level for enhancing relations. when having a foreignkey, you get the browse-icon (lens) right near the input-field. it´d be awesome to search the relations just by typing something into the field … the autocomplete-functionality should then search the related entries on the basis of the defined search-fields.
Well, this is what I’m going to do today :)
Preface
Thanks to the way Django’s forms work, the following implementation should be applicable to your own Django code — but please bare in mind that this isn’t a copy and paste how-to. I rather encourage you to learn about the flexibility of forms, fields and widgets and how it’s used in the automatic Admin interface. You’ll be able to — and sometimes required — to customize the code posted below.
As an example I’m going to use the message system that is included in Django’s auth contrib app. As far as I know Django doesn’t provide an admin representation for the Message class by default because it’s supposed to be used programmatically (and is used by the admin itself, too). The model consists of two fields, a ForeignKey to a User and a text field for the message.
Use case
Since ForeignKey fields are rendered as select fields by default there is a good chance that they become unusable if there are a lot of possible choices. So in case you have a lot of users in your database for example it might be a bit cumbersome to choose a user from the ForeignKey dropdown menu. Django provides an option that allows specifying a raw_id_fields attribute in your ModelAdmin subclass that will tell Django to render the ForeignKey as a simple input taking the primary key of the refered object. The actual selection is done by clicking the browse icon next to the field and browsing the model with the standard admin interface in a popup. What I want to provide here though is a way to do that directly in place, in the original form.
Widget
We are going to use the jQuery and the great Autocomplete plugin again which does all the hard work. Please download the sources of the autocomplete plugin if you don’t have it already. It includes everything we need to get started. Copy all files and folders of the extracted jQuery Autocomplete zip file to the directory you specified in the MEDIA_ROOT setting. It should then contain: ‘jquery.autocomplete.js’, ‘jquery.autocomplete.css’ and a ‘lib’ directory with the other necessary files. In case you arrange your files differently don’t hesitate to change the paths below in the inner Media class.
It has been proven to be a good idea to put something like the follwing widget in the widgets.py of your Django app, but any place will do if you change the import paths.
from django import forms from django.conf import settings from django.utils.safestring import mark_safe from django.utils.text import truncate_words class ForeignKeySearchInput(forms.HiddenInput): """ A Widget for displaying ForeignKeys in an autocomplete search input instead in a <select> box. """ class Media: css = { 'all': ('jquery.autocomplete.css',) } js = ( 'lib/jquery.js', 'lib/jquery.bgiframe.min.js', 'lib/jquery.ajaxQueue.js', 'jquery.autocomplete.js' ) def label_for_value(self, value): key = self.rel.get_related_field().name obj = self.rel.to._default_manager.get(**{key: value}) return truncate_words(obj, 14) def __init__(self, rel, search_fields, attrs=None): self.rel = rel self.search_fields = search_fields super(ForeignKeySearchInput, self).__init__(attrs) def render(self, name, value, attrs=None): if attrs is None: attrs = {} rendered = super(ForeignKeySearchInput, self).render(name, value, attrs) if value: label = self.label_for_value(value) else: label = u'' return rendered + mark_safe(u''' <style type="text/css" media="screen"> #lookup_%(name)s { padding-right:16px; background: url( %(admin_media_prefix)simg/admin/selector-search.gif ) no-repeat right; } #del_%(name)s { display: none; } </style> <input type="text" id="lookup_%(name)s" value="%(label)s" /> <a href="#" id="del_%(name)s"> <img src="%(admin_media_prefix)simg/admin/icon_deletelink.gif" /> </a> <script type="text/javascript"> if ($('#lookup_%(name)s').val()) { $('#del_%(name)s').show() } $('#lookup_%(name)s').autocomplete('../search/', { extraParams: { search_fields: '%(search_fields)s', app_label: '%(app_label)s', model_name: '%(model_name)s', }, }).result(function(event, data, formatted) { if (data) { $('#id_%(name)s').val(data[1]); $('#del_%(name)s').show(); } }); $('#del_%(name)s').click(function(ele, event) { $('#id_%(name)s').val(''); $('#del_%(name)s').hide(); $('#lookup_%(name)s').val(''); }); </script> ''') % { 'search_fields': ','.join(self.search_fields), 'admin_media_prefix': settings.ADMIN_MEDIA_PREFIX, 'model_name': self.rel.to._meta.module_name, 'app_label': self.rel.to._meta.app_label, 'label': label, 'name': name, } </select>
This widget renders ForeignKeys as hidden inputs and adds a second input field to search in the related model using asynchronous requests, sometimes refered to as AJAX. Once the user selects a result the widget will automatically set the hidden field to the primary key. When editing an existing entry this will automatically populate the search field with the correct verbose value.
Admin integration
As already stated we are going to create an admin class for the Message model to enable admins to create messages for other users that will be displayed the next time they use the admin (or any other site that uses User.get_and_delete_messages).
The best place for the admin class is an admin.py file in one of your own app directories because it’s then picked up by Django’s autodiscover() function.
import operator from django.db import models from django.contrib.auth.models import Message from django.http import HttpResponse, HttpResponseNotFound from django.contrib import admin from django.db.models.query import QuerySet from django.utils.encoding import smart_str from yourapp.widgets import ForeignKeySearchInput class MessageAdmin(admin.ModelAdmin): list_display = ('user', 'message') related_search_fields = { 'user': ('username', 'email'), } def __call__(self, request, url): if url is None: pass elif url == 'search': return self.search(request) return super(MessageAdmin, self).__call__(request, url) def search(self, request): """ Searches in the fields of the given related model and returns the result as a simple string to be used by the jQuery Autocomplete plugin """ query = request.GET.get('q', None) app_label = request.GET.get('app_label', None) model_name = request.GET.get('model_name', None) search_fields = request.GET.get('search_fields', None) if search_fields and app_label and model_name and query: def construct_search(field_name): # use different lookup methods depending on the notation if field_name.startswith('^'): return "%s__istartswith" % field_name[1:] elif field_name.startswith('='): return "%s__iexact" % field_name[1:] elif field_name.startswith('@'): return "%s__search" % field_name[1:] else: return "%s__icontains" % field_name model = models.get_model(app_label, model_name) qs = model._default_manager.all() for bit in query.split(): or_queries = [models.Q(**{construct_search( smart_str(field_name)): smart_str(bit)}) for field_name in search_fields.split(',')] other_qs = QuerySet(model) other_qs.dup_select_related(qs) other_qs = other_qs.filter(reduce(operator.or_, or_queries)) qs = qs & other_qs data = ''.join([u'%s|%s\n' % (f.__unicode__(), f.pk) for f in qs]) return HttpResponse(data) return HttpResponseNotFound() def formfield_for_dbfield(self, db_field, **kwargs): """ Overrides the default widget for Foreignkey fields if they are specified in the related_search_fields class attribute. """ if isinstance(db_field, models.ForeignKey) and \ db_field.name in self.related_search_fields: kwargs['widget'] = ForeignKeySearchInput(db_field.rel, self.related_search_fields[db_field.name]) return super(MessageAdmin, self).formfield_for_dbfield(db_field, **kwargs) admin.site.register(Message, MessageAdmin)
So this code does several things:
- It inherits from the default ModelAdmin to be able to override the default widget.
-
A new
related_search_fieldsclass attribute that is a mapping between lowercase model names and a list of fields to searched in. It uses the same syntax as the search_fields attribute to speed up the lookups or restrict them if needed. The following example would only match the beginning of the first and last name of users:
related_search_fields = { 'user': ('^first_name', '^last_name'), }
-
It overrides the
__call__method to add additional URL handler to the admin to be used by the widget for searching. It will be waiting for requests on/<root-admin-path>/auth/message/search/. -
The
searchview that constructs the querysets depending on the provided model and search fields and returns the data as a simple string. The Autocomplete plugin can parse that and will display it in the selection. -
A
formfield_for_dbfieldmethod that will override the widget of any ForeignKey field whose name is specified in the related_search_fields class attribute.
Once assembled the result will mostly look like that:
Please note that I use a different stylesheet for the Autocomplete plugin here.

