February 16th, 2012

Add an AJAX powered quick search to any django site

Tutorial for: django-selectable

Requirements:

Do you have a Django website with a large Knowledgebase? Do you want to make this knowledgebase easily searchable from any page on your site with a fancy jQuery auto-complete box? This tutorial has your solution!

This tutorial assumes that you know how to use Django and have an existing website and a compatible data model.

The first thing you will want to do is install django-selectable, you can click the package link above to locate it's download page or simply use pip. Your model will need to have a field which stores the entire content of the article you would like to be searchable. Installing django-selectable is as simple as adding a simple entry to your urls.py:

urlpatterns = patterns('',
  # Other patterns go here
  (r'^selectable/', include('selectable.urls')),
)

In order to make this all work, you will need to create a couple files and edit your base.html template to add additional javascript:

<link type="text/css" href="/css/redmond/jquery-ui-1.8.16.custom.css" rel="stylesheet" />
<link type="text/css" href="/css/dj.selectable.css" rel="stylesheet" />
<script type="text/javascript" src="/js/jquery-1.6.2.min.js"></script>
<script type="text/javascript" src="/js/jquery-ui-1.8.16.custom.min.js"></script>
<script type="text/javascript" src="/js/jquery.dj.selectable.js"></script>

If you would like your users to be-able to click on the options in the drop down to automatically move to the article, use this jQuery code:

$(function(){
  $('[id=id_q]').bind('autocompleteselect', function(event, ui){
    $(this).val(ui.item.value);
    $(this).parents("form").submit();
  });
});

In order for django-selectable to work, there needs to be a lookups.py file in your apps directory. This file contains a class which describes how the queryset is generated to perform a lookup. Here is a sample lookups.py to get you started:

from selectable.base import ModelLookup
from selectable.registry import registry, LookupAlreadyRegistered
from kbase.models import Entry

class EntryLookup(ModelLookup):
  model = Entry
  search_field = 'content__contains'

try:
  registry.register(EntryLookup)
except LookupAlreadyRegistered:
  pass

There is a very strange bug in django-selectable, where it attempts to import the lookups.py file twice, and I'm not entirely sure why. Next we need a form, this can live in your apps directory forms.py module, here is a working example to get you started:

from django import forms
from selectable.forms import AutoCompleteWidget
from kbase.lookups import EntryLookup

class KbaseForm(forms.Form):
  q = forms.CharField(
    label='',
    widget=AutoCompleteWidget(EntryLookup),
    required=False,
  )

I'll explain what this does, it creates a form which we will render later. It assigns a widget to the CharField, which is the django-selectable auto-complete jQuery widget. We tell the widget the location of the lookup we created earlier. The next thing we need to do is render the form for the user can actually interact with it. Since we would like to be-able to reference this on any page we would like, and not necessarily have it on every single page, we will create a template tag. In order to do this, you will need to create a new directory in your apps directory called templatetags, and inside here, touch a file called __init__.py to make the directory a python package. I have a file called kbase_tags.py, which I stored this template tag inside of, and here is the code for it:

from django import template
from kbase.forms import KbaseForm

register = template.Library()

@register.inclusion_tag('kbase/kbase_lookup.html')
def kbase_lookup():
  return {'form': KbaseForm()}

Almost done, now all we need to create is the template for the form:

<form action="{% url kbase_search %}" method="get">
{{form}}
</form>

The template for the form is very simple and very portable throughout your website. Here is a sample view to get you started with adding the finishing touches to make it all come together nicely:

def search(req):
  if 'q' in req.GET:
    entry_list = Entry.objects.filter(
      Q(title__contains=req.GET['q']) | 
      Q(content__contains=req.GET['q'])
    )
    if entry_list.count() == 1:
      return redirect(entry_list[0])
    return render_to_response("kbase/search.html",
      {'entry_list':entry_list})
  else:
    return render_to_response("kbase/search_error.html",
      {'error':'Search query missing.'})

It is good to be backwards compatible and support older browsers too, or users who just have JavaScript disabled. This view will also check to see if a single result is being returned, if so, then just redirect to that result instead of a showing a list with one result.

That's all there is to it to using django-selectable in a nutshell. This definitely adds an element of interactivity to a website, and also allows users to perform searches on your content with very little effort and instant results.

Feb. 22, 2012, 6:50 p.m. - Mark Lavin

The double import/LookupAlreadyRegistered error is in part related to the magic that manage.py does to your Python path (see http://groups.google.com/group/django-developers/browse_thread/thread/44b70a37ff73298b). It allows you to import a module which is inside your project with both `from myproject.myapp import models` or `from myapp import models`. Thankfully this hack is being removed in the upcoming 1.4 release. I really enjoyed this tutorial. I'm also glad to see django-selectable working side by side with Twitter Bootstrap. Best, Mark

Feb. 23, 2012, 12:07 a.m. - Kevin Veroneau

Thank you for your comment Mark. I just realized that you are actually the maintainer of django-selectable. Thank you for creating such a great django app, and keeping it very plug-able into projects. It works very nicely alongside of Twitter Bootstrap. The only problem I ran up against was that I cannot add custom html tag properties using the AutoCompleteWidget which Twitter Bootstrap requires. So instead of rendering the form django-style, I manually put in the INPUT tag with the appropriate tag properties. The extra properties are "class" and "placeholder". It would work fine without, but wouldn't keep the consistent look. Is there a way to add custom tag properties using django.forms? I would hope at least adding a "class" property should be doable. Anyways, thank you again.

Feb. 23, 2012, 5:36 p.m. - Mark Lavin

Yep it's me. I was cruising around looking for people complaining about my project and stumbled onto your very nice post. You can add attributes to the widgets (this isn't specific to django-selectable) by passing them as attrs such as selectable.AutoCompleteWidget(FruitLookup, attrs={'class': 'foo', 'placeholder': 'bar'}). Here are the official docs https://docs.djangoproject.com/en/1.3/ref/forms/widgets/#django.forms.Widget.attrs It's kind of ugly to me because you are basically writing HTML in Python. There are some other projects out there that are made to help with rendering forms to work with Bootstrap like https://github.com/tzangms/django-bootstrap-form. They should work just fine with django-selectable and if they don't just open and issue.

July 1, 2012, 6:34 p.m. - Tuna VARGI

Thanks for sharing such a valuable method. I have been using selectable autocomplete for a while. I decided to use your method for search with selectable. But when use whole method above in terminal i get error such "[01/Jul/2012 18:29:14] "GET /selectable/tanim-descriptionlookup/?term=yyyyyyy&timestamp=1341185354816 HTTP/1.1" 500 103788" I have created a yyyyyyy object. But having trouble to see the object in the autocomplete search bar. Do you have any idea? Should I use a different view or template? Thanks Tuna

Python Powered | © 2012-2014 Kevin Veroneau