views:

555

answers:

1

Hi,

I found a nice snippet which seems to be a pretty nice and generic way to get autocomplete to your widgets using jquery autocomplete: link

http://www.djangosnippets.org/snippets/233/

Unfortunately I did not found a complete example implementing this snippet and it seems that I do not understand it in detail to be able to implement it by myself :D

Therefore I'm searching some help to implement it. Some questions would be:

_1: When I assign the JQueryAutoComplete widget to a form field. How do I tell the field that it should use the values from a view, or how do i tell the field to use the values from a list.

Should work somehow like this:

class ProjectForm(forms.ModelForm):

    auto_test = forms.CharField(max_length=10, widget=JQueryAutoComplete())


    class Meta:
        model = Project
        exclude = ('created_by',)

class Project(models.Model):
    name = models.CharField(max_length=100)

    project_owner_externally = models.ForeignKey(Contact, null=True, related_name='project_owner_externally')

_2: In the description of the snippet the author wrote: "to be implemented: - store the pk value into an hidden field - " - How can I implement a hidden field in my form?

_3: Is there a way to ship the widget with the needed jquery source? Or do I have to set the jquery links in each page using this widget?

Edit: Part of the solution:

Ok I made it some steps further to the solution.

I had to make some small changes to the snippet:

from django import forms
from django.forms.widgets import flatatt
from django.forms.util import smart_unicode
from django.utils.html import escape
from django.utils.simplejson import JSONEncoder
from django.utils.safestring import mark_safe

class JQueryAutoComplete(forms.TextInput):
    def __init__(self, source, options={}, attrs={}):
        """source can be a list containing the autocomplete values or a
        string containing the url used for the XHR request.

        For available options see the autocomplete sample page::
        http://jquery.bassistance.de/autocomplete/"""

        self.options = None
        self.attrs = {'autocomplete': 'off'}
        self.source = source
        if len(options) > 0:
            self.options = JSONEncoder().encode(options)

        self.attrs.update(attrs)

    def render_js(self, field_id):
        if isinstance(self.source, list):
            source = JSONEncoder().encode(self.source)
        elif isinstance(self.source, str):
            source = "'%s'" % escape(self.source)
        else:
            raise ValueError('source type is not valid')

        options = ''
        if self.options:
            options += ',%s' % self.options

        return u'$(\'#%s\').autocomplete(%s%s);' % (field_id, source, options)

    def render(self, name, value=None, attrs=None):
        final_attrs = self.build_attrs(attrs, name=name)
        if value:
            final_attrs['value'] = escape(smart_unicode(value))

        if not self.attrs.has_key('id'):
            final_attrs['id'] = 'id_%s' % name 

# I added here the mark_safe in order to prevent escaping:
return mark_safe(u'''<input type="text" %(attrs)s/>
        <script type="text/javascript"><!--//
        %(js)s//--></script>
        ''' % {
            'attrs' : flatatt(final_attrs),
            'js' : self.render_js(final_attrs['id']),
        })

With this snipped placed into a widget you can assign the widget to a form field like this:

class ProjectForm(forms.ModelForm):

    project_owner_externally = forms.CharField(max_length=25, widget=JQueryAutoComplete('/pm/contact_autocomplete'))

    class Meta:
        model = Project
        exclude = ('created_by',)

    class Media:
            js = (
                settings.MEDIA_URL + "js/jquery.autocomplete.js",
            )
            css = {
                'screen': (settings.MEDIA_URL + "css/jquery.autocomplete.css",),
            }

Note the url reference in the widget instantiation. widget=JQueryAutoComplete('/pm/contact_autocomplete') . This url has to be defined in your urls config. In my case this url points to a view which returns the filtered contacts like this:

from django.http import HttpResponse, HttpResponseBadRequest
from django.views.decorators.cache import cache_page
from crm.models import Contact


def contact_autocomplete(request):
    print "holy jquery s..."
    def iter_results(results):
        if results:
            for r in results:
                yield '%s|%s\n' % (r.first_name, r.id)

    if not request.GET.get('q'):
        return HttpResponse(mimetype='text/plain')

    q = request.GET.get('q')
    limit = request.GET.get('limit', 15)
    try:
        limit = int(limit)
    except ValueError:
        return HttpResponseBadRequest() 

    contacts = Contact.objects.filter(first_name__startswith=q)[:limit]
    print contacts
    return HttpResponse(iter_results(contacts), mimetype='text/plain')

And tada.. the form does show now all the contacts found in your input field.

This works nice if you want to fill in a char field with some live-search values.

The question for me is now how do I handle this stuff in order to not only put the first_name of the result in the text box, but to create an hidden field which holds the id of the contact. Still an open question for me is how I can handle this stuff for a ForeignKey field, since the project_owner_externally is a ForeignKey field.

project_owner_externally = models.ForeignKey(Contact, null=True, related_name='project_owner_externally')
A: 

Ok I found a way to run this snippet. The solution is pretty hackish but it works. Would be great if somebody could post a neater way to do auto-completion for foreign key fields.

The use-case for this solution is. We have a contact model with about 2000 contacts. We have projects and we want to assign a project responsible person to a project. This person is selected out of the 2000 contacts. Therefore a simple select dropdown can not be used. Thus i tried to get some auto-completion up and running.

I try to explain it here.

These are the two models:

class Project(models.Model):
    name = models.CharField(max_length=100)
    project_manager_externally = models.ForeignKey(Contact, null=True, related_name='project_manager_externally')

class Contact(models.Model):
    first_name = models.CharField(max_length=200)
    last_name = models.CharField(max_length=200)

This is the form. The ProjectForm uses the JQueryAutoComplete widget which is from of http://www.djangosnippets.org/snippets/233/ , adapted to serve my needs. Note that the field project_manager_externally is the model value that we want to assign with the foreignKey to a Contact model. We hide this field from the user with widget=forms.HiddenInput(). The autocomplete.js will update this field for the user, when the user selects an entry from the autopopulated list in project_manager_externally_auto_complete. Note that I limit the choices to all Contacts available with choices=contact_choices(). Since the JQueryAutoComplete renders as an text input field, the user can insert anything he/she wants. With adding the choices option to the field I leave the validation rules up to django.

The last open question was now. What happens when the user inserts a valid name, but does not select it over the autocomplete.js dropdown. Since the autocomplete.js updates the project_manager_externally with the correct contact pk, the contact pk would not be updated and therefore we would not save the new input. Therefore I overwrite the clean method. First I get the Contact object with cleaned_data.get("project_manager_externally"). If the Contact object has been updated with the autocomplete.js I can be sure that first_name and last_name matches with the value in project_manager_externally_auto_complete. Like you see below in the JQueryAutoComplete widget the input field is populated with first_name and last_name. If these do not match I know that the user inserted the name of the contact directly without using the autocomplete dropdown.

from crm.models import Contact
from project_management.models import Project
from ajax_filtered_fields.forms import ForeignKeyByLetter
from django.forms.util import ErrorList

def contact_choices():    
    for contact in Contact.objects.all():
        the_value = contact.first_name + " "  + contact.last_name
        yield (the_value, the_value)

class ProjectForm(forms.ModelForm):

    project_manager_externally_auto_complete = forms.ChoiceField(choices=contact_choices(), label='fake p manager extern', widget=JQueryAutoComplete('/pm/contact_autocomplete'))
    project_manager_externally = forms.ModelChoiceField(queryset=Contact.objects.all(), widget=forms.HiddenInput())

    def clean(self):
        cleaned_data = self.cleaned_data
        project_manager_externally_auto_complete = cleaned_data.get("project_manager_externally_auto_complete")
        project_manager_externally = cleaned_data.get("project_manager_externally")

        if project_manager_externally_auto_complete and project_manager_externally:
            the_value = project_manager_externally.first_name + " "  + project_manager_externally.last_name
            if the_value != project_manager_externally_auto_complete:
                msg = u"Please select a value with the drop-down!"
                self._errors["project_manager_externally_auto_complete"] = ErrorList([msg])

        return cleaned_data

    class Meta:
        model = Project
        exclude = ('created_by',)

   class Media:
            js = (
                settings.MEDIA_URL + "js/jquery.autocomplete.js",
            )
            css = {
                'screen': (settings.MEDIA_URL + "css/jquery.autocomplete.css",),
            }

The adapted JQueryAutoComplete widget: I just adapted the js return stuff and added this part. After a successfull autocomplete call the result function will replace the value attribut with item[1], whereas item[1] is the pk of the contact selected. See the view below for further details about item[1]

from django import forms
from django.forms.widgets import flatatt
from django.forms.util import smart_unicode
from django.utils.html import escape
from django.utils.simplejson import JSONEncoder
from django.utils.safestring import mark_safe

class JQueryAutoComplete(forms.TextInput):
    def __init__(self, source, options={}, attrs={}):
        """source can be a list containing the autocomplete values or a
        string containing the url used for the XHR request.

        For available options see the autocomplete sample page::
        http://jquery.bassistance.de/autocomplete/"""

        self.options = None
        self.attrs = {'autocomplete': 'off'}
        self.source = source
        if len(options) > 0:
            self.options = JSONEncoder().encode(options)

        self.attrs.update(attrs)

    def render_js(self, field_id):
        if isinstance(self.source, list):
            source = JSONEncoder().encode(self.source)
        elif isinstance(self.source, str):
            source = "'%s'" % escape(self.source)
        else:
            raise ValueError('source type is not valid')

        options = ''
        if self.options:
            options += ',%s' % self.options

        #in order to reference to the original field we remove the _auto_complete value from the field_id
        #note that this is a convention. in order to make this work, each form field using this widget has to be named
        #after the field for which it handles the foreign keys + the string '_auto_complete'
        field_id_origin = field_id.replace('_auto_complete', '')

        return u'$(\'#%s\').autocomplete(%s%s).result(function(event, item) {$(\'#%s\').attr("value", item[1]);});' % (field_id, source, options, field_id_origin)  
    def render(self, name, value=None, attrs=None):
        final_attrs = self.build_attrs(attrs, name=name)
        if value:
            final_attrs['value'] = escape(smart_unicode(value))

        if not self.attrs.has_key('id'):
            final_attrs['id'] = 'id_%s' % name    


        return mark_safe(u'''<input type="text" %(attrs)s/>
        <script type="text/javascript"><!--//
        %(js)s//--></script>
        ''' % {
            'attrs' : flatatt(final_attrs),
            'js' : self.render_js(final_attrs['id']),
        })

The view:

This is also an adapted part of the snippet. The important part here is: yield '%s %s|%s\n' % (r.first_name, r.last_name, r.id) The only thing that you have to understand here is that this line is the reason that you can access the contact id with item[1] in the result branch of the autocomplete call above.

from django.http import HttpResponse, HttpResponseBadRequest
from django.views.decorators.cache import cache_page
from crm.models import Contact
from django.db.models import Q

@cache_page(120)
def contact_autocomplete(request):
    print "holy jquery shit"
    def iter_results(results):
        if results:
            for r in results:
                yield '%s %s|%s\n' % (r.first_name, r.last_name, r.id)

    if not request.GET.get('q'):
        return HttpResponse(mimetype='text/plain')

    q = request.GET.get('q')
    limit = request.GET.get('limit', 15)
    try:
        limit = int(limit)
    except ValueError:
        return HttpResponseBadRequest() 

    contacts = Contact.objects.filter(Q(first_name__istartswith=q) | Q(last_name__istartswith=q))
    return HttpResponse(iter_results(contacts), mimetype='text/plain')

I hope this will help somebody implementing an autocomplete solution for foreign key fields. I know that there are less hackish solutions out there, but I did not find one serving my needs today. Thus I came up with my own solution :-)

Actually if somebody has an alternative for handling foreign keys with a lot of values it would be great to get a post to another solution.

Tom Tom