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)
comments
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
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 :)
Quite a useful script!
I'was doing something like that too. Now I don't have to build my own wheels. Thanks a lot.
Thanks, Jannis.
Right to the point.
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 ...
@PATRICKK
Well, that'd be an awesome widget, of course. Sounds like a great weekend project :)
@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.
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.
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.
Thanks Martin, the bugs are now fixed.
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.
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.
This is a fantastic example of hacking Django Forms. The script is quite useful. Thanks a lot.