Mark Lavin avatar Mark Lavin committed 02e079c Merge

Merge latest work into stable branch.

Comments (0)

Files changed (34)

+Primary author: 
+
+Mark Lavin
+
 The following people who have contributed to django-selectable:
 
-Mark Lavin
 Colin Copeland
 Karen Tracey
 
-Copyright (c) 2010-2011, Mark Lavin
+Copyright (c) 2010-2012, Mark Lavin
 All rights reserved.
 
 Redistribution and use in source and binary forms, with or without modification,
 Documentation for django-selectable is available on 
 `Read The Docs <http://readthedocs.org/>`_:
 
+- `Dev <http://readthedocs.org/docs/django-selectable/en/latest/>`_
+- `v0.2.0 <http://readthedocs.org/docs/django-selectable/en/version-0.2.0/>`_
 - `v0.1.2 <http://readthedocs.org/docs/django-selectable/en/version-0.1.2/>`_
-- `latest <http://readthedocs.org/docs/django-selectable/en/latest/>`_
+
 
 
 Contributing
 on `overriding the default widgets <http://docs.djangoproject.com/en/1.3/topics/forms/modelforms/#overriding-the-default-field-types-or-widgets>`_. As you will see integrating Django-Selectables in the admin
 is the same as working with regular forms.
 
+.. _admin-jquery-include:
 
 Including jQuery & jQuery UI
 --------------------------------------
 Basic Example
 --------------------------------------
 
-In our sample project we have a `Farm` model with a foreign key to `auth.User` and 
-a many to many relation to our `Fruit` model.
+In our sample project we have a ``Farm`` model with a foreign key to ``auth.User`` and 
+a many to many relation to our ``Fruit`` model.
 
     .. literalinclude:: ../example/core/models.py
        :pyobject: Farm
 
 You'll note this form also for new users to be created and associated with the
 farm if no user is found matching the given name. To make use of this feature we
-need to add `owner` to the exclude so that it will pass model validation. Unfortunately
+need to add ``owner`` to the exclude so that it will pass model validation. Unfortunately
 that means we must set the owner manual in the save and in the initial data because
-the `ModelForm` will no longer do this for you. Since `fruit` does not allow new
+the ``ModelForm`` will no longer do this for you. Since ``fruit`` does not allow new
 items you'll see these steps are not necessary.
 
 
 Inline Example
 --------------------------------------
 
-With our `Farm` model we can also associate the `UserAdmin` with a `Farm`
+With our ``Farm`` model we can also associate the ``UserAdmin`` with a ``Farm``
 by making use of the `InlineModelAdmin 
 <http://docs.djangoproject.com/en/1.3/ref/contrib/admin/#inlinemodeladmin-objects>`_.
-We can even make use of the same `FarmAdminForm`.
+We can even make use of the same ``FarmAdminForm``.
 
     .. literalinclude:: ../example/core/admin.py
         :pyobject: FarmInline

docs/advanced.rst

+Advanced Usage
+==========================
+
+We've gone through the most command and simple use cases for django-selectable. Now
+we'll take a lot at some of the more advanced features of this project. This assumes
+that you are comfortable reading and writing a little bit of Javascript making
+use of jQuery.
+
+
+Additional Parameters
+--------------------------------------
+
+The basic lookup is based on handling a search based on a single term string.
+If additional filtering is needed it can be inside the lookup ``get_query`` but
+you would need to define this when the lookup is defined. While this fits a fair
+number of use cases there are times when you need to define additional query
+parameters that won't be know until the either the form is bound or until selections
+are made on the client side. This section will detail how to handle both of these
+cases.
+
+
+How Parameters are Passed
+_______________________________________
+
+As with the search term the additional parameters you define will be passed in
+``request.GET``. Since ``get_query`` gets the current request so you will have access to
+them. Since they can be manipulated on the client side, these parameters should be
+treated like all user input. It should be properly validated and sanitized.
+
+
+Limiting the Result Set
+_______________________________________
+
+The number of results are globally limited/paginated by the :ref:`SELECTABLE_MAX_LIMIT`
+but you can also lower this limit on the field or widget level. Each field and widget
+takes a ``limit`` argument in the ``__init__`` that will be passed back to the lookup
+through the ``limit`` query parameter. The result set will be automatically paginated
+for you if you use either this parameter or the global setting.
+
+
+.. _server-side-parameters:
+
+Adding Parameters on the Server Side
+_______________________________________
+
+Each of the widgets define ``update_query_parameters`` which takes a dictionary. The
+most common way to use this would be in the form ``__init__``.
+
+    .. code-block:: python
+
+        class FruitForm(forms.Form):
+            autocomplete = forms.CharField(
+                label='Type the name of a fruit (AutoCompleteWidget)',
+                widget=selectable.AutoCompleteWidget(FruitLookup),
+                required=False,
+            )
+
+            def __init__(self, *args, **kwargs):
+                super(FruitForm, self).__init__(*args, **kwargs)
+                self.fields['autocomplete'].widget.update_query_parameters({'foo': 'bar'})
+
+
+.. _client-side-parameters:
+
+Adding Parameters on the Client Side
+_______________________________________
+
+There are times where you want to filter the result set based other selections
+by the user such as a filtering cities by a previously selected state. In this
+case you will need to bind a ``prepareQuery`` to the field. This function should accept the query dictionary. 
+You are free to make adjustments to  the query dictionary as needed.
+
+    .. code-block:: html
+
+        <script type="text/javascript">
+            function newParameters(query) {
+                query.foo = 'bar';
+            }
+
+            $(document).ready(function() {
+                $('#id_autocomplete').djselectable('option', 'prepareQuery', newParameters);
+            });
+        </script>
+
+
+.. _chain-select-example:
+
+Chained Selection
+--------------------------------------
+
+It's a fairly common pattern to have two or more inputs depend one another such City/State/Zip.
+In fact there are other Django apps dedicated to this purpose such as 
+`django-smart-selects <https://github.com/digi604/django-smart-selects>`_ or
+`django-ajax-filtered-fields <http://code.google.com/p/django-ajax-filtered-fields/>`_.
+It's possible to handle this kind of selection with django-selectable if you are willing
+to write a little javascript.
+
+Suppose we have city model
+
+    .. literalinclude:: ../example/core/models.py
+        :pyobject: City
+
+and a simple form
+
+    .. literalinclude:: ../example/core/forms.py
+        :pyobject: ChainedForm
+
+We want our users to select a city and if they choose a state then we will only
+show them cities in that state. To do this we will pass back chosen state as 
+addition parameter with the following javascript:
+
+    .. literalinclude:: ../example/core/templates/advanced.html
+        :language: html
+        :start-after: {% block extra-js %}
+        :end-before: {% endblock %}
+
+
+Then in our lookup we will grab the state value and filter our results on it:
+
+    .. literalinclude:: ../example/core/lookups.py
+        :pyobject: CityLookup
+
+And that's it! We now have a working chained selection example. The full source
+is included in the example project.
+
+.. _client-side-changes:
+
+Detecting Client Side Changes
+____________________________________________
+
+Our previous example made us of detecting changes to the selection on the client
+side to pass new parameters to the lookup. Since django-selectable is built on top of the jQuery UI 
+`Autocomplete plug-in <http://jqueryui.com/demos/autocomplete/>`_, the widgets
+expose the events defined by the plugin.
+
+    - autocompletecreate
+    - autocompletesearch
+    - autocompleteopen
+    - autocompletefocus
+    - autocompleteselect
+    - autocompleteclose
+    - autocompletechange
+
+For the most part these event names should be self-explanatory. If you need additional
+detail you should refer to the `jQuery UI docs on these events <http://jqueryui.com/demos/autocomplete/#events>`_.
+
+
+Submit On Selection
+--------------------------------------
+
+You might want to help your users by submitting the form once they have selected a valid
+item. To do this you simply need to listen for the ``autocompleteselect`` event. This
+event is fired by the text input which has an index of 0. If you field is named ``my_field``
+then input to watch would be ``my_field_0`` such as:
+
+    .. code-block:: html
+
+        <script type="text/javascript">
+            $(document).ready(function() {
+                $(':input[name=my_field_0]').bind('autocompleteselect', function(event, ui) {
+                    $(this).parents("form").submit();
+                });
+            });
+        </script>
+
+
+Dynamically Added Forms
+--------------------------------------
+
+django-selectable can work with dynamically added forms such as inlines in the admin.
+To make django-selectable work in the admin there is nothing more to do than include
+the necessary static media as described in the 
+:ref:`Admin Integration <admin-jquery-include>` section.
+
+If you are making use of the popular `django-dynamic-formset <http://code.google.com/p/django-dynamic-formset/>`_
+then you can make django-selectable work by passing ``bindSelectables`` to the 
+`added <http://code.google.com/p/django-dynamic-formset/source/browse/trunk/docs/usage.txt#259>`_ option:
+
+    .. code-block:: html
+
+        <script type="text/javascript">
+            $(document).ready(function() {
+                $('#my-formset').formset({
+               		added: bindSelectables	
+                });
+            });
+        </script>
+
+Currently you must include the django-selectable javascript below this formset initialization
+code for this to work. See django-selectable `issue #31 <https://bitbucket.org/mlavin/django-selectable/issue/31/>`_
+for some additional detail on this problem.
+
+
+.. _advanaced-label-formats:
+
+Label Formats on the Client Side
+--------------------------------------
+
+The lookup label is the text which is shown in the list before it is selected.
+You can use the :ref:`get_item_label <lookup-get-item-label>` method in your lookup
+to do this on the server side. This works for most applications. However if you don't
+want to write your HTML in Python or need to adapt the format on the client side you
+can use the :ref:`formatLabel <javascript-formatLabel>` option.
+
+``formatLabel`` takes two paramaters the current label and the current selected item.
+The item is a dictionary object matching what is returned by the lookup's
+:ref:`format_item <lookup-format-item>`. ``formatLabel`` should return the string
+which should be used for the label.
+
+Going back to the ``CityLookup`` we can adjust the label to wrap the city and state
+portions with their own classes for additional styling:
+
+    .. literalinclude:: ../example/core/lookups.py
+        :pyobject: CityLookup
+
+    .. code-block:: html
+
+        <script type="text/javascript">
+            $(document).ready(function() {
+                function formatLabel(label, item) {
+                    var data = label.split(',');
+                    return '<span class="city">' + data[0] + '</span>, <span class="state">' + data[1] + '</span>';
+                }
+                $('#id_city_0').djselectable('option', 'formatLabel', formatLabel);
+            });
+        </script>
+
+This is a rather simple example but you could also pass additional information in ``format_item``
+such as a flag of whether the city is the capital and render the state captials differently.
 
 # General information about the project.
 project = u'Django-Selectable'
-copyright = u'2011, Mark Lavin'
+copyright = u'2011-2012, Mark Lavin'
 
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
 # built documents.
 #
 # The short X.Y version.
-version = '0.2'
+version = '0.3'
 # The full version, including alpha/beta/rc tags.
-release = '0.2.0'
+release = '0.3.0dev'
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
 
 Django-Selectable defines a number of fields for selecting either single or mutliple
 lookup items. Item in this context corresponds to the object return by the underlying
-lookup `get_item`. The single select select fields (:ref:`AutoCompleteSelectField` and
+lookup ``get_item``. The single select select fields (:ref:`AutoCompleteSelectField` and
 :ref:`AutoComboboxSelectField`) allow for the creation of new items. To use this feature the field's
-lookup class must define `create_item`. In the case of lookups extending from
+lookup class must define ``create_item``. In the case of lookups extending from
 :ref:`ModelLookup` newly created items have not yet been saved into the database and saving
 should be handled by the form. All fields take the lookup class as the first required
 argument.
 --------------------------------------
     
 Field tied to :ref:`AutoCompleteSelectWidget` to bind the selection to the form and  
-create new items, if allowed. The `allow_new` keyword argument (default: `False`)
+create new items, if allowed. The ``allow_new`` keyword argument (default: ``False``)
 which determines if the field allows new items. This field cleans to a single item.
 
+    .. literalinclude:: ../example/core/forms.py
+        :start-after: # AutoCompleteSelectField (no new items)
+        :end-before: # AutoCompleteSelectField (allows new items)
+
 
 .. _AutoComboboxSelectField:
 
 --------------------------------------
 
 Field tied to :ref:`AutoComboboxSelectWidget` to bind the selection to the form and 
-create new items, if allowed. The `allow_new` keyword argument (default: `False`)
+create new items, if allowed. The ``allow_new`` keyword argument (default: ``False``)
 which determines if the field allows new items. This field cleans to a single item.
 
+    .. literalinclude:: ../example/core/forms.py
+        :start-after: # AutoComboboxSelectField (no new items)
+        :end-before: # AutoComboboxSelectField (allows new items)
+
 
 .. _AutoCompleteSelectMultipleField:
 
 This field cleans to a list of items. :ref:`AutoCompleteSelectMultipleField` does not
 allow for the creation of new items.
 
+    .. literalinclude:: ../example/core/forms.py
+        :start-after: # AutoCompleteSelectMultipleField
+        :end-before: # AutoComboboxSelectMultipleField
+
 
 .. _AutoComboboxSelectMultipleField:
 
 Field tied to :ref:`AutoComboboxSelectMultipleWidget` to bind the selection to the form.
 This field cleans to a list of items. :ref:`AutoComboboxSelectMultipleField` does not 
 allow for the creation of new items.
+
+    .. literalinclude:: ../example/core/forms.py
+        :start-after: # AutoComboboxSelectMultipleField
+        :end-before: class ChainedForm
     overview
     quick-start
     lookups
-    parameters
+    advanced
     admin    
     fields
     widgets
 django-selectable uses a registration pattern similar to the Django admin.
 Lookups should be defined in a `lookups.py` in your application's module. Once defined
 you must register in with django-selectable. All lookups must extend from 
-`selectable.base.LookupBase` which defines the API for every lookup.
+``selectable.base.LookupBase`` which defines the API for every lookup.
 
     .. code-block:: python
 
     :param term: The search term from the widget input.
     :return: An iterable set of data of items matching the search term.
 
+.. _lookup-get-item-label:
+
 .. py:method:: LookupBase.get_item_label(item)
 
     This is first of three formatting methods. The label is shown in the
-    drop down menu of search results. This defaults to `item.__unicode__`.
+    drop down menu of search results. This defaults to ``item.__unicode__``.
 
     :param item: An item from the search results.
     :return: A string representation of the item to be shown in the search results.
 
+    .. versionadded:: 0.3
+
+    The label can include HTML. For changing the label format on the client side
+    see :ref:`Advanaced Label Formats <advanaced-label-formats>`.
+    
+
 .. py:method:: LookupBase.get_item_id(item)
 
     This is second of three formatting methods. The id is the value that will eventually
-    be returned by the field/widget. This defaults to `item.__unicode__`.
+    be returned by the field/widget. This defaults to ``item.__unicode__``.
 
     :param item: An item from the search results.
     :return: A string representation of the item to be returned by the field/widget.
 .. py:method:: LookupBase.get_item_value(item)
 
     This is last of three formatting methods. The value is shown in the
-    input once the item has been selected. This defaults to `item.__unicode__`.
+    input once the item has been selected. This defaults to ``item.__unicode__``.
 
     :param item: An item from the search results.
     :return: A string representation of the item to be shown in the input.
 
 .. py:method:: LookupBase.get_item(value)
 
-    `get_item` is the reverse of `get_item_id`. This should take the value
+    ``get_item`` is the reverse of ``get_item_id``. This should take the value
     from the form initial values and return the current item. This defaults
     to simply return the value.
 
 
     If you plan to use a lookup with a field or widget which allows the user
     to input new values then you must define what it means to create a new item
-    for your lookup. By default this raises a `NotImplemented` error.
+    for your lookup. By default this raises a ``NotImplemented`` error.
 
     :param value: The user given value.
     :return: The new item created from the item.
 
+.. _lookup-format-item:
+
 .. py:method:: LookupBase.format_item(item)
 
-    By default `format_item` creates a dictionary with the three keys used by
+    By default ``format_item`` creates a dictionary with the three keys used by
     the UI plugin: id, value, label. These are generated from the calls to
-    `get_item_id`, `get_item_value`, and `get_item_label`. If you want to
+    ``get_item_id``, ``get_item_value``, and ``get_item_label``. If you want to
     add additional keys you should add them here.
 
     :param item: An item from the search results.
 
 .. py:method:: LookupBase.paginate_results(request, results, limit)
 
-    If :ref:`SELECTABLE_MAX_LIMIT` is defined or `limit` is passed in request.GET
-    then `paginate_results` will return the current page using Django's
+    If :ref:`SELECTABLE_MAX_LIMIT` is defined or ``limit`` is passed in request.GET
+    then ``paginate_results`` will return the current page using Django's
     built in pagination. See the Django docs on `pagination <https://docs.djangoproject.com/en/1.3/topics/pagination/>`_
     for more info.
 
 --------------------------------------
 
 Perhaps the most common use case is to define a lookup based on a given Django model.
-For this you can extend `selectable.base.ModelLookup`. To extend `ModelLookup` you
-should set two class attributes: `model` and `search_field`.
+For this you can extend ``selectable.base.ModelLookup``. To extend ``ModelLookup`` you
+should set two class attributes: ``model`` and ``search_fields``.
 
     .. literalinclude:: ../example/core/lookups.py
         :pyobject: FruitLookup
 
-The syntax for `search_field` is the same as the Django 
+The syntax for ``search_fields`` is the same as the Django 
 `field lookup syntax <http://docs.djangoproject.com/en/1.3/ref/models/querysets/#field-lookups>`_. 
-You may optionally define a third class attribute `filters` which is a dictionary of
+Each of these lookups are combined as OR so any one of them matching will return a
+result. You may optionally define a third class attribute ``filters`` which is a dictionary of
 filters to be applied to the model queryset. The keys should be a string defining a field lookup
-and the value should be the value for the field lookup.
+and the value should be the value for the field lookup. Filters on the other hand are
+combined with AND.
 
+.. versionadded:: 0.3
+
+Prior to version 0.3 the model based lookups used a single string ``search_field``. This
+will continue to work in v0.3 but will raise a DeprecationWarning. This support will
+be removed in v0.4.
+
+
+User Lookup Example
+--------------------------------------
+
+Below is a larger model lookup example using multiple search fields, filters 
+and display options for the `auth.User <https://docs.djangoproject.com/en/1.3/topics/auth/#users>`_ 
+model.
+
+    .. code-block:: python
+
+        from django.contrib.auth.models import User
+        from selectable.base import ModelLookup
+        from selectable.registry import registry
+
+
+        class UserLookup(ModelLookup):
+            model = User
+            search_fields = (
+                'username__icontains',
+                'first_name__icontains',
+                'last_name__icontains',
+            )
+            filters = {'is_active': True, }
+
+            def get_item_value(self, item):
+                # Display for currently selected item
+                return item.username
+
+            def get_item_label(self, item):
+                # Display for choice listings
+                return u"%s (%s)" % (item.username, item.get_full_name())
+
+        registry.register(UserLookup)
+

docs/parameters.rst

-Additional Parameters
-=========================
-
-The basic lookup is based on handling a search based on a single term string.
-If additional filtering is needed it can be inside the lookup `get_query` but
-you would need to define this when the lookup is defined. While this fits a fair
-number of use cases there are times when you need to define additional query
-parameters that won't be know until the either the form is bound or until selections
-are made on the client side. This section will detail how to handle both of these
-cases.
-
-
-How Parameters are Passed
---------------------------------------
-
-As with the search term the additional parameters you define will be passed in
-`request.GET`. Since `get_query` gets the current request so you will have access to
-them. Since they can be manipulated on the client side, these parameters should be
-treated like all user input. It should be properly validated and sanitized.
-
-
-Limiting the Result Set
---------------------------------------
-
-The number of results are globally limited/paginated by the :ref:`SELECTABLE_MAX_LIMIT`
-but you can also lower this limit on the field or widget level. Each field and widget
-takes a `limit` argument in the `__init__` that will be passed back to the lookup
-through the `limit` query parameter. The result set will be automatically paginated
-for you if you use either this parameter or the global setting.
-
-
-.. _server-side-parameters:
-
-Adding Parameters on the Server Side
---------------------------------------
-
-Each of the widgets define `update_query_parameters` which takes a dictionary. The
-most common way to use this would be in the form `__init__`.
-
-    .. code-block:: python
-
-        class FruitForm(forms.Form):
-            autocomplete = forms.CharField(
-                label='Type the name of a fruit (AutoCompleteWidget)',
-                widget=selectable.AutoCompleteWidget(FruitLookup),
-                required=False,
-            )
-
-            def __init__(self, *args, **kwargs):
-                super(FruitForm, self).__init__(*args, **kwargs)
-                self.fields['autocomplete'].widget.update_query_parameters({'foo': 'bar'})
-
-
-.. _client-side-parameters:
-
-Adding Parameters on the Client Side
---------------------------------------
-
-There are times where you want to filter the result set based other selections
-by the user such as a filtering cities by a previously selected state. In this
-case you will need to bind a `prepareQuery` to the field. This function should accept the query dictionary. 
-You are free to make adjustments to  the query dictionary as needed.
-
-    .. code-block:: html
-
-        <script type="text/javascript">
-            function newParameters(query) {
-                query.foo = 'bar';
-            }
-
-            $(document).ready(function() {
-                $('#id_autocomplete').djselectable('option', 'prepareQuery', newParameters);
-            });
-        </script>
-
-
-.. _client-side-changes:
-
-Detecting Client Side Changes
---------------------------------------
-
-Since django-selectable is built on top of the jQuery UI 
-`Autocomplete plug-in <http://jqueryui.com/demos/autocomplete/>`_, the widgets
-expose the events defined by the plugin.
-
-    - autocompletecreate
-    - autocompletesearch
-    - autocompleteopen
-    - autocompletefocus
-    - autocompleteselect
-    - autocompleteclose
-    - autocompletechange
-
-For the most part these event names should be self-explanatory. If you need additional
-detail you should refer to the `jQuery UI docs on these events <http://jqueryui.com/demos/autocomplete/#events>`_.
-
-
-.. _chain-select-example:
-
-Chained Selection Example
---------------------------------------
-
-It's a fairly common pattern to have two or more inputs depend one another such City/State/Zip.
-In fact there are other Django apps dedicated to this purpose such as 
-`django-smart-selects <https://github.com/digi604/django-smart-selects>`_ or
-`django-ajax-filtered-fields <http://code.google.com/p/django-ajax-filtered-fields/>`_.
-It's possible to handle this kind of selection with django-selectable if you are willing
-to write a little javascript.
-
-Suppose we have city model
-
-    .. literalinclude:: ../example/core/models.py
-        :pyobject: City
-
-and a simple form
-
-    .. literalinclude:: ../example/core/forms.py
-        :pyobject: ChainedForm
-
-We want our users to select a city and if they choose a state then we will only
-show them cities in that state. To do this we will pass back chosen state as 
-addition parameter with the following javascript:
-
-    .. literalinclude:: ../example/core/templates/advanced.html
-        :start-after: {% block extra-js %}
-        :end-before: {% endblock %}
-
-
-Then in our lookup we will grab the state value and filter our results on it:
-
-    .. literalinclude:: ../example/core/lookups.py
-        :pyobject: CityLookup
-
-And that's it! We now have a working chained selection example. The full source
-is included in the example project.
-
-
-Submit On Selection Example
---------------------------------------
-
-You might want to help your users by submitting the form once they have selected a valid
-item. To do this you simply need to listen for the `autocompleteselect` event. This
-event is fired by the text input which has an index of 0. If you field is named `my_field`
-then input to watch would be `my_field_0` such as:
-
-    .. code-block:: html
-
-        <script type="text/javascript">
-            $(document).ready(function() {
-                $(':input[name=my_field_0]').bind('autocompleteselect', function(event, ui) {
-                    $(this).parents("form").submit();
-                });
-            });
-        </script>
-

docs/quick-start.rst

 Including jQuery & jQuery UI
 --------------------------------------
 
+The widgets in django-selectable define the media they need as described in the
+Django documentation on `Form Media <https://docs.djangoproject.com/en/1.3/topics/forms/media/>`_.
+That means to include the javascript and css you need to make the widgets work you
+can include ``{{ form.media.css }}`` and ``{{ form.media.js }}`` in your template. This is
+assuming your form is called `form` in the template context. For more information
+please check out the `Django documentation <https://docs.djangoproject.com/en/1.3/topics/forms/media/>`_.
+
 The jQuery and jQuery UI libraries are not included in the distribution but must be included
 in your templates. See the example project for an example using these libraries from the
 `Google CDN <http://code.google.com/apis/libraries/devguide.html#jquery>`_. Django-Selectable
 should work with `jQuery <http://jquery.com/>`_ >= 1.4.3 and `jQuery UI <http://jqueryui.com/>`_ >= 1.8
 
     .. literalinclude:: ../example/core/templates/base.html
-        :start-after: {{ form.media.css }}
-        :end-before: {{ form.media.js }}
+        :start-after: {% block extra-css %}{% endblock %}
+        :end-before: {% block extra-js %}
 
 
 You must also include a `jQuery UI theme <http://jqueryui.com/themeroller/>`_ stylesheet. In the
 
     .. literalinclude:: ../example/core/templates/base.html
         :start-after: </title>
-        :end-before: {{ form.media.css }}
+        :end-before: {% block extra-css %}
 
 
 Defining a Lookup
     .. literalinclude:: ../example/core/lookups.py
         :pyobject: FruitLookup
 
-This lookups extends `selectable.base.ModelLookup` and defines two things: one is
+This lookups extends ``selectable.base.ModelLookup`` and defines two things: one is
 the model on which we will be searching and the other is the field which we are searching.
 This syntax should look familiar as it is the same as the `field lookup syntax <http://docs.djangoproject.com/en/1.3/ref/models/querysets/#field-lookups>`_
 for making queries in Django.
         :pyobject: FruitForm
         :end-before: newautocomplete
 
-This replaces the default widget for the `CharField` with the `AutoCompleteWidget`.
+This replaces the default widget for the ``CharField`` with the ``AutoCompleteWidget``.
 This will allow the user to fill this field with values taken from the names of
-existing `Fruit` models.
+existing ``Fruit`` models.
 
 And that's pretty much it. Keep on reading if you want to learn about the other
 types of fields and widgets that are available as well as defining more complicated

docs/releases.rst

 Release Notes
 ==================
 
-v0.2.0
+v0.3.0 (Released TBD)
+--------------------------------------
+
+Features
+_________________
+
+- Multiple search fields for :ref:`model based lookups <ModelLookup>`
+- Support for :ref:`highlighting term matches <javascript-highlightMatch>`
+- Support for HTML in :ref:`result labels <lookup-get-item-label>`
+- Support for :ref:`client side formatting <advanaced-label-formats>`
+- Additional documentation
+- Expanded examples in example project
+
+
+Bug Fixes
+_________________
+
+- Fixed issue with Enter key removing items from select multiple widgets `#24 <https://bitbucket.org/mlavin/django-selectable/issue/24/pressing-enter-when-autocomplete-input-box>`_
+
+
+Backwards Incompatible Changes
+________________________________
+
+- The fix for #24 changed the remove items from a button to an anchor tag. If you were previously using the button tag for additional styling then you will need to adjust your styles.
+- The static resources were moved into a `selectable` sub-directory. This makes the media more in line with the template directory conventions. If you are using the widgets in the admin there is nothing to change. If you are using ``{{ form.media }}`` then there is also nothing to change. However if you were including static media manually then you will need to adjust them to include the selectable prefix.
+
+
+v0.2.0 (Released 2011-08-13)
 --------------------------------------
 
 Features
 - :ref:`Chained selection example <chain-select-example>`
 
 
-v0.1.2
+v0.1.2 (Released 2011-05-25)
 --------------------------------------
 
 Bug Fixes
 - Fixed issue `#17 <https://bitbucket.org/mlavin/django-selectable/issue/17/update-not-working>`_
 
 
-v0.1.1
+v0.1.1 (Release 2011-03-21)
 --------------------------------------
 
 Bug Fixes
 - Refactored JS for easier configuration
 
 
-v0.1
+v0.1 (Released 2011-03-13)
 --------------------------------------
 
 Initial public release

docs/settings.rst

 
         SELECTABLE_MAX_LIMIT = None
 
-Default: 25
+Default: ``25``
 
+
+.. _javascript-options:
+
+Javascript Plugin Options
+--------------------------------------
+
+Below the options for configuring the Javascript behavior of the django-selectable
+widgets.
+
+
+.. _javascript-removeIcon:
+
+removeIcon
+______________________________________
+
+.. versionadded:: 0.2
+
+This is the class name used for the remove buttons for the multiple select widgets.
+The set of icon classes built into the jQuery UI framework can be found here:
+http://jqueryui.com/themeroller/
+
+Default: ``ui-icon-close``
+
+
+.. _javascript-comboboxIcon:
+
+comboboxIcon
+______________________________________
+
+.. versionadded:: 0.2
+
+This is the class name used for the combobox dropdown icon. The set of icon classes built 
+into the jQuery UI framework can be found here: http://jqueryui.com/themeroller/
+
+Default: ``ui-icon-triangle-1-s``
+
+
+.. _javascript-prepareQuery:
+
+prepareQuery
+______________________________________
+
+.. versionadded:: 0.2
+
+``prepareQuery`` is a function that is run prior to sending the search request to
+the server. It is an oppotunity to add additional parameters to the search query.
+It takes one argument which is the current search parameters as a dictionary. For
+more information on its usage see :ref:`Adding Parameters on the Client Side <client-side-parameters>`.
+
+Default: ``null``
+
+
+.. _javascript-highlightMatch:
+
+highlightMatch
+______________________________________
+
+.. versionadded:: 0.3
+
+If true the portions of the label which match the current search term will be wrapped
+in a span with the class ``highlight``.
+
+Default: ``true``
+
+
+.. _javascript-formatLabel:
+
+formatLabel
+______________________________________
+
+.. versionadded:: 0.3
+
+``formatLabel`` is a function that is run prior to rendering the search results in
+the dropdown menu. It takes two arguments: the current item label and the item data
+dictionary. It should return the label which should be used. For more information
+on its usage see :ref:`Label Formats on the Client Side <advanaced-label-formats>`.
+
+Default: ``null``
+
 --------------------------------------
 
 Basic widget for auto-completing text. The widget returns the item value as defined
-by the lookup `get_item_value`. If the `allow_new` keyword argument is passed as
+by the lookup ``get_item_value``. If the ``allow_new`` keyword argument is passed as
 true it will allow the user to type any text they wish.
 
 
 --------------------------------------
 
 Builds a list of selected items from auto-completion. This widget will return a list
-of item ids as defined by the lookup `get_item_id`. Using this widget with the
+of item ids as defined by the lookup ``get_item_id``. Using this widget with the
 :ref:`AutoCompleteSelectMultipleField` will clean the items to the item objects. This does
-not allow for creating new items. There is another optional keyword argument `postion`
+not allow for creating new items. There is another optional keyword argument ``postion``
 which can take four possible values: `bottom`, `bottom-inline`, `top` or `top-inline`.
 This determine the position of the deck list of currently selected items as well as
 whether this list is stacked or inline. The default is `bottom`.

example/core/fixtures/initial_data.json

         "fields": {
             "name": "Orange"
         }
-    },
-{
+    }, 
+    {
+        "pk": 1, 
+        "model": "core.farm", 
+        "fields": {
+            "owner": 1, 
+            "fruit": [
+                1
+            ], 
+            "name": "My Farm"
+        }
+    }, 
+    {
+        "pk": 2, 
+        "model": "core.farm", 
+        "fields": {
+            "owner": 1, 
+            "fruit": [
+                1
+            ], 
+            "name": "Test"
+        }
+    }, 
+    {
         "pk": 1, 
         "model": "core.city", 
         "fields": {
         }
     }, 
     {
-        "pk": 6, 
-        "model": "core.city", 
-        "fields": {
-            "state": "DC-VA-MD-WV", 
-            "name": "Washington-Arlington-Alexandria"
-        }
-    }, 
-    {
         "pk": 7, 
         "model": "core.city", 
         "fields": {
         }
     }, 
     {
-        "pk": 16, 
-        "model": "core.city", 
-        "fields": {
-            "state": "MA-NH", 
-            "name": "Boston-Cambridge-Quincy"
-        }
-    }, 
-    {
         "pk": 17, 
         "model": "core.city", 
         "fields": {
         }
     }, 
     {
-        "pk": 20, 
-        "model": "core.city", 
-        "fields": {
-            "state": "MN-WI", 
-            "name": "Minneapolis-St. Paul-Bloomington"
-        }
-    }, 
-    {
         "pk": 21, 
         "model": "core.city", 
         "fields": {
         }
     }, 
     {
-        "pk": 28, 
-        "model": "core.city", 
-        "fields": {
-            "state": "IL-IN-WI", 
-            "name": "Chicago-Naperville-Joliet"
-        }
-    }, 
-    {
         "pk": 29, 
         "model": "core.city", 
         "fields": {
         }
     }, 
     {
-        "pk": 30, 
-        "model": "core.city", 
-        "fields": {
-            "state": "OR-WA", 
-            "name": "Portland-Vancouver-Beaverton"
-        }
-    }, 
-    {
         "pk": 31, 
         "model": "core.city", 
         "fields": {
         }
     }, 
     {
-        "pk": 34, 
-        "model": "core.city", 
-        "fields": {
-            "state": "NY-NJ-PA", 
-            "name": "New York-Northern New Jersey-Long Island"
-        }
-    }, 
-    {
         "pk": 35, 
         "model": "core.city", 
         "fields": {
         }
     }, 
     {
-        "pk": 39, 
-        "model": "core.city", 
-        "fields": {
-            "state": "PA-NJ-DE-MD", 
-            "name": "Philadelphia-Camden-Wilmington"
-        }
-    }, 
-    {
-        "pk": 40, 
-        "model": "core.city", 
-        "fields": {
-            "state": "NC-SC", 
-            "name": "Charlotte-Gastonia-Concord"
-        }
-    }, 
-    {
         "pk": 41, 
         "model": "core.city", 
         "fields": {
         }
     }, 
     {
-        "pk": 60, 
-        "model": "core.city", 
-        "fields": {
-            "state": "MO-KS", 
-            "name": "Kansas City"
-        }
-    }, 
-    {
         "pk": 61, 
         "model": "core.city", 
         "fields": {
         }
     }, 
     {
-        "pk": 84, 
-        "model": "core.city", 
-        "fields": {
-            "state": "MO-IL", 
-            "name": "St. Louis"
-        }
-    }, 
-    {
         "pk": 85, 
         "model": "core.city", 
         "fields": {
         }
     }, 
     {
-        "pk": 86, 
-        "model": "core.city", 
-        "fields": {
-            "state": "NH-ME", 
-            "name": "Rochester-Dover"
-        }
-    }, 
-    {
         "pk": 87, 
         "model": "core.city", 
         "fields": {
         }
     }, 
     {
-        "pk": 94, 
-        "model": "core.city", 
-        "fields": {
-            "state": "WI-MN", 
-            "name": "La Crosse"
-        }
-    }, 
-    {
-        "pk": 95, 
-        "model": "core.city", 
-        "fields": {
-            "state": "OH-KY-IN", 
-            "name": "Cincinnati-Middletown"
-        }
-    }, 
-    {
         "pk": 96, 
         "model": "core.city", 
         "fields": {
         }
     }, 
     {
-        "pk": 101, 
-        "model": "core.city", 
-        "fields": {
-            "state": "MA-CT", 
-            "name": "Worcester"
-        }
-    }, 
-    {
         "pk": 102, 
         "model": "core.city", 
         "fields": {
         }
     }, 
     {
-        "pk": 103, 
-        "model": "core.city", 
-        "fields": {
-            "state": "AR-MO", 
-            "name": "Fayetteville-Springdale-Rogers"
-        }
-    }, 
-    {
         "pk": 104, 
         "model": "core.city", 
         "fields": {
         }
     }, 
     {
-        "pk": 107, 
-        "model": "core.city", 
-        "fields": {
-            "state": "MN-WI", 
-            "name": "Duluth"
-        }
-    }, 
-    {
         "pk": 108, 
         "model": "core.city", 
         "fields": {
         }
     }, 
     {
-        "pk": 114, 
-        "model": "core.city", 
-        "fields": {
-            "state": "MA-CT", 
-            "name": "Springfield"
-        }
-    }, 
-    {
         "pk": 115, 
         "model": "core.city", 
         "fields": {
         }
     }, 
     {
-        "pk": 119, 
-        "model": "core.city", 
-        "fields": {
-            "state": "IA-IL", 
-            "name": "Davenport-Moline-Rock Island"
-        }
-    }, 
-    {
-        "pk": 120, 
-        "model": "core.city", 
-        "fields": {
-            "state": "RI-MA", 
-            "name": "Providence-Fall River-Warwick"
-        }
-    }, 
-    {
         "pk": 121, 
         "model": "core.city", 
         "fields": {
         }
     }, 
     {
-        "pk": 138, 
-        "model": "core.city", 
-        "fields": {
-            "state": "NE-IA", 
-            "name": "Omaha-Council Bluffs"
-        }
-    }, 
-    {
         "pk": 139, 
         "model": "core.city", 
         "fields": {
         }
     }, 
     {
-        "pk": 143, 
-        "model": "core.city", 
-        "fields": {
-            "state": "NH-ME", 
-            "name": "Portsmouth"
-        }
-    }, 
-    {
         "pk": 144, 
         "model": "core.city", 
         "fields": {
         }
     }, 
     {
-        "pk": 148, 
-        "model": "core.city", 
-        "fields": {
-            "state": "UT-ID", 
-            "name": "Logan"
-        }
-    }, 
-    {
         "pk": 149, 
         "model": "core.city", 
         "fields": {
         }
     }, 
     {
-        "pk": 154, 
-        "model": "core.city", 
-        "fields": {
-            "state": "PA-NJ", 
-            "name": "Allentown-Bethlehem-Easton"
-        }
-    }, 
-    {
         "pk": 155, 
         "model": "core.city", 
         "fields": {
         }
     }, 
     {
-        "pk": 159, 
-        "model": "core.city", 
-        "fields": {
-            "state": "CT-RI", 
-            "name": "Norwich-New London"
-        }
-    }, 
-    {
         "pk": 160, 
         "model": "core.city", 
         "fields": {
         }
     }, 
     {
-        "pk": 170, 
-        "model": "core.city", 
-        "fields": {
-            "state": "ND-MN", 
-            "name": "Fargo"
-        }
-    }, 
-    {
         "pk": 171, 
         "model": "core.city", 
         "fields": {
         }
     }, 
     {
-        "pk": 173, 
-        "model": "core.city", 
-        "fields": {
-            "state": "TN-GA", 
-            "name": "Chattanooga"
-        }
-    }, 
-    {
         "pk": 174, 
         "model": "core.city", 
         "fields": {
         }
     }, 
     {
-        "pk": 179, 
-        "model": "core.city", 
-        "fields": {
-            "state": "KY-IN", 
-            "name": "Louisville-Jefferson County"
-        }
-    }, 
-    {
         "pk": 180, 
         "model": "core.city", 
         "fields": {
         }
     }, 
     {
-        "pk": 182, 
-        "model": "core.city", 
-        "fields": {
-            "state": "VA-NC", 
-            "name": "Virginia Beach-Norfolk-Newport News"
-        }
-    }, 
-    {
         "pk": 183, 
         "model": "core.city", 
         "fields": {
         }
     }, 
     {
-        "pk": 184, 
-        "model": "core.city", 
-        "fields": {
-            "state": "MD-WV", 
-            "name": "Cumberland"
-        }
-    }, 
-    {
         "pk": 185, 
         "model": "core.city", 
         "fields": {
         }
     }, 
     {
-        "pk": 195, 
-        "model": "core.city", 
-        "fields": {
-            "state": "ND-MN", 
-            "name": "Grand Forks"
-        }
-    }, 
-    {
-        "pk": 196, 
-        "model": "core.city", 
-        "fields": {
-            "state": "IN-MI", 
-            "name": "South Bend-Mishawaka"
-        }
-    }, 
-    {
         "pk": 197, 
         "model": "core.city", 
         "fields": {
         }
     }, 
     {
-        "pk": 200, 
-        "model": "core.city", 
-        "fields": {
-            "state": "WV-KY-OH", 
-            "name": "Huntington-Ashland"
-        }
-    }, 
-    {
         "pk": 201, 
         "model": "core.city", 
         "fields": {
         }
     }, 
     {
-        "pk": 225, 
-        "model": "core.city", 
-        "fields": {
-            "state": "GA-AL", 
-            "name": "Columbus"
-        }
-    }, 
-    {
-        "pk": 226, 
-        "model": "core.city", 
-        "fields": {
-            "state": "GA-SC", 
-            "name": "Augusta-Richmond County"
-        }
-    }, 
-    {
         "pk": 227, 
         "model": "core.city", 
         "fields": {
         }
     }, 
     {
-        "pk": 244, 
-        "model": "core.city", 
-        "fields": {
-            "state": "IA-NE-SD", 
-            "name": "Sioux City"
-        }
-    }, 
-    {
         "pk": 245, 
         "model": "core.city", 
         "fields": {
         }
     }, 
     {
-        "pk": 282, 
-        "model": "core.city", 
-        "fields": {
-            "state": "MO-KS", 
-            "name": "St. Joseph"
-        }
-    }, 
-    {
         "pk": 283, 
         "model": "core.city", 
         "fields": {
         }
     }, 
     {
-        "pk": 285, 
-        "model": "core.city", 
-        "fields": {
-            "state": "TN-MS-AR", 
-            "name": "Memphis"
-        }
-    }, 
-    {
         "pk": 286, 
         "model": "core.city", 
         "fields": {
         }
     }, 
     {
-        "pk": 295, 
-        "model": "core.city", 
-        "fields": {
-            "state": "IN-KY", 
-            "name": "Evansville"
-        }
-    }, 
-    {
-        "pk": 296, 
-        "model": "core.city", 
-        "fields": {
-            "state": "WV-OH", 
-            "name": "Wheeling"
-        }
-    }, 
-    {
         "pk": 297, 
         "model": "core.city", 
         "fields": {
         }
     }, 
     {
-        "pk": 301, 
-        "model": "core.city", 
-        "fields": {
-            "state": "AR-OK", 
-            "name": "Fort Smith"
-        }
-    }, 
-    {
         "pk": 302, 
         "model": "core.city", 
         "fields": {
         }
     }, 
     {
-        "pk": 307, 
-        "model": "core.city", 
-        "fields": {
-            "state": "VA-WV", 
-            "name": "Winchester"
-        }
-    }, 
-    {
         "pk": 308, 
         "model": "core.city", 
         "fields": {
         }
     }, 
     {
-        "pk": 319, 
-        "model": "core.city", 
-        "fields": {
-            "state": "TN-KY", 
-            "name": "Clarksville"
-        }
-    }, 
-    {
-        "pk": 320, 
-        "model": "core.city", 
-        "fields": {
-            "state": "MD-WV", 
-            "name": "Hagerstown-Martinsburg"
-        }
-    }, 
-    {
         "pk": 321, 
         "model": "core.city", 
         "fields": {
         }
     }, 
     {
-        "pk": 323, 
-        "model": "core.city", 
-        "fields": {
-            "state": "TN-VA", 
-            "name": "Kingsport-Bristol-Bristol"
-        }
-    }, 
-    {
         "pk": 324, 
         "model": "core.city", 
         "fields": {
         }
     }, 
     {
-        "pk": 336, 
-        "model": "core.city", 
-        "fields": {
-            "state": "OH-PA", 
-            "name": "Youngstown-Warren-Boardman"
-        }
-    }, 
-    {
         "pk": 337, 
         "model": "core.city", 
         "fields": {
         }
     }, 
     {
-        "pk": 339, 
-        "model": "core.city", 
-        "fields": {
-            "state": "WV-OH", 
-            "name": "Parkersburg-Marietta-Vienna"
-        }
-    }, 
-    {
-        "pk": 340, 
-        "model": "core.city", 
-        "fields": {
-            "state": "ID-WA", 
-            "name": "Lewiston"
-        }
-    }, 
-    {
         "pk": 341, 
         "model": "core.city", 
         "fields": {
         }
     }, 
     {
-        "pk": 346, 
-        "model": "core.city", 
-        "fields": {
-            "state": "TX-AR", 
-            "name": "Texarkana-Texarkana"
-        }
-    }, 
-    {
         "pk": 347, 
         "model": "core.city", 
         "fields": {
         }
     }, 
     {
-        "pk": 358, 
-        "model": "core.city", 
-        "fields": {
-            "state": "WV-OH", 
-            "name": "Weirton-Steubenville"
-        }
-    }, 
-    {
         "pk": 359, 
         "model": "core.city", 
         "fields": {

example/core/forms.py

 from django import forms
+from django.forms.models import modelformset_factory
 from django.contrib.localflavor.us.forms import USStateField, USStateSelect
 
 import selectable.forms as selectable
 
 from example.core.lookups import FruitLookup, CityLookup
+from example.core.models import Farm
 
 
 class FruitForm(forms.Form):
         widget=selectable.AutoComboboxWidget(FruitLookup, allow_new=True),
         required=False,
     )
+    # AutoCompleteSelectField (no new items)
     autocompleteselect = selectable.AutoCompleteSelectField(
         lookup_class=FruitLookup,
         label='Select a fruit (AutoCompleteField)',
         required=False,
     )
+    # AutoCompleteSelectField (allows new items)
     newautocompleteselect = selectable.AutoCompleteSelectField(
         lookup_class=FruitLookup,
         allow_new=True,
         label='Select a fruit (AutoCompleteField which allows new items)',
         required=False,
     )
+    # AutoComboboxSelectField (no new items)
     comboboxselect = selectable.AutoComboboxSelectField(
         lookup_class=FruitLookup,
         label='Select a fruit (AutoComboboxSelectField)',
         required=False,
     )
+    # AutoComboboxSelectField (allows new items)
     newcomboboxselect = selectable.AutoComboboxSelectField(
         lookup_class=FruitLookup,
         allow_new=True,
         label='Select a fruit (AutoComboboxSelectField which allows new items)',
         required=False,
     )
+    # AutoCompleteSelectMultipleField
     multiautocompleteselect = selectable.AutoCompleteSelectMultipleField(
         lookup_class=FruitLookup,
         label='Select a fruit (AutoCompleteSelectMultipleField)',
         required=False,
     )
+    # AutoComboboxSelectMultipleField
     multicomboboxselect = selectable.AutoComboboxSelectMultipleField(
         lookup_class=FruitLookup,
         label='Select a fruit (AutoComboboxSelectMultipleField)',
     )
     state = USStateField(widget=USStateSelect, required=False)
 
+
+class FarmForm(forms.ModelForm):
+
+    class Meta(object):
+        model = Farm
+        widgets = {
+            'fruit': selectable.AutoCompleteSelectMultipleWidget(lookup_class=FruitLookup),
+        }
+
+
+FarmFormset = modelformset_factory(Farm, FarmForm)
+

example/core/lookups.py

 
 class FruitLookup(ModelLookup):
     model = Fruit
-    search_field = 'name__icontains'
-
+    search_fields = ('name__icontains', )
 
 registry.register(FruitLookup)
 
 
 class OwnerLookup(ModelLookup):
     model = User
-    search_field = 'username__icontains'
+    search_fields = ('username__icontains', )
 
 
 registry.register(OwnerLookup)
 
 class CityLookup(ModelLookup):
     model = City
-    search_field = 'name__icontains'
+    search_fields = ('name__icontains', )
 
     def get_query(self, request, term):
         results = super(CityLookup, self).get_query(request, term)

example/core/templates/base.html

 <head>
 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
 <title>Django-Selectable Example</title>
-<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.13/themes/ui-lightness/jquery-ui.css" type="text/css" media="screen">
+<link rel="stylesheet" href="//ajax.googleapis.com/ajax/libs/jqueryui/1.8.13/themes/ui-lightness/jquery-ui.css" type="text/css" />
 {{ form.media.css }}
 {% block extra-css %}{% endblock %}
-<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js"></script>
-<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.13/jquery-ui.min.js"></script>
+<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js"></script>
+<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jqueryui/1.8.13/jquery-ui.min.js"></script>
 {{ form.media.js }}
 {% block extra-js %}{% endblock %}
 </head>
     <body>
+        {% block content %}
         <form action="" method="post" >
             {% csrf_token %}
             {% if form.is_valid %}
             {% endif %}
             <button>Submit</button>
         </form>
+        {% endblock %}
     </body>
 </html>

example/core/templates/formset.html

+{% extends "base.html" %}
+
+{% block extra-css %}
+{{ formset.media.css }}
+{% endblock %}
+
+{% block extra-js %}
+<script type="text/javascript" src="http://django-dynamic-formset.googlecode.com/svn/trunk/src/jquery.formset.min.js"></script>
+<script type="text/javascript">
+    $(document).ready(function() {
+        $('#id_farm_table tbody tr').formset({
+            prefix: '{{ formset.prefix }}',
+       		added: function(row) {
+       			bindSelectables($(row));
+       		}	               		
+        });
+    });
+</script>
+{{ formset.media.js }}
+{% endblock %}
+
+
+{% block content %}
+    <form action="" method="post" >
+        {% csrf_token %}
+        <table id="id_farm_table" border="0" cellpadding="0" cellspacing="5">
+            <thead>
+                <tr>
+                    <th scope="col">Name</th>
+                    <th scope="col">Owner</th>
+                    <th scope="col">Fruit</th>
+                </tr>
+            </thead>
+            <tbody>
+                <tr>{{ formset.management_form }}</tr>
+                {% for form in formset.forms %}
+                <tr id="{{ form.prefix }}-row">
+                    <td>{{ form.name }}</td>
+                    <td>{{ form.owner }}</td>
+                    <td>{{ form.fruit }}</td>
+                </tr>
+                {% endfor %}
+            </tbody>
+        </table>
+        <button>Submit</button>
+    </form>
+{% endblock %}

example/core/urls.py

 
 
 urlpatterns = patterns('example.core.views',
+    url(r'^formset/', 'formset', name='example-formset'),    
     url(r'^advanced/', 'advanced', name='example-advanced'),
     url(r'^', 'index', name='example-index'),
 )

example/core/views.py

 from django.shortcuts import render_to_response
 from django.template import RequestContext
 
-from example.core.forms import FruitForm, ChainedForm
+from example.core.forms import FruitForm, ChainedForm, FarmFormset
 
 
 def index(request):
             form = ChainedForm()
 
     return render_to_response('advanced.html', {'form': form}, context_instance=RequestContext(request))
+
+
+def formset(request):
+
+    if request.method == 'POST':
+        formset = FarmFormset(request.POST)
+    else:
+        if request.GET:
+            formset = FarmFormset(initial=request.GET)
+        else:
+            formset = FarmFormset()
+
+    return render_to_response('formset.html', {'formset': formset}, context_instance=RequestContext(request))

example/templates/admin/base_site.html

 {% extends "admin/base.html" %}
 {% block title %}{{ title }} | Django-Selectable Example Site{% endblock title %}
 {% block extrahead %}
-<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.13/themes/ui-lightness/jquery-ui.css" type="text/css" media="screen">
-<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js"></script>
-<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.13/jquery-ui.min.js"></script>
+<link rel="stylesheet" href="//ajax.googleapis.com/ajax/libs/jqueryui/1.8.13/themes/ui-lightness/jquery-ui.css" type="text/css" />
+<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js"></script>
+<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jqueryui/1.8.13/jquery-ui.min.js"></script>
 {% endblock %}
 {% block branding %}
 <h1 id="site-name">Django-Selectable Administration</h1>

selectable/__init__.py

 
 __version_info__ = {
     'major': 0,
-    'minor': 2,
+    'minor': 3,
     'micro': 0,
-    'releaselevel': 'final',
+    'releaselevel': 'dev',
 }
 
 def get_version():

selectable/base.py

+import operator
 import re
 
 from django.conf import settings
 from django.core.paginator import Paginator, InvalidPage, EmptyPage
 from django.core.urlresolvers import reverse
 from django.core.serializers.json import DjangoJSONEncoder
+from django.db.models import Q
 from django.http import HttpResponse
 from django.utils import simplejson as json
 from django.utils.encoding import smart_unicode
     model = None
     filters = {}
     search_field = ''
+    search_fields = ()
+
+    def __init__(self):
+        super(ModelLookup, self).__init__()
+        if self.search_field and not self.search_fields:
+            self.search_fields = (self.search_field, )
 
     def get_query(self, request, term):
         qs = self.get_queryset()
-        if term and self.search_field:
-            qs = qs.filter(**{self.search_field: term})
+        if term:
+            search_filters = []
+            if self.search_fields:
+                for field in self.search_fields:
+                    search_filters.append(Q(**{field: term}))
+            qs = qs.filter(reduce(operator.or_, search_filters))
         return qs
 
     def get_queryset(self):
 
     def create_item(self, value):
         data = {}
-        if self.search_field:
-            field_name = re.sub(r'__\w+$', '',  self.search_field)
+        if self.search_fields:
+            field_name = re.sub(r'__\w+$', '',  self.search_fields[0])
             if field_name:
                 data = {field_name: value}
         return self.model(**data)

selectable/exceptions.py

+class LookupAlreadyRegistered(Exception):
+    "Exception when trying to register a lookup which is already registered."
+
+
+class LookupNotRegistered(Exception):
+    "Exception when trying use a lookup which is not registered."
+
+
+class LookupInvalid(Exception):
+    "Exception when register an invalid lookup class."

selectable/forms/widgets.py

 
 
 MEDIA_URL = settings.MEDIA_URL
-STATIC_URL = getattr(settings, 'STATIC_URL', '')
-MEDIA_PREFIX = STATIC_URL or MEDIA_URL
+STATIC_URL = getattr(settings, 'STATIC_URL', u'')
+MEDIA_PREFIX = u'%sselectable/' % STATIC_URL or MEDIA_URL
 
 
 class SelectableMediaMixin(object):
 
     class Media(object):
         css = {
-            'all': ('%scss/dj.selectable.css' % MEDIA_PREFIX, )
+            'all': (u'%scss/dj.selectable.css' % MEDIA_PREFIX, )
         }
-        js = ('%sjs/jquery.dj.selectable.js' % MEDIA_PREFIX, )
+        js = (u'%sjs/jquery.dj.selectable.js' % MEDIA_PREFIX, )
 
 
 class AutoCompleteWidget(forms.TextInput, SelectableMediaMixin):

selectable/registry.py

 from django.utils.encoding import force_unicode
 
-from selectable.base import LookupBase
-
-
-class LookupAlreadyRegistered(Exception):
-    pass
-
-
-class LookupNotRegistered(Exception):
-    pass
-
-
-class LookupInvalid(Exception):
-    pass
+from selectable.base import LookupBase, ModelLookup
+from selectable.exceptions import (LookupAlreadyRegistered, LookupNotRegistered,
+                                    LookupInvalid)
 
 
 class LookupRegistry(object):
     def validate(self, lookup):
         if not issubclass(lookup, LookupBase):
             raise LookupInvalid(u'Registered lookups must inherit from the LookupBase class')
+        if issubclass(lookup, ModelLookup) and getattr(lookup, 'search_field'):
+            import warnings
+            warnings.warn(
+                u"ModelLookup.search_field is deprecated; Use ModelLookup.search_fields instead.", 
+                DeprecationWarning
+            )
 
     def register(self, lookup):
 

selectable/static/css/dj.selectable.css

-ul.selectable-deck, ul.ui-autocomplete {
-    list-style: none outside none;
-}
-ul.selectable-deck li.selectable-deck-item,
-ul.ui-autocomplete li.ui-menu-item {
-    margin: 0;
-    list-style-type: none;
-}
-ul.selectable-deck li.selectable-deck-item .selectable-deck-remove {
-    float: right;
-}
-ul.selectable-deck-bottom-inline,
-ul.selectable-deck-top-inline {
-    padding: 0;
-}
-ul.selectable-deck-bottom-inline li.selectable-deck-item,
-ul.selectable-deck-top-inline li.selectable-deck-item {
-    display: inline;
-}
-ul.selectable-deck-bottom-inline li.selectable-deck-item .selectable-deck-remove,
-ul.selectable-deck-top-inline li.selectable-deck-item .selectable-deck-remove {
-    margin-left: 0.4em;
-    display: inline;
-    float: none;
-}

selectable/static/js/jquery.dj.selectable.js

-(function($) {
-
-	$.widget("ui.djselectable", {
-
-        options: {
-            removeIcon: "ui-icon-close",
-            comboboxIcon: "ui-icon-triangle-1-s",
-            prepareQuery: null
-        },
-        
-        _initDeck: function(hiddenInputs) {
-            var self = this;
-            var data = $(this.element).data();
-            var style = data.selectablePosition || data['selectable-position'] || 'bottom';
-            this.deck = $('<ul>').addClass('ui-widget selectable-deck selectable-deck-' + style);
-            if (style === 'bottom' || style === 'bottom-inline') {
-                $(this.element).after(this.deck);
-            } else {
-                $(this.element).before(this.deck);
-            }
-            $(hiddenInputs).each(function(i, input) {
-                self._addDeckItem(input);
-            });
-        },
-
-        _addDeckItem: function(input) {
-            var self = this;
-            $('<li>')
-            .text($(input).attr('title'))
-            .addClass('selectable-deck-item')
-            .appendTo(this.deck)
-            .append(
-                $('<div>')
-                .addClass('selectable-deck-remove')
-                .append(
-                    $('<button>')
-                    .button({
-                        icons: {
-                            primary: self.options.removeIcon
-                        },
-                        text: false
-                    })
-                    .click(function() {
-                        $(input).remove();
-                        $(this).closest('li').remove();
-                        return false;
-                    })
-                )
-            );
-        },
-
-        _create: function() {
-            var self = this,
-            input = this.element;
-            var data = $(input).data();
-            var allowNew = data.selectableAllowNew || data['selectable-allow-new'];
-            var allowMultiple = data.selectableMultiple || data['selectable-multiple'];
-            var textName = $(input).attr('name');
-            var hiddenName = textName.replace('_0', '_1');
-            var hiddenSelector = 'input[type=hidden][data-selectable-type=hidden-multiple][name=' + hiddenName + ']';
-            if (allowMultiple) {
-                allowNew = false;
-                $(input).val("");