Login

autocompleter with database query

Author:
bbolli
Posted:
July 5, 2007
Language:
Python
Version:
.96
Score:
1 (after 1 ratings)

This is an improvement of snippet 253 in that it supports database queries.

Implementing autocompletion for foreign keys takes a few steps:

1) Put the snippet above into <app>/widgets/autocomplete.py.

2) Create a view of your foreign key model (here: Donator) in <app>/donator/views.py:

from models import Donator
from widgets.autocomplete import autocomplete_response

def autocomplete(request):
    return autocomplete_response(
        request.REQUEST['text'], Donator, (
            'line_1', 'line_2', 'line_3', 'line_4',
            'line_5', 'line_6', 'line_7', 'line_8',
            '^zip_code', 'location'
        )
    )

This view returns the autocompletion result by searching the fields in the tuple. Each word from the form field must appear at least in one database field.

3) Create a URLconf that points to this new view.

4) In the form where you need the autocompletion, define the widget of the foreign key field as an instance of AutoCompleteField:

from widget.autocomplete import AutoCompleteField

field.widget = AutoCompleteField(
    url='/donator/autocomplete/'),
    options={'minChars': 3}
)

The url parameter is the URL connected to the view in step 3), the options dict is passed on to the Ajax.Autocompleter JavaScript object.

Links:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
"""Autocomplete support for foreign keys.
Requires the protoculous JavaScript library."""

# Widget

# originally from http://www.djangosnippets.org/snippets/253/

from django import newforms as forms
from django.newforms.widgets import TextInput, flatatt
from django.newforms.util import smart_unicode

from django.utils.html import escape

class AutoCompleteField(TextInput):
    def __init__(self, url='', options=None, attrs=None):
        self.url = url
        self.options = {'paramName': 'text'}
        if options:
            self.options.update(options)
        if attrs is None:
            attrs = {}
        self.attrs = attrs

    def render(self, name, value=None, attrs=None):
        final_attrs = self.build_attrs(attrs, name=name)
        if value:
            value = smart_unicode(value)
            final_attrs['value'] = escape(value)
        if not self.attrs.has_key('id'):
            final_attrs['id'] = 'id_%s' % name
        return (u'''<input type="text"%(attrs)s /><div class="autocomplete" id="box_%(name)s"></div>
<script type="text/javascript">new Ajax.Autocompleter('%(id)s', 'box_%(name)s', '%(url)s', %(options)s);</script>'''
        ) % {
            'attrs': flatatt(final_attrs),
            'name': name,
            'id': final_attrs['id'],
            'url': self.url,
            'options': plist_from_dict(self.options)
        }

def plist_from_dict(d):
    """Convert a Python dict into a JavaScript property list.
    The order of the items in the returned string is undefined."""
    return '{' + ', '.join(['%s: %r' % kv for kv in d.items()]) + '}'

# Query helper

from django.db.models import Model, Q

def autocomplete_query(text, model_or_qs, fields):
    """Return just those rows of model that contain
    all of the words of text in the fields string list.

    Instead of a Model class, you may also pass an initial QuerySet
    instance, e.g. for prefiltering or setting a different order.

    If a field name starts with a caret (^), the term is a prefix match
    (field LIKE "word%"), otherwise it is a full search (field LIKE "%word%")."""

    # check if a Model or a QuerySet was passed
    if issubclass(model_or_qs, Model):
        qs = model_or_qs.objects.all()
    else:
        qs = model_or_qs

    # pre-calculate the filter terms, they'll be user once per word
    terms = [
        f.startswith('^') and f[1:] + '__istartswith' or f + '__icontains'
        for f in fields
    ]

    # look for each word in every field -- this queryset represents
    # the statement
    #
    # SELECT * FROM model_or_qs
    # WHERE (field1 LIKE '%word1%' OR field2 LIKE '%word1%' OR ...)
    #   AND (field1 LIKE '%word2%' OR field2 LIKE '%word2%' OR ...)
    #   AND (field1 LIKE ...)
    #   ...
	#
    for word in text.split():
        q = Q()
        for term in terms:
            q = q | Q(**{term: word})
        qs = qs.filter(q)

    return qs

# View helper

from django.http import HttpResponse

def autocomplete_response(text, model_or_qs, fields, max_count=50):
    """Return the unordered list that is required by the Ajax.AutoCompleter.
    The field value will be the item's id; its __str__ is displayed
    along with it, but not stored."""

    qs = autocomplete_query(text, model_or_qs, fields)
    if qs.count() > max_count:
        result = [(0, _('Too many results, please enter more'))]
    else:
        result = [(d.id, '%s' % d) for d in qs]
    result = '\n'.join([
        '<li>%d<span class="informal">) %s</span></li>' % (id, escape(name))
        for id, name in result
    ])

    return HttpResponse('<ul>' + result + '</ul>')

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 10 months, 1 week ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 10 months, 2 weeks ago
  3. Serializer factory with Django Rest Framework by julio 1 year, 5 months ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 6 months ago
  5. Help text hyperlinks by sa2812 1 year, 6 months ago

Comments

Please login first before commenting.