
Enables editing of `ForeignKey`, `ManyToMany` and simple text fields using the Autocomplete - `jQuery` plugin.

django-ajax-selects will work in any normal form as well as in the admin.

==User experience==

selecting...

http://crucial-systems.com/crucialwww/uploads/posts/selecting.png

selected.

http://crucial-systems.com/crucialwww/uploads/posts/selected.png

The user is presented with a text field.  They type a search term or a few letters of a name they are looking for, an ajax request is sent to the server, a search channel returns possible results.  Results are displayed as a drop down menu.  When an item is selected it is added to a display area just below the text field.

A single view services all of the ajax search requests, delegating the searches to named 'channels'.

A channel is a simple class that handles the actual searching, defines how you want to treat the query input (split first name and last name, which fields to search etc.) and returns id and formatted results back to the view which sends it to the browser.

For instance the search channel 'contacts' would search for Contact models.  The class would be named ContactLookup.  This channel can be used for both AutoCompleteSelect ( foreign key, single item ) and AutoCompleteSelectMultiple (many to many) fields. 

Simple search channels can also be automatically generated, you merely specify the model and the field to search against (see examples below).

Custom search channels can be written when you need to do a more complex search, check the user's permissions, format the results differently or customize the sort order of the results.


==Requirements==

  * Django 1.0 +
  * jquery 1.26 +
  * Autocomplete - jQuery plugin 1.0.2 [http://bassistance.de/jquery-plugins/jquery-plugin-autocomplete/]
  * jquery.autocomplete.css (included with Autocomplete)


==Installation==

in settings.py :

{{{
INSTALLED_APPS = (
                ...,
                'ajax_select'
                )
}}}


Make sure that these js/css files appear on your page:

  * jquery-1.2.6.js or greater
  * jquery.autocomplete.js
  * jquery.autocomplete.css
  * ajax_select.js (for pop up admin support)
  * iconic.css (optional, or use this as a starting point)

I like to use django-compress:

{{{
COMPRESS_CSS = {
    'all': {
        'source_filenames': (
            ...
            'shared/js/jqplugins/jquery.autocomplete.css',
        ),
        'output_filename': 'css/all_compressed.css',
        'extra_context': {
            'media': 'screen,projection',
        },
    },
}

COMPRESS_JS = {
    'all': {
        'source_filenames': (
            'shared/jquery_ui/jquery-1.2.6.js',
            'shared/js/jqplugins/jquery.autocomplete.js',
            ),
        'output_filename': 'js/all_compressed.js',
    },
}
}}}

But it would be nice if js and css files could be included from any path, not just those in the MEDIA_ROOT.  You will have to copy or symlink the included files to place them somewhere where they can be served.



in your `settings.py` define the channels in use on the site:

{{{
AJAX_LOOKUP_CHANNELS = {
    # the simplest case, pass a DICT with the model and field to search against :
    'track' : dict(model='music.track',search_field='title'),
    # this generates a simple channel
    # specifying the model Track in the music app, and searching against the 'title' field

    # or write a custom search channel and specify that using a TUPLE
    'contact' : ('peoplez.lookups', 'ContactLookup'),
    # this specifies to look for the class `ContactLookup` in the `peoplez.lookups` module
}
}}}

Custom search channels can be written when you need to do a more complex search, check the user's permissions (if the lookup URL should even be accessible to them, and then to perhaps filter which items they are allowed to see), format the results differently or customize the sort order of the results.   Search channel objects should implement the 4 methods shown in the following example.

`peoplez/lookups.py`
{{{
from peoplez.models import Contact
from django.db.models import Q

class ContactLookup(object):

    def get_query(self,q,request):
        """ return a query set.  you also have access to request.user if needed """
        return Contact.objects.filter(Q(name__istartswith=q) | Q(fname__istartswith=q) | Q(lname__istartswith=q) | Q(email__icontains=q))

    def format_result(self,contact):
        """ the search results display in the dropdown menu.  may contain html and multiple-lines. will remove any |  """
        return u"%s %s %s (%s)" % (contact.fname, contact.lname,contact.name,contact.email)

    def format_item(self,contact):
        """ the display of a currently selected object in the area below the search box. html is OK """
        return unicode(contact)

    def get_objects(self,ids):
        """ given a list of ids, return the objects ordered as you would like them on the admin page.
            this is for displaying the currently selected items (in the case of a ManyToMany field)
        """
        return Contact.objects.filter(pk__in=ids).order_by('name','lname')
}}}

HTML is fine in the result or item format.  Newlines and pipe chars will be removed and everything will be escaped properly.

Example showing security:
{{{
from django.http import HttpResponseForbidden

class ContactLookup(object):

    def get_query(self,q,request):
        """ return a query set.  you also have access to request.user if needed """
          if not request.user.is_authenticated():
             raise HttpResponseForbidden() # raising an exception, django will catch this and return an Http 403
        # filtering only this user's contacts
        return Contact.objects.filter(name__istartswith=q,created_by=request.user)

    ...

}}}


include the urls in your site's `urls.py`. This adds the lookup view and the pop up admin view.

{{{
    (r'^ajax_select/', include('ajax_select.urls')),
}}}


==Example==

for an example model:

{{{
class ContactMailing(models.Model):
    """ can mail to multiple contacts, has one author """
    contacts = models.ManyToManyField(Contact,blank=True)
    author = models.ForeignKey(Contact,blank=False)
    ...
}}}


in the `admin.py` for this app:

{{{
from ajax_select import make_ajax_form

class ContactMailingAdmin(Admin):
    form = make_ajax_form(ContactMailing,dict(author='contact',contacts='contact'))
}}}

`make_ajax_form( model, fieldlist )` is a factory function which will insert the ajax powered form field inputs 
so in this example the `author` field (`ForeignKey`) uses the 'contact' channel
and the `contacts` field (`ManyToMany`) also uses the 'contact' channel


If you need to write your own form class then specify that form for the admin as usual:

{{{
from forms import ContactMailingForm

class ContactMailingAdmin(admin.ModelAdmin):
    form = ContactMailingForm

admin.site.register(ContactMailing,ContactMailingAdmin)
}}}

in `forms.py` for that app:

{{{
from ajax_select.fields import AutoCompleteSelectMultipleField, AutoCompleteSelectField

class ContactMailingForm(models.ModelForm):

    # declare a field and specify the named channel that it uses
    contacts = AutoCompleteSelectMultipleField('contact', required=False)
    author = AutoCompleteSelectField('contact', required=False)

}}}


==Add another via popup==

Note that ajax_selects does not need to be in an admin.  Popups will still use an admin view (the registered admin for the model being added), even if your form does not.

1. subclass `AjaxSelectAdmin` or include the `autoselect_fields_check_can_add` hook in your admin's `get_form()`  [see AjaxSelectAdmin]

{{{
    def get_form(self, request, obj=None, **kwargs):
        form = super(AjaxSelectAdmin,self).get_form(request,obj,**kwargs)
        autoselect_fields_check_can_add(form,self.model,request.user)
        return form
}}}

2. Make sure that `js/ajax_select.js` is included in your admin's media or in your site's admin js stack.


`autoselect_fields_check_can_add(form,model,user)`

This checks if the user has permission to add the model, 
delegating first to the channel if that implements `can_add(user,model)`
otherwise using django's standard user.has_perm check.

The pop up is served by a custom view that uses the model's registered admin

3. For this to work you must include ajax_select/urls.py in your root urls.py under this directory:    

	`(r'^ajax_select/', include('ajax_select.urls')),`


Once the related object is successfully added, the mischevious custom view hijacks the little javascript response and substitutes `didAddPopup(win,newId,newRepr)` which is in `ajax_select.js`

Integrating with Django's normal popup admin system is tricky for a number of reasons.

`ModelAdmin` creates default fields for each field on the model.  Then for `ForeignKey` and `ManyToMany` fields it wraps the (default) form field's widget with a `RelatedFieldWidgetWrapper` that adds the magical green +.  (Incidentally it adds this regardless of whether you have permission to add the model or not.  This is a bug I need to file)

It then overwrites all of those with any explicitly declared fields.  `AutoComplete` fields are declared fields on your form, so if there was a Select field with a wrapper, it gets overwritten by the `AutoCompleteSelect`.  That doesn't matter anyway because `RelatedFieldWidgetWrapper` operates only with the default `SelectField` that it is expecting.

The green + pops open a window with a GET param: `_popup=1`.  The `ModelAdmin` recognizes this, the template uses if statements to reduce the page's html a bit, and when the ModelAdmin saves, it returns a simple response with just some javascript that calls `dismissAddAnotherPopup(win, newId, newRepr)` which is a function in `RelatedObjects.js`.  That looks for the form field, and if it is a `SelectField` as expected then it alters that accordingly.  Then it shuts the pop up window.




==Using ajax selects in a `FormSet`==

There might be a better way to do this.

`forms.py`
{{{
from django.forms.models import modelformset_factory
from django.forms.models import BaseModelFormSet
from ajax_select.fields import AutoCompleteSelectMultipleField, AutoCompleteSelectField

from models import *

# create a superclass
class BaseTaskFormSet(BaseModelFormSet):

    # that adds the field in, overwriting the previous default field
    def add_fields(self, form, index):
        super(BaseTaskFormSet, self).add_fields(form, index)
        form.fields["project"] = AutoCompleteSelectField('project', required=False)

# pass in the base formset class to the factory
TaskFormSet = modelformset_factory(Task,fields=('name','project','area'),extra=0,formset=BaseTaskFormSet)
}}}


==customizing the html or javascript==

django's `select_template` is used to choose the template to render the widget's interface:

    `autocompleteselect_{channel}.html` or `autocompleteselect.html`

So by writing a template `autocompleteselect_{channel}.html` you can customize the interface just for that channel.


==Handlers: On item added or removed==

Triggers are a great way to keep code clean and untangled.  Two triggers/signals are sent: 'added' and 'killed'.  These are sent to the p 'on deck' element.  That is the area that surrounds the currently selected items.  Its quite easy to bind functions to respond to these triggers.

Extend the template, implement the extra_script block and bind functions that will respond to the trigger:

multi select:
{{{
{% block extra_script %}
	$("#{{html_id}}_on_deck").bind('added',function() {
		id = $("#{{html_id}}").val();
		alert('added id:' + id );
	});
	$("#{{html_id}}_on_deck").bind('killed',function() {
		current = $("#{{html_id}}").val()
		alert('removed, current is:' + current);
	});
{% endblock %}
}}}

select:
{{{
{% block extra_script %}
	$("#{{html_id}}_on_deck").bind('added',function() {
		id = $("#{{html_id}}").val();
		alert('added id:' + id );
	});
	$("#{{html_id}}_on_deck").bind('killed',function() {
		alert('removed');
	});
{% endblock %}
}}}

auto-complete text select
{{{
{% block extra_script %}
$('#{{ html_id }}').bind('added',function() {
	entered = $('#{{ html_id }}').val();
	alert( entered );
});
{% endblock %}
}}}

There is no remove as there is no kill/delete button. The user may clear the text themselves but there is no javascript involved.  Its just a text field.


see:
http://docs.jquery.com/Events/trigger


==Help text==

If you are using AutoCompleteSelectMultiple outside of the admin then pass in `show_help_text=True`. 

This is because the admin displays the widget's help text and the widget would also.  But when used outside of the admin you need the help text.  This is not the case for `AutoCompleteSelect`.

When defining a db ManyToMany field django will append 'Hold down "Control", or "Command" on a Mac, to select more than one.' regardless of what widget is actually used. http://code.djangoproject.com/ticket/12359

Thus you should always define the help text in your form field, and its usually nicer to tell people what fields will be searched against.  Its not inherently obvious that the text field is ajax powered.


==CSS==

See iconic.css for some example styling.  autocomplete.js adds the .ac_loading class to the text field while the search is being served.  You can style this with fashionable ajax spinning discs etc.  A fashionable ajax spinning disc is included.


==Planned Improvements==

  * including of media will be improved to use field/admin's Media but it would be preferable if that can be integrated with django-compress

  * make it work within inline many to many fields (when the inlines themselves have lookups)


==Changelog==

1.1.0
Changed AutoCompleteSelect to work like AutoCompleteSelectMultiple:
	after the result is selected it is displayed below the text input and the text input is cleared.
	a clickable span is added to remove the item
Simplified functions a bit, cleaned up code
Added blocks: script and extra_script
Added 'killed' and 'added' triggers/signals
Support for adding an item via a pop up (ie. the django admin green + sign)




