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.
Hi,
found this today. implemented it within minutes. Works like a charm.
Thanks for this!
Kai
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
Answering my own question, I found ModelAdmin.formfield_overrides, which provides a custom widget mechanism for change_list.
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!)
well, silly me, it seems to do that already!
looks good i will give it a try, i how it will work
python rocks!!!
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.
Here is my take on the idea: http://code.google.com/p/django-tagging-autocomplete/
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).
Interesting post. I was looking for this widget before implementing it from scratch. It works like a charm.
Thanks