← 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 (23)

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.

Kai Diefenbach June 4, 2009, 2:12 p.m.

Hi,

found this today. implemented it within minutes. Works like a charm.

Thanks for this!
Kai

Jeff Kowalczyk June 4, 2009, 11:50 p.m.

Thanks Jannis, it worked great for me as well. I tried this with Django trunk admin with the Tag field included in list_editable. The autocomplete widget does not render on the ChangeList. Is there a different mechanism for the change_list view to select widgets for rendering?

Jeff

Jeff Kowalczyk June 5, 2009, 2:30 a.m.

Answering my own question, I found ModelAdmin.formfield_overrides, which provides a custom widget mechanism for change_list.

Paul Oct. 12, 2009, 5:13 p.m.

Thanks, this is great.

For tagging though it really needs to be able to autocomplete more than one tag.

Something like this tokenised autocomplete plugin would be good:
http://loopj.com/2009/04/25/jquery-plugin-tokenizing-autocomplete-text-entry/

But then that plugin itself needs some hacking to work with local instead of ajax data. Also it's designed to submit a list of ids rather than the raw tag strings.

If I get round to hacking it up I'll post the code... (hoping someone else already has!)

Paul Oct. 13, 2009, 6:59 p.m.

well, silly me, it seems to do that already!

joel Nov. 25, 2009, 3:20 a.m.

looks good i will give it a try, i how it will work
python rocks!!!

Richard Nov. 25, 2009, 10:34 p.m.

Great idea Jannis. I have used your code in one of my own apps where I have a number of models tagged.

I made the class AutoCompleteTagInput() a little more generic by wrapping it in a function which takes a model parameter.
def make_AutoCompleteTagInput(tagged_object)
class AutoCompleteTagInput(forms.TextInput):
...
def render(self, name, value, attrs=None):
...
page_tags = tag.objects.usage_for_model(tagged_object)

Then calling the make_AutoCompleteTagInput() from the forms.py file is the only other change needed:
tags = TagField(widget=make_AutoCompleteTagInput(my_model), required=False)

It makes the widget.py file a little more DRY when using tagging on more than one model. There is probably a smarter implementation with metaclasses.

Ludwik Nov. 29, 2009, 6:48 p.m.

Here is my take on the idea: http://code.google.com/p/django-tagging-autocomplete/

Ludwik Nov. 29, 2009, 6:54 p.m.

Oh, and I should probably note that I created mine before I've seen your post, but the main difference between the two is that my app use AJAX to fetch the tags in the background. So yours is probably faster for sites that uses less tags, while mine is a better choice for projects having more than 20 or 30 different tags in their database (or planing to do so).

gregory desvaux Jan. 14, 2010, 10:51 p.m.

Interesting post. I was looking for this widget before implementing it from scratch. It works like a charm.
Thanks