← A WYSIWYM editor widget for ...

An autocomplete form widget for ... →

An autocomplete widget for django-tagging form fields

Today while working on django-page-cms I received a feature request by an user to enable the admin form field for tags of a page to automatically suggest input options — just like YouTube‘s or Google search field for example. Go ahead and try it if you don’t know what I mean.

Like my last article about a custom WYMEditor widget I will show you how such a autocomplete widget could be implemented by using jQuery and the great Autocomplete plugin which does all the hard work for us. Please download the sources of the autocomplete plugin if you don’t have it already. It includes everything we need to get started.

On the server side I’m going to use the well-known django-tagging which luckily also makes handling the tags a piece of cake.

The model

Although the whole process is entirely applicable to your own models, I’m going to use the following model as a matter of example.

from django.db import models
from tagging.fields import TagField

class Article(models.Model):
    title = models.CharField(max_length=50)
    content = models.TextField()
    tags = TagField()

It’s a rather simple model with a field that uses django-tagging‘s backend. It will later be rendered with your autocomplete widget.

The widget

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 thing to put something like this widget in the widgets.py of a Django app.

from django import forms
from django.db.models import get_model
from django.utils import simplejson
from django.utils.safestring import mark_safe
from tagging.models import Tag

Article = get_model('yourapp', 'article')

class AutoCompleteTagInput(forms.TextInput):
    class Media:
        css = {
            'all': ('jquery.autocomplete.css',)
        }
        js = (
            'lib/jquery.js',
            'lib/jquery.bgiframe.min.js',
            'lib/jquery.ajaxQueue.js',
            'jquery.autocomplete.js'
        )

    def render(self, name, value, attrs=None):
        output = super(AutoCompleteTagInput, self).render(name, value, attrs)
        page_tags = Tag.objects.usage_for_model(Article)
        tag_list = simplejson.dumps([tag.name for tag in page_tags],
                                    ensure_ascii=False)
        return output + mark_safe(u'''<script type="text/javascript">
            jQuery("#id_%s").autocomplete(%s, {
                width: 150,
                max: 10,
                highlight: false,
                multiple: true,
                multipleSeparator: ", ",
                scroll: true,
                scrollHeight: 300,
                matchContains: true,
                autoFill: true,
            });
            </script>''' % (name, tag_list))

The render method queries all tags saved in other articles instances and passes them as a JavaScript array to the autocomplete initializer in the HTML.

The form

Next the widget needs to be hooked up with the admin interface. One way to do that is to override the default form Django is using when generating the admin site. This form inherits from the default ModelForm class and has a tags field that uses your custom AutoCompleteTagInput widget and is used for the model field with the same name. Please save the following code in the 'forms.py' of your app.

from django import forms
from django.db.models import get_model
from tagging.forms import TagField
from yourapp.widgets import AutoCompleteTagInput

class ArticleAdminModelForm(forms.ModelForm):
    tags = TagField(widget=AutoCompleteTagInput(), required=False)

    class Meta:
        model = get_model('yourapp', 'article')

Assembling

Now all what’s left to do is to connect your new ModelForm class with the admin interface by creating a custom ModelAdmin to override the default. At the end just register your model with the default admin site.

from django.contrib import admin
from django.db.models import get_model
from yourapp.forms import ArticleAdminModelForm

class ArticleAdmin(admin.ModelAdmin):
    # ..
    form = ArticleAdminModelForm

admin.site.register(get_model('yourapp', 'article'), ArticleAdmin)

Django, Python Nov. 6, 2008, 1:50 a.m. comments (13)

comments

greg.newman Nov. 6, 2008, 2:25 a.m.

This is awesome Janis! Believe me or not but I was working with Brosner's oebfare project tonight and thought it would be nice to have a tagging autocomplete, maybe not for his project but for tagging in general. Thanks

Jannis Leidel Nov. 6, 2008, 2:33 a.m.

Thanks Greg, I'm glad you like it. Your's exactly the reason why I looked into making such a thing. I think there are others but didn't find it anywhere :)

omtv Nov. 6, 2008, 3:27 a.m.

Quite a useful script!
I'was doing something like that too. Now I don't have to build my own wheels. Thanks a lot.

Ahik Nov. 6, 2008, 7:52 a.m.

Thanks, Jannis.
Right to the point.

patrickk Nov. 6, 2008, 10:17 a.m.

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.
not sure if this is possible though ...

Jannis Leidel Nov. 6, 2008, 11:32 a.m.

@PATRICKK

Well, that'd be an awesome widget, of course. Sounds like a great weekend project :)

patrickk Nov. 6, 2008, 12:36 p.m.

@JANNIS

I was thinking about integrating something like this into Grappelli, see http://code.google.com/p/django-grappelli/
If you plan to work it out or if you have some code for testing, it´d be nice to let me know ...

thanks.

Martin Geber Nov. 7, 2008, 9:55 a.m.

Hey,

this is great! And some Additions:

1. ``from django import forms``'s missing in the ``widget.py``

2. How to make this 'pluggable'? "Article" independent?

Great tip.

Cheers.

Martin Geber Nov. 7, 2008, 10:11 a.m.

Hello,

Me again... Some more example "bugs":

Missing in widget:
1. from django import forms (as said above)
2. from django.utils.safestring import mark_safe

Line 22 in Widget, should be:
output = super(AutoCompleteTagInput, self).render(name, value, attrs)

Again: Cool thing!

Cheers.

Jannis Leidel Nov. 7, 2008, 10:56 a.m.

Thanks Martin, the bugs are now fixed.

Michael Buell Nov. 10, 2008, 6:52 p.m.

I'm getting a TemplateSyntaxError from the line:
tags = TagField(widget=AutoCompleteTagInput(), required=False)

unexpected keyword argument "widget"

If I get rid of it, it gives me the same error for "required"

I'm using Django 1.0 and the trunk version of django-tagging. I'm a bit new at this, so sorry if this should be obvious.

Michael Buell Nov. 11, 2008, 12:26 a.m.

Solved it. Accidentally typed tagging.fields instead of .forms

Having TagField() as a field and as a form is confusing, but it's standard Django practice, I guess.

Pranav Prakash Nov. 15, 2008, 3:11 p.m.

This is a fantastic example of hacking Django Forms. The script is quite useful. Thanks a lot.