Commits

Mark Lavin  committed 1e17a4a Merge

Merging the latest default work into stable.

  • Participants
  • Parent commits 5168a20, 8ee36b2
  • Branches stable

Comments (0)

Files changed (26)

 0000000000000000000000000000000000000000 version-0.2.0
 0000000000000000000000000000000000000000 version-0.2.0
 c6671545c9e32bf1635c684f16ee7fb645cae7dd version-0.2.0
+b0e1a2bcc104a8951db42f809aced3d3c9d018e1 version-0.3.0
 f04a62d8741f8ed6ff02da2ef9754b634b0b861d version-0.3.1
 The following people who have contributed to django-selectable:
 
 Colin Copeland
+Dan Poirier
 Karen Tracey
 
 Thanks for all of your work!
 - `v0.1.2 <http://readthedocs.org/docs/django-selectable/en/version-0.1.2/>`_
 
 
+Additional Help/Support
+-----------------------------------
+
+You can find additional help or support on the mailing list: http://groups.google.com/group/django-selectable
+
 
 Contributing
 --------------------------------------

File docs/admin.rst

 fields and widgets in the admin make sure you are familiar with the Django
 documentation on the `ModelAdmin.form <http://docs.djangoproject.com/en/1.3/ref/contrib/admin/#django.contrib.admin.ModelAdmin.form>`_ 
 and `ModelForms <http://docs.djangoproject.com/en/1.3/topics/forms/modelforms/>`_ particularly
-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.
+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-selectable in the adminis the same as working with regular forms.
 
 .. _admin-jquery-include:
 
 the ``ModelForm`` will no longer do this for you. Since ``fruit`` does not allow new
 items you'll see these steps are not necessary.
 
+.. versionadded:: 0.4
+
+The django-selectable widgets are compatitible with the add anther popup in the
+admin. It's that little green plus sign that appears next to ``ForeignKey`` or
+``ManyToManyField`` items. This makes django-selectable a user friendly replacement
+for the `ModelAdmin.raw_id_fields <https://docs.djangoproject.com/en/1.3/ref/contrib/admin/#django.contrib.admin.ModelAdmin.raw_id_fields>`_ 
+when the default select box grows too long.
+
 
 .. _admin-inline-example:
 

File docs/advanced.rst

 use of jQuery.
 
 
+.. _additional-parameters:
+
 Additional Parameters
 --------------------------------------
 
                 super(FruitForm, self).__init__(*args, **kwargs)
                 self.fields['autocomplete'].widget.update_query_parameters({'foo': 'bar'})
 
+.. versionadded:: 0.4
+
+You can also pass the query parameters into the widget using the ``query_params``
+keyword argument. It depends on your use case as to whether the parameters are
+know when the form is defined or when an instance of the form is created.
+
 
 .. _client-side-parameters:
 

File docs/conf.py

 # built documents.
 #
 # The short X.Y version.
-version = '0.3'
+version = '0.4'
 # The full version, including alpha/beta/rc tags.
-release = '0.3.1'
+release = '0.4.0dev'
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.

File docs/contribute.rst

 
     python selectable/tests/runtests.py
 
-Tests for the client side code is planned. If javascript testing is something you
-are familiar with then it would be a great help to this project.
+Client side tests are written using `QUnit <http://docs.jquery.com/QUnit>`_. They
+can be found in `selectable/tests/qunit/`.
 
 
 Building the Documentation

File docs/quick-start.rst

 
         registry.register(FruitLookup)
 
+.. note::
+
+    You should only register your lookup once. Attempting to register the same lookup class
+    more than once will lead to ``LookupAlreadyRegistered`` errors. A common problem related to the
+    ``LookupAlreadyRegistered`` error is related to inconsistant import paths in your project.
+    Prior to Django 1.4 the default ``manage.py`` allows for importing both with and without
+    the project name (i.e. ``from myproject.myapp import lookups`` or ``from myapp import lookups``).
+    This leads to the ``lookup.py`` file being imported twice and the registration code
+    executing twice. Thankfully this is no longer the default in Django 1.4. Keeping
+    your import consistant to include the project name (when your app is included inside the
+    project directory) will avoid these errors.
+
 
 Defining Forms
 --------------------------------

File docs/releases.rst

 Release Notes
 ==================
 
+v0.4.0 (Released TBD)
+--------------------------------------
+
+Features
+_________________
+
+- Better compatibility with :ref:`AutoCompleteSelectWidget`/:ref:`AutoComboboxSelectWidget` and Django's ModelChoiceField
+- Better compatibility with the Django admin :ref:`add another popup <admin-basic-example>`
+- Easier passing of query parameters. See the :ref:`Additional Parameters <additional-parameters>` section
+- Additional documentation
+- QUnit tests for JS functionality
+
+
+Backwards Incompatible Changes
+________________________________
+
+- Support for ``ModelLookup.search_field`` string has been removed. You should use the ``ModelLookup.search_fields`` tuple instead.
+
+
 v0.3.1 (Released 2012-02-23)
 --------------------------------------
 

File docs/settings.rst

 SELECTABLE_MAX_LIMIT
 --------------------------------------
 
-.. versionadded:: 0.2
-
 This setting is used to limit the number of results returned by the auto-complete fields.
 Each field/widget can individually lower this maximum. The result sets will be
 paginated allowing the client to ask for more results. The limit is passed as a
 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:
 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/
 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.

File docs/widgets.rst

 Below are the custom widgets defined by Django-Selectable. All widgets take the 
 lookup class as the first required argument.
 
+.. versionadded:: 0.4
+
+These widgets all support a ``query_params`` keyword argument which is used to pass
+additional query parameters to the lookup search. See the section on 
+:ref:`Adding Parameters on the Server Side <server-side-parameters>` for more
+information.
+
 
 .. _AutoCompleteWidget:
 
 This widget should be used in conjunction with the :ref:`AutoCompleteSelectField` as it will
 return both the text entered by the user and the id (if an item was selected/matched).
 
+.. versionadded:: 0.4
+
+Prior to 0.4 :ref:`AutoCompleteSelectWidget` could not work directly with Django's
+`ModelChoiceField <https://docs.djangoproject.com/en/1.3/ref/forms/fields/#modelchoicefield>`_.
+Starting with 0.4 you can simply replace the widget without replacing the entire field.
+
+    .. code-block:: python
+
+        class FarmAdminForm(forms.ModelForm):
+
+            class Meta(object):
+                model = Farm
+                widgets = {
+                    'owner': selectable.AutoCompleteSelectWidget(lookup_class=FruitLookup),
+                }
+
+The one catch is that you must use ``allow_new=False`` which is the default.
+
 
 .. _AutoComboboxSelectWidget:
 
 
 Similar to :ref:`AutoCompleteSelectWidget` but has a button to reveal all options.
 
+.. versionadded:: 0.4
+
+Prior to 0.4 :ref:`AutoComboboxSelectWidget` could not work directly with Django's
+`ModelChoiceField <https://docs.djangoproject.com/en/1.3/ref/forms/fields/#modelchoicefield>`_.
+Starting with 0.4 you can simply replace the widget without replacing the entire field.
+
+    .. code-block:: python
+
+        class FarmAdminForm(forms.ModelForm):
+
+            class Meta(object):
+                model = Farm
+                widgets = {
+                    'owner': selectable.AutoComboboxSelectWidget(lookup_class=FruitLookup),
+                }
+
+The one catch is that you must use ``allow_new=False`` which is the default.
+
 
 .. _AutoCompleteSelectMultipleWidget:
 

File selectable/__init__.py

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

File selectable/base.py

 class ModelLookup(LookupBase):
     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:

File selectable/forms/fields.py

     def to_python(self, value):
         if value in EMPTY_VALUES:
             return None
+        lookup =self.lookup_class()
         if isinstance(value, list):
             # Input comes from an AutoComplete widget. It's two
             # components: text and id
             if len(value) != 2:
                 raise ValidationError(self.error_messages['invalid_choice'])
-            lookup =self.lookup_class()
             if value[1] in EMPTY_VALUES:
                 if not self.allow_new:
                     if value[0]:
                 value = lookup.get_item(value[1])
                 if value is None:
                     raise ValidationError(self.error_messages['invalid_choice'])
+        else:
+            value = lookup.get_item(value)
+            if value is None:
+                raise ValidationError(self.error_messages['invalid_choice'])
         return value
 
 

File selectable/forms/widgets.py

     def __init__(self, lookup_class, *args, **kwargs):
         self.lookup_class = lookup_class
         self.allow_new = kwargs.pop('allow_new', False)
-        self.qs = {}
+        self.qs = kwargs.pop('query_params', {})
         self.limit = kwargs.pop('limit', None)
         super(AutoCompleteWidget, self).__init__(*args, **kwargs)
 
         self.lookup_class = lookup_class
         self.allow_new = kwargs.pop('allow_new', False)
         self.limit = kwargs.pop('limit', None)
+        query_params = kwargs.pop('query_params', {})
         widgets = [
-            AutoCompleteWidget(lookup_class, allow_new=self.allow_new, limit=self.limit),
+            AutoCompleteWidget(
+                lookup_class, allow_new=self.allow_new,
+                limit=self.limit, query_params=query_params
+            ),
             forms.HiddenInput(attrs={u'data-selectable-type': 'hidden'})
         ]
         super(AutoCompleteSelectWidget, self).__init__(widgets, *args, **kwargs)
             return [item_value, value]
         return [None, None]
 
+    def value_from_datadict(self, data, files, name):
+        value = super(AutoCompleteSelectWidget, self).value_from_datadict(data, files, name)
+        if not self.allow_new:
+            return value[1]
+        return value
+
 
 class AutoComboboxWidget(AutoCompleteWidget, SelectableMediaMixin):
 
         self.lookup_class = lookup_class
         self.allow_new = kwargs.pop('allow_new', False)
         self.limit = kwargs.pop('limit', None)
+        query_params = kwargs.pop('query_params', {})
         widgets = [
-            AutoComboboxWidget(lookup_class, allow_new=self.allow_new, limit=self.limit),
+            AutoComboboxWidget(
+                lookup_class, allow_new=self.allow_new,
+                limit=self.limit, query_params=query_params
+            ),
             forms.HiddenInput(attrs={u'data-selectable-type': 'hidden'})
         ]
         super(AutoComboboxSelectWidget, self).__init__(widgets, *args, **kwargs)
             return [item_value, value]
         return [None, None]
 
+    def value_from_datadict(self, data, files, name):
+        value = super(AutoComboboxSelectWidget, self).value_from_datadict(data, files, name)
+        if not self.allow_new:
+            return value[1]
+        return value
+
 
 class LookupMultipleHiddenInput(forms.MultipleHiddenInput):
 
             u'data-selectable-multiple': 'true',
             u'data-selectable-position': position
         }
+        query_params = kwargs.pop('query_params', {})
         widgets = [
-            AutoCompleteWidget(lookup_class, allow_new=False, limit=self.limit, attrs=attrs),
+            AutoCompleteWidget(
+                lookup_class, allow_new=False,
+                limit=self.limit, query_params=query_params, attrs=attrs
+            ),
             LookupMultipleHiddenInput(lookup_class)
         ]
         super(AutoCompleteSelectMultipleWidget, self).__init__(widgets, *args, **kwargs)
             u'data-selectable-multiple': 'true',
             u'data-selectable-position': position
         }
+        query_params = kwargs.pop('query_params', {})
         widgets = [
-            AutoComboboxWidget(lookup_class, allow_new=False, limit=self.limit, attrs=attrs),
+            AutoComboboxWidget(
+                lookup_class, allow_new=False,
+                limit=self.limit, query_params=query_params, attrs=attrs
+            ),
             LookupMultipleHiddenInput(lookup_class)
         ]
         super(AutoComboboxSelectMultipleWidget, self).__init__(widgets, *args, **kwargs)

File selectable/registry.py

     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'):
+        if issubclass(lookup, ModelLookup) and getattr(lookup, 'search_field', None):
             import warnings
             warnings.warn(
                 u"ModelLookup.search_field is deprecated; Use ModelLookup.search_fields instead.", 

File selectable/static/selectable/css/dj.selectable.css

+/* 
+ * django-selectable UI widget CSS
+ * Source: https://bitbucket.org/mlavin/django-selectable
+ * Docs: http://django-selectable.readthedocs.org/
+ *
+ * Copyright 2010-2012, Mark Lavin
+ * BSD License
+ *
+*/
 ul.selectable-deck, ul.ui-autocomplete {
     list-style: none outside none;
 }

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

+/* 
+ * django-selectable UI widget
+ * Source: https://bitbucket.org/mlavin/django-selectable
+ * Docs: http://django-selectable.readthedocs.org/
+ *
+ * Depends:
+ *   - jQuery 1.4.3+
+ *   - jQuery UI 1.8 widget factory
+ *
+ * Copyright 2010-2012, Mark Lavin
+ * BSD License
+ *
+*/
 (function($) {
 
 	$.widget("ui.djselectable", {
             formatLabel: null
         },
         
-        _initDeck: function(hiddenInputs) {
+        _initDeck: function() {
+            /* Create list display for currently selected items for multi-select */
             var self = this;
             var data = $(this.element).data();
             var style = data.selectablePosition || data['selectable-position'] || 'bottom';
             } else {
                 $(this.element).before(this.deck);
             }
-            $(hiddenInputs).each(function(i, input) {
+            $(self.hiddenMultipleSelector).each(function(i, input) {
                 self._addDeckItem(input);
             });
         },
 
         _addDeckItem: function(input) {
+            /* Add new deck list item from a given hidden input */
             var self = this;
             $('<li>')
             .text($(input).attr('title'))
             );
         },
 
-        _create: function() {
+        select: function(item) {
+            /* Trigger selection of a given item.
+            Item should contain two properties: id and value */
             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).removeClass('ui-state-error');
+            if (item) {
+                if (self.allowMultiple) {
+                    $(input).val("");
+                    $(input).data("autocomplete").term = "";
+                    if ($(self.hiddenMultipleSelector + '[value=' + item.id + ']').length === 0) {
+                        var newInput = $('<input />', {
+                            'type': 'hidden',
+                            'name': self.hiddenName,
+                            'value': item.id,
+                            'title': item.value,
+                            'data-selectable-type': 'hidden-multiple'
+                        });
+                        $(input).after(newInput);
+                        self._addDeckItem(newInput);
+                        return false;
+                    }
+                } else {
+                    $(input).val(item.value);
+                    var ui = {item: item};
+                    $(input).trigger('autocompleteselect', [ui ]);
+                }
+            }
+        },
+
+        _create: function() {
+            /* Initialize a new selectable widget */
+            var self = this,
+            input = this.element,
+            data = $(input).data();
+            self.allowNew = data.selectableAllowNew || data['selectable-allow-new'];
+            self.allowMultiple = data.selectableMultiple || data['selectable-multiple'];
+            self.textName = $(input).attr('name');
+            self.hiddenName = self.textName.replace('_0', '_1');
+            self.hiddenSelector = ':input[data-selectable-type=hidden][name=' + self.hiddenName + ']';
+            self.hiddenMultipleSelector = ':input[data-selectable-type=hidden-multiple][name=' + self.hiddenName + ']';
+            if (self.allowMultiple) {
+                self.allowNew = false;
                 $(input).val("");
-                this._initDeck(hiddenSelector);
+                this._initDeck();
             }
 
             function dataSource(request, response) {
+                /* Custom data source to uses the lookup url with pagination
+                Adds hook for adjusting query parameters.
+                Includes timestamp to prevent browser caching the lookup. */
                 var url = data.selectableUrl || data['selectable-url'];
                 var now = new Date().getTime();
                 var query = {term: request.term, timestamp: now};
                 }
 				$.getJSON(url, query, response);
             }
-
+            // Create base auto-complete lookup
             $(input).autocomplete({
                 source: dataSource,
                 change: function(event, ui) {
                     $(input).removeClass('ui-state-error');
                     if ($(input).val() && !ui.item) {
-                        if (!allowNew) {
+                        if (!self.allowNew) {
                             $(input).addClass('ui-state-error');
                         } 
                     } 
-                    if (allowMultiple && !$(input).hasClass('ui-state-error')) {
+                    if (self.allowMultiple && !$(input).hasClass('ui-state-error')) {
                         $(input).val("");
-	                    $(input).data("autocomplete").term = "";
+                        $(input).data("autocomplete").term = "";
                     }
                 },
                 select: function(event, ui) {
                     $(input).removeClass('ui-state-error');
                     if (ui.item && ui.item.page) {
-                        $(input).data("page", ui.item.page);
+                        // Set current page value
+                        $(input).data("page", ui.tem.page);
                         $('.selectable-paginator', self.menu).remove();
+                        // Search for next page of results
                         $(input).autocomplete("search");
                         return false;
                     }
-                    if (ui.item && allowMultiple) {
-                        $(input).val("");
-		                $(input).data("autocomplete").term = "";
-                        if ($(hiddenSelector + '[value=' + ui.item.id + ']').length === 0) {
-                            var newInput = $('<input />', {
-                                'type': 'hidden',
-                                'name': hiddenName,
-                                'value': ui.item.id,
-                                'title': ui.item.value,
-                                'data-selectable-type': 'hidden-multiple'
-                            });
-                            $(input).after(newInput);
-                            self._addDeckItem(newInput);
-                            return false;
-                        }
-                    }
+                    self.select(ui.item);
                 }
             }).addClass("ui-widget ui-widget-content ui-corner-all");
+            // Override the default auto-complete render.
             $(input).data("autocomplete")._renderItem = function(ul, item) {
+                /* Adds hook for additional formatting, allows HTML in the label,
+                highlights term matches and handles pagination. */
                 var label = item.label;
                 if (self.options.formatLabel) {
                     label = self.options.formatLabel(label, item);
                     label = label.replace(re, "<span class='highlight'>$1</span>");
                 }
                 var li =  $("<li></li>")
-			        .data("item.autocomplete", item)
-			        .append($("<a></a>").append(label))
-			        .appendTo(ul);
+                    .data("item.autocomplete", item)
+                    .append($("<a></a>").append(label))
+                    .appendTo(ul);
                 if (item.page) {
                     li.addClass('selectable-paginator');
                 }
-	            return li;
+                return li;
             };
+            // Override the default auto-complete suggest.
             $(input).data("autocomplete")._suggest = function(items) {
+                /* Needed for handling pagination links */
                 var page = $(input).data('page');
                 var ul = this.menu.element;
                 if (!page) {
                     ul.empty();
                 }
                 $(input).data('page', null);
-			    ul.zIndex(this.element.zIndex() + 1);
-		        this._renderMenu(ul, items);
-	            this.menu.deactivate();
+                ul.zIndex(this.element.zIndex() + 1);
+                this._renderMenu(ul, items);
+                this.menu.deactivate();
                 this.menu.refresh();
-		        // size and position menu
-		        ul.show();
-		        this._resizeMenu();
-		        ul.position($.extend({of: this.element}, this.options.position));
-		        if (this.options.autoFocus) {
-			        this.menu.next(new $.Event("mouseover"));
-		        }
-	        };
+                // size and position menu
+                ul.show();
+                this._resizeMenu();
+                ul.position($.extend({of: this.element}, this.options.position));
+                if (this.options.autoFocus) {
+                    this.menu.next(new $.Event("mouseover"));
+                }
+            };
+            // Additional work for combobox widgets
             var selectableType = data.selectableType || data['selectable-type'];
             if (selectableType === 'combobox') {
                 // Change auto-complete options
                 })
                 .removeClass("ui-corner-all")
                 .addClass("ui-corner-left ui-combo-input");
-
+                // Add show all items button
                 $("<button>&nbsp;</button>").attr("tabIndex", -1).attr("title", "Show All Items")
                 .insertAfter($(input))
                 .button({
                     text: false
                 })
                 .removeClass("ui-corner-all")
-		        .addClass("ui-corner-right ui-button-icon ui-combo-button")
+                .addClass("ui-corner-right ui-button-icon ui-combo-button")
                 .click(function() {
                     // close if already visible
                     if ($(input).autocomplete("widget").is(":visible")) {
                         $(input).autocomplete("close");
                         return false;
                     }
-
                     // pass empty string as value to search for, displaying all results
                     $(input).autocomplete("search", "");
                     $(input).focus();
 })(jQuery);
 
 function bindSelectables(context) {
+    /* Bind all selectable widgets in a given context.
+    Automatically called on document.ready.
+    Additional calls can be made for dynamically added widgets.
+    */
     $(":input[data-selectable-type=text]", context).djselectable();
     $(":input[data-selectable-type=combobox]", context).djselectable();
     $(":input[data-selectable-type=hidden]", context).each(function(i, elem) {
     });
 }
 
-if (typeof(django) != "undefined" && typeof(django.jQuery) != "undefined") {
+/* Monkey-patch Django's dynamic formset, if defined */
+if (typeof(django) !== "undefined" && typeof(django.jQuery) !== "undefined") {
     if (django.jQuery.fn.formset) {
         var oldformset = django.jQuery.fn.formset;
-	    django.jQuery.fn.formset = function(opts) {
+        django.jQuery.fn.formset = function(opts) {
             var options = $.extend({}, opts);
             var addedevent = function(row) {
                 bindSelectables($(row));
             };
             var added = null;
             if (options.added) {
+                // Wrap previous added function and include call to bindSelectables
                 var oldadded = options.added;
                 added = function(row) { oldadded(row); addedevent(row); };
             }
     }
 }
 
+/* Monkey-patch Django's dismissAddAnotherPopup(), if defined */
+if (typeof(dismissAddAnotherPopup) !== "undefined" && typeof(windowname_to_id) !== "undefined" && typeof(html_unescape) !== "undefined") {
+    var django_dismissAddAnotherPopup = dismissAddAnotherPopup;
+    dismissAddAnotherPopup = function(win, newId, newRepr) {
+        /* See if the popup came from a selectable field.
+           If not, pass control to Django's code.
+           If so, handle it. */
+        var fieldName = windowname_to_id(win.name); /* e.g. "id_fieldname" */
+        var field = $('#' + fieldName);
+        var multiField = $('#' + fieldName + '_0');
+        /* Check for bound selectable */
+        var singleWidget = field.data('djselectable');
+        var multiWidget = multiField.data('djselectable');
+        if (singleWidget || multiWidget) {
+            // newId and newRepr are expected to have previously been escaped by
+            // django.utils.html.escape.
+            var item =  {
+                id: html_unescape(newId),
+                value: html_unescape(newRepr)
+            };
+            if (singleWidget) {
+                field.djselectable('select', item);
+            }
+            if (multiWidget) {
+                multiField.djselectable('select', item);
+            }
+            win.close();
+        } else {
+            /* Not ours, pass on to original function. */
+            return django_dismissAddAnotherPopup(win, newId, newRepr);
+        }
+    };
+}
+
 $(document).ready(function() {
-    bindSelectables('body');
+    // Bind existing widgets on document ready
+    if (typeof(djselectableAutoLoad) === "undefined" || djselectableAutoLoad) {
+        bindSelectables('body');
+    }
 });

File selectable/tests/__init__.py

 
 class ThingLookup(ModelLookup):
     model = Thing
-    search_field = 'name__icontains'
+    search_fields = ('name__icontains', )
 
 
 registry.register(ThingLookup)

File selectable/tests/base.py

 __all__ = (
     'ModelLookupTestCase',
     'MultiFieldLookupTestCase',
-    'LegacyModelLookupTestCase',
 )
 
 
         qs = lookup.get_query(request=None, term='other')
         self.assertTrue(thing.pk not in qs.values_list('id', flat=True))
         self.assertTrue(other_thing.pk in qs.values_list('id', flat=True))
-
-
-class LegacyModelLookup(ModelLookup):
-    model = Thing
-    search_field = 'name__icontains'
-
-
-class LegacyModelLookupTestCase(ModelLookupTestCase):
-    lookup_cls = LegacyModelLookup
-
-    def test_get_name(self):
-        name = self.__class__.lookup_cls.name()
-        self.assertEqual(name, 'tests-legacymodellookup')
-
-    def test_get_url(self):
-        url = self.__class__.lookup_cls.url()
-        test_url = reverse('selectable-lookup', args=['tests-legacymodellookup'])
-        self.assertEqual(url, test_url)

File selectable/tests/functests.py

 from django import forms
 
 from selectable.forms import AutoCompleteSelectField
+from selectable.forms import AutoCompleteSelectWidget, AutoComboboxSelectWidget
 from selectable.tests import OtherThing, ThingLookup
 from selectable.tests.base import BaseSelectableTestCase
 
 
 __all__ = (
     'FuncAutoCompleteSelectTestCase',
+    'FuncSelectModelChoiceTestCase',
+    'FuncComboboxModelChoiceTestCase',
 )
 
 
         # Selected pk should be populated
         thing_1 = 'name="thing_1" value="%s"' % self.test_thing.pk
         self.assertTrue(thing_1 in rendered_form, u"Didn't render selected pk.")
+
+
+class SelectWidgetForm(forms.ModelForm):
+
+    class Meta(object):
+        model = OtherThing
+        widgets = {
+            'thing': AutoCompleteSelectWidget(lookup_class=ThingLookup)
+        }
+
+
+class FuncSelectModelChoiceTestCase(BaseSelectableTestCase):
+    """
+    Functional tests for AutoCompleteSelectWidget compatibility
+    with a ModelChoiceField.
+    """
+
+    def setUp(self):
+        self.test_thing = self.create_thing()
+
+    def test_valid_form(self):
+        "Valid form using an AutoCompleteSelectField."
+        data = {
+            'name': self.get_random_string(),
+            'thing_0': self.test_thing.name, # Text input
+            'thing_1': self.test_thing.pk, # Hidden input
+        }
+        form = SelectWidgetForm(data=data)
+        self.assertTrue(form.is_valid(), str(form.errors))
+
+
+class ComboboxSelectWidgetForm(forms.ModelForm):
+
+    class Meta(object):
+        model = OtherThing
+        widgets = {
+            'thing': AutoComboboxSelectWidget(lookup_class=ThingLookup)
+        }
+
+
+class FuncComboboxModelChoiceTestCase(BaseSelectableTestCase):
+    """
+    Functional tests for AutoComboboxSelectWidget compatibility
+    with a ModelChoiceField.
+    """
+
+    def setUp(self):
+        self.test_thing = self.create_thing()
+
+    def test_valid_form(self):
+        "Valid form using an AutoCompleteSelectField."
+        data = {
+            'name': self.get_random_string(),
+            'thing_0': self.test_thing.name, # Text input
+            'thing_1': self.test_thing.pk, # Hidden input
+        }
+        form = ComboboxSelectWidgetForm(data=data)
+        self.assertTrue(form.is_valid(), str(form.errors))

File selectable/tests/qunit/index.html

+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+<head>
+    <title>Django Selectable Qunit Test Suite</title>
+    <link rel="stylesheet" href="qunit.css" type="text/css" media="screen" />
+    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js"></script>
+    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.13/jquery-ui.min.js"></script>
+    <script type="text/javascript" src="qunit.js"></script>
+    <script>
+        // Disable auto-binding of djselectable widgets to avoid test race conditions
+        var djselectableAutoLoad = false;
+    </script>
+    <script type="text/javascript" src="../../static/selectable/js/jquery.dj.selectable.js"></script>
+    <script>
+    $(document).ready(function(){
+        module("Text Autocomplete");
+        test("bind input", function() {
+            bindSelectables('#text');            
+            var input = $('#id_autocomplete');
+            ok(input.data('djselectable'), "input should be bound with djselecable widget");
+        });
+        test("item selection", function() {
+            bindSelectables('#text-initial');
+            var input = $('#id_autocomplete-initial');
+            ok(input.val(), 'Initial', "initial text value should not be lost");
+            var item = {id: "1", value: 'foo'};
+            input.djselectable('select', item);
+            ok(input.val(), item.value, "input should get item value");
+        });
+
+        module("Text Combobox");
+        test("bind input", function() {
+            bindSelectables('#combobox');
+            var input = $('#id_combobox');
+            ok(input.data('djselectable'), "input should be bound with djselecable widget");
+            var button = $('button', '#combobox');
+            equal(button.attr('title'), 'Show All Items', "combobox button should be created");
+        });
+        test("item selection", function() {
+            bindSelectables('#combobox-initial');
+            var input = $('#id_autocomplete-initial');
+            ok(input.val(), 'Initial', "initial text value should not be lost");
+            var item = {id: "1", value: 'foo'};
+            input.djselectable('select', item);
+            ok(input.val(), item.value, "input should get item value");
+        });
+
+        module("Select Autocomplete");
+        test("bind input", function() {
+            bindSelectables('#auto-select');
+            var input = $('#id_autoselect_0');
+            var hidden = $('#id_autoselect_1');
+            ok(input.data('djselectable'), "input should be bound with djselecable widget");
+            ok(!hidden.data('djselectable'), "hidden should not be bound with djselecable widget");
+        });
+        test("item selection", function() {
+            bindSelectables('#auto-select-initial');
+            var input = $('#id_autoselect-initial_0');
+            var hidden = $('#id_autoselect-initial_1');
+            equal(input.val(), 'Initial', 'initial text value should not be lost');
+            equal(hidden.val(), "0", 'initial hidden value should not be lost');
+            var item = {id: "1", value: 'foo'};
+            input.djselectable('select', item);
+            equal(input.val(), item.value, 'text input should get item value');
+            equal(hidden.val(), item.id, 'hidden input should get item id');
+        });
+
+        module("Select Combobox");
+        test("bind input", function() {
+            bindSelectables('#combo-select');
+            var input = $('#id_comboselect_0');
+            var hidden = $('#id_comboselect_1');
+            ok(input.data('djselectable'), "input should be bound with djselecable widget");
+            ok(!hidden.data('djselectable'), "hidden should not be bound with djselecable widget");
+            var button = $('button', '#combo-select');
+            equal(button.attr('title'), 'Show All Items', "combobox button should be created");
+        });
+        test("item selection", function() {
+            bindSelectables('#combo-select-initial');
+            var input = $('#id_comboselect-initial_0');
+            var hidden = $('#id_comboselect-initial_1');
+            equal(input.val(), 'Initial', 'initial text value should not be lost');
+            equal(hidden.val(), "0", 'initial hidden value should not be lost');
+            var item = {id: "1", value: 'foo'};
+            input.djselectable('select', item);
+            equal(input.val(), item.value, 'text input should get item value')
+            equal(hidden.val(), item.id, 'hidden input should get item id');
+        });
+
+        module("Select Multiple");
+        test("bind input", function() {
+            bindSelectables('#multi-select');
+            var input = $('#id_multiselect_0');
+            var deck = $('#multi-select ul');
+            ok(input.data('djselectable'), "input should be bound with djselecable widget");
+            ok(deck.hasClass('selectable-deck'), 'deck should have selectable-deck class');
+            equal($('li', deck).length, 0, 'no initial deck items');
+        }); 
+        test("item selection", function() {
+            bindSelectables('#multi-select-initial');
+            var input = $('#id_multiselect-initial_0');
+            var hidden = $(':input[type=hidden][name=multiselect-initial_1]');
+            var deck = $('#multi-select-initial ul');
+            equal($('li', deck).length, 1, 'one initial deck item');
+            equal(hidden.length, 1, 'one initial hidden input');
+            var item = {id: "1", value: 'foo'};
+            input.djselectable('select', item);
+            equal(input.val(), '', 'text input should be empty');
+            hidden = $(':input[type=hidden][name=multiselect-initial_1]');
+            equal(hidden.length, 2, 'new hidden input');
+            equal($('li', deck).length, 2, 'new deck item');
+            hidden = hidden.eq(0);
+            equal(hidden.val(), item.id, 'hidden input should get item id');
+            equal(hidden.attr('title'), item.value, 'hidden input title should be item value');
+        });
+
+        module("Combobox Multiple");
+        test("bind input", function() {
+            bindSelectables('#multi-combo');
+            var input = $('#id_multicombo_0');
+            var deck = $('#multi-combo ul');
+            ok(input.data('djselectable'), "input should be bound with djselecable widget");
+            ok(deck.hasClass('selectable-deck'), 'deck should have selectable-deck class');
+            equal($('li', deck).length, 0, 'no initial deck items');
+            var button = $('button', '#multi-combo');
+            equal(button.attr('title'), 'Show All Items', "combobox button should be created");
+        }); 
+        test("item selection", function() {
+            bindSelectables('#multi-combo-initial');
+            var input = $('#id_multicombo-initial_0');
+            var hidden = $(':input[type=hidden][name=multicombo-initial_1]');
+            var deck = $('#multi-combo-initial ul');
+            equal($('li', deck).length, 1, 'one initial deck item');
+            equal(hidden.length, 1, 'one initial hidden input');
+            var item = {id: "1", value: 'foo'};
+            input.djselectable('select', item);
+            equal(input.val(), '', 'text input should be empty');
+            hidden = $(':input[type=hidden][name=multicombo-initial_1]');
+            equal(hidden.length, 2, 'new hidden input');
+            equal($('li', deck).length, 2, 'new deck item');
+            hidden = hidden.eq(0);
+            equal(hidden.val(), item.id, 'hidden input should get item id');
+            equal(hidden.attr('title'), item.value, 'hidden input title should be item value');
+        });
+    });
+    </script>
+</head>
+<body>
+    <h1 id="qunit-header">Django Selectable Test Suite</h1>
+    <h2 id="qunit-banner"></h2>
+    <div id="qunit-testrunner-toolbar"></div>
+    <h2 id="qunit-userAgent"></h2>
+    <ol id="qunit-tests"></ol>
+    <div id="qunit-fixture">
+        <div id="text">
+            <input name="autocomplete" data-selectable-type="text" data-selectable-allow-new="false" data-selectable-url="/_/core-fruitlookup/" type="text" id="id_autocomplete" />
+        </div>
+        <div id="text-initial">
+            <input name="autocomplete-initial" data-selectable-type="text" data-selectable-allow-new="false" data-selectable-url="/_/core-fruitlookup/" type="text" id="id_autocomplete-initial" value="Initial" />
+        </div>
+        <div id="combobox">
+            <input name="combobox" data-selectable-type="combobox" data-selectable-allow-new="false" data-selectable-url="/_/core-fruitlookup/" type="text" id="id_combobox" />
+        </div>
+        <div id="combobox-initial">
+            <input name="combobox-initial" data-selectable-type="combobox" data-selectable-allow-new="false" data-selectable-url="/_/core-fruitlookup/" type="text" id="id_combobox-initial" value="Initial" />
+        </div>
+        <div id="auto-select">
+            <input name="autoselect_0" data-selectable-type="text" data-selectable-allow-new="false" data-selectable-url="/_/core-fruitlookup/" type="text" id="id_autoselect_0" />
+            <input name="autoselect_1" data-selectable-type="hidden" type="hidden" id="id_autoselect_1" />
+        </div>
+        <div id="auto-select-initial">
+            <input name="autoselect-initial_0" data-selectable-type="text" data-selectable-allow-new="false" data-selectable-url="/_/core-fruitlookup/" type="text" id="id_autoselect-initial_0" value="Initial" />
+            <input name="autoselect-initial_1" data-selectable-type="hidden" type="hidden" id="id_autoselect-initial_1" value="0" />
+        </div>
+        <div id="combo-select">
+            <input name="comboselect_0" data-selectable-type="combobox" data-selectable-allow-new="false" data-selectable-url="/_/core-fruitlookup/" type="text" id="id_comboselect_0" />
+            <input name="comboselect_1" data-selectable-type="hidden" type="hidden" id="id_comboselect_1" />
+        </div>
+        <div id="combo-select-initial">
+            <input name="comboselect-initial_0" data-selectable-type="combobox" data-selectable-allow-new="false" data-selectable-url="/_/core-fruitlookup/" type="text" id="id_comboselect-initial_0" value="Initial" />
+            <input name="comboselect-initial_1" data-selectable-type="hidden" type="hidden" id="id_comboselect-initial_1" value="0" />
+        </div>
+        <div id="multi-select">
+            <input data-selectable-multiple="true" name="multiselect_0" data-selectable-type="text" data-selectable-position="bottom" data-selectable-allow-new="false" data-selectable-url="/_/core-fruitlookup/" type="text" id="id_multiselect_0" />
+        </div>
+        <div id="multi-select-initial">
+            <input data-selectable-multiple="true" name="multiselect-initial_0" data-selectable-type="text" data-selectable-position="bottom" data-selectable-allow-new="false" data-selectable-url="/_/core-fruitlookup/" type="text" id="id_multiselect-initial_0" />
+            <input name="multiselect-initial_1" title="Initial" value="0" data-selectable-type="hidden-multiple" type="hidden" id="id_multiselect-initial_1_0">
+        </div>
+        <div id="multi-combo">
+            <input data-selectable-multiple="true" name="multicombo_0" data-selectable-type="combobox" data-selectable-position="bottom" data-selectable-allow-new="false" data-selectable-url="/_/core-fruitlookup/" type="text" id="id_multicombo_0" />
+        </div>
+        <div id="multi-combo-initial">
+            <input data-selectable-multiple="true" name="multicombo-initial_0" data-selectable-type="combobox" data-selectable-position="bottom" data-selectable-allow-new="false" data-selectable-url="/_/core-fruitlookup/" type="text" id="id_multicombo-initial_0" />
+            <input name="multicombo-initial_1" title="Initial" value="0" data-selectable-type="hidden-multiple" type="hidden" id="id_multicombo-initial_1_0">
+        </div>
+    </div>
+</body>
+</html>

File selectable/tests/qunit/qunit.css

+/**
+ * QUnit v1.2.0 - A JavaScript Unit Testing Framework
+ *
+ * http://docs.jquery.com/QUnit
+ *
+ * Copyright (c) 2011 John Resig, Jörn Zaefferer
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * or GPL (GPL-LICENSE.txt) licenses.
+ */
+
+/** Font Family and Sizes */
+
+#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult {
+	font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
+}
+
+#qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; }
+#qunit-tests { font-size: smaller; }
+
+
+/** Resets */
+
+#qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult {
+	margin: 0;
+	padding: 0;
+}
+
+
+/** Header */
+
+#qunit-header {
+	padding: 0.5em 0 0.5em 1em;
+
+	color: #8699a4;
+	background-color: #0d3349;
+
+	font-size: 1.5em;
+	line-height: 1em;
+	font-weight: normal;
+
+	border-radius: 15px 15px 0 0;
+	-moz-border-radius: 15px 15px 0 0;
+	-webkit-border-top-right-radius: 15px;
+	-webkit-border-top-left-radius: 15px;
+}
+
+#qunit-header a {
+	text-decoration: none;
+	color: #c2ccd1;
+}
+
+#qunit-header a:hover,
+#qunit-header a:focus {
+	color: #fff;
+}
+
+#qunit-banner {
+	height: 5px;
+}
+
+#qunit-testrunner-toolbar {
+	padding: 0.5em 0 0.5em 2em;
+	color: #5E740B;
+	background-color: #eee;
+}
+
+#qunit-userAgent {
+	padding: 0.5em 0 0.5em 2.5em;
+	background-color: #2b81af;
+	color: #fff;
+	text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
+}
+
+
+/** Tests: Pass/Fail */
+
+#qunit-tests {
+	list-style-position: inside;
+}
+
+#qunit-tests li {
+	padding: 0.4em 0.5em 0.4em 2.5em;
+	border-bottom: 1px solid #fff;
+	list-style-position: inside;
+}
+
+#qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running  {
+	display: none;
+}
+
+#qunit-tests li strong {
+	cursor: pointer;
+}
+
+#qunit-tests li a {
+	padding: 0.5em;
+	color: #c2ccd1;
+	text-decoration: none;
+}
+#qunit-tests li a:hover,
+#qunit-tests li a:focus {
+	color: #000;
+}
+
+#qunit-tests ol {
+	margin-top: 0.5em;
+	padding: 0.5em;
+
+	background-color: #fff;
+
+	border-radius: 15px;
+	-moz-border-radius: 15px;
+	-webkit-border-radius: 15px;
+
+	box-shadow: inset 0px 2px 13px #999;
+	-moz-box-shadow: inset 0px 2px 13px #999;
+	-webkit-box-shadow: inset 0px 2px 13px #999;
+}
+
+#qunit-tests table {
+	border-collapse: collapse;
+	margin-top: .2em;
+}
+
+#qunit-tests th {
+	text-align: right;
+	vertical-align: top;
+	padding: 0 .5em 0 0;
+}
+
+#qunit-tests td {
+	vertical-align: top;
+}
+
+#qunit-tests pre {
+	margin: 0;
+	white-space: pre-wrap;
+	word-wrap: break-word;
+}
+
+#qunit-tests del {
+	background-color: #e0f2be;
+	color: #374e0c;
+	text-decoration: none;
+}
+
+#qunit-tests ins {
+	background-color: #ffcaca;
+	color: #500;
+	text-decoration: none;
+}
+
+/*** Test Counts */
+
+#qunit-tests b.counts                       { color: black; }
+#qunit-tests b.passed                       { color: #5E740B; }
+#qunit-tests b.failed                       { color: #710909; }
+
+#qunit-tests li li {
+	margin: 0.5em;
+	padding: 0.4em 0.5em 0.4em 0.5em;
+	background-color: #fff;
+	border-bottom: none;
+	list-style-position: inside;
+}
+
+/*** Passing Styles */
+
+#qunit-tests li li.pass {
+	color: #5E740B;
+	background-color: #fff;
+	border-left: 26px solid #C6E746;
+}
+
+#qunit-tests .pass                          { color: #528CE0; background-color: #D2E0E6; }
+#qunit-tests .pass .test-name               { color: #366097; }
+
+#qunit-tests .pass .test-actual,
+#qunit-tests .pass .test-expected           { color: #999999; }
+
+#qunit-banner.qunit-pass                    { background-color: #C6E746; }
+
+/*** Failing Styles */
+
+#qunit-tests li li.fail {
+	color: #710909;
+	background-color: #fff;
+	border-left: 26px solid #EE5757;
+	white-space: pre;
+}
+
+#qunit-tests > li:last-child {
+	border-radius: 0 0 15px 15px;
+	-moz-border-radius: 0 0 15px 15px;
+	-webkit-border-bottom-right-radius: 15px;
+	-webkit-border-bottom-left-radius: 15px;
+}
+
+#qunit-tests .fail                          { color: #000000; background-color: #EE5757; }
+#qunit-tests .fail .test-name,
+#qunit-tests .fail .module-name             { color: #000000; }
+
+#qunit-tests .fail .test-actual             { color: #EE5757; }
+#qunit-tests .fail .test-expected           { color: green;   }
+
+#qunit-banner.qunit-fail                    { background-color: #EE5757; }
+
+
+/** Result */
+
+#qunit-testresult {
+	padding: 0.5em 0.5em 0.5em 2.5em;
+
+	color: #2b81af;
+	background-color: #D2E0E6;
+
+	border-bottom: 1px solid white;
+}
+
+/** Fixture */
+
+#qunit-fixture {
+	position: absolute;
+	top: -10000px;
+	left: -10000px;
+}

File selectable/tests/qunit/qunit.js

+/**
+ * QUnit v1.2.0 - A JavaScript Unit Testing Framework
+ *
+ * http://docs.jquery.com/QUnit
+ *
+ * Copyright (c) 2011 John Resig, Jörn Zaefferer
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * or GPL (GPL-LICENSE.txt) licenses.
+ */
+
+(function(window) {
+
+var defined = {
+	setTimeout: typeof window.setTimeout !== "undefined",
+	sessionStorage: (function() {
+		try {
+			return !!sessionStorage.getItem;
+		} catch(e) {
+			return false;
+		}
+	})()
+};
+
+var	testId = 0,
+	toString = Object.prototype.toString,
+	hasOwn = Object.prototype.hasOwnProperty;
+
+var Test = function(name, testName, expected, testEnvironmentArg, async, callback) {
+	this.name = name;
+	this.testName = testName;
+	this.expected = expected;
+	this.testEnvironmentArg = testEnvironmentArg;
+	this.async = async;
+	this.callback = callback;
+	this.assertions = [];
+};
+Test.prototype = {
+	init: function() {
+		var tests = id("qunit-tests");
+		if (tests) {
+			var b = document.createElement("strong");
+				b.innerHTML = "Running " + this.name;
+			var li = document.createElement("li");
+				li.appendChild( b );
+				li.className = "running";
+				li.id = this.id = "test-output" + testId++;
+			tests.appendChild( li );
+		}
+	},
+	setup: function() {
+		if (this.module != config.previousModule) {
+			if ( config.previousModule ) {
+				runLoggingCallbacks('moduleDone', QUnit, {
+					name: config.previousModule,
+					failed: config.moduleStats.bad,
+					passed: config.moduleStats.all - config.moduleStats.bad,
+					total: config.moduleStats.all
+				} );
+			}
+			config.previousModule = this.module;
+			config.moduleStats = { all: 0, bad: 0 };
+			runLoggingCallbacks( 'moduleStart', QUnit, {
+				name: this.module
+			} );
+		}
+
+		config.current = this;
+		this.testEnvironment = extend({
+			setup: function() {},
+			teardown: function() {}
+		}, this.moduleTestEnvironment);
+		if (this.testEnvironmentArg) {
+			extend(this.testEnvironment, this.testEnvironmentArg);
+		}
+
+		runLoggingCallbacks( 'testStart', QUnit, {
+			name: this.testName,
+			module: this.module
+		});
+
+		// allow utility functions to access the current test environment
+		// TODO why??
+		QUnit.current_testEnvironment = this.testEnvironment;
+
+		try {
+			if ( !config.pollution ) {
+				saveGlobal();
+			}
+
+			this.testEnvironment.setup.call(this.testEnvironment);
+		} catch(e) {
+			QUnit.ok( false, "Setup failed on " + this.testName + ": " + e.message );
+		}
+	},
+	run: function() {
+		config.current = this;
+		if ( this.async ) {
+			QUnit.stop();
+		}
+
+		if ( config.notrycatch ) {
+			this.callback.call(this.testEnvironment);
+			return;
+		}
+		try {
+			this.callback.call(this.testEnvironment);
+		} catch(e) {
+			fail("Test " + this.testName + " died, exception and test follows", e, this.callback);
+			QUnit.ok( false, "Died on test #" + (this.assertions.length + 1) + ": " + e.message + " - " + QUnit.jsDump.parse(e) );
+			// else next test will carry the responsibility
+			saveGlobal();
+
+			// Restart the tests if they're blocking
+			if ( config.blocking ) {
+				QUnit.start();
+			}
+		}
+	},
+	teardown: function() {
+		config.current = this;
+		try {
+			this.testEnvironment.teardown.call(this.testEnvironment);
+			checkPollution();
+		} catch(e) {
+			QUnit.ok( false, "Teardown failed on " + this.testName + ": " + e.message );
+		}
+	},
+	finish: function() {
+		config.current = this;
+		if ( this.expected != null && this.expected != this.assertions.length ) {
+			QUnit.ok( false, "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run" );
+		}
+
+		var good = 0, bad = 0,
+			tests = id("qunit-tests");
+
+		config.stats.all += this.assertions.length;
+		config.moduleStats.all += this.assertions.length;
+
+		if ( tests ) {
+			var ol = document.createElement("ol");
+
+			for ( var i = 0; i < this.assertions.length; i++ ) {
+				var assertion = this.assertions[i];
+
+				var li = document.createElement("li");
+				li.className = assertion.result ? "pass" : "fail";
+				li.innerHTML = assertion.message || (assertion.result ? "okay" : "failed");
+				ol.appendChild( li );
+
+				if ( assertion.result ) {
+					good++;
+				} else {
+					bad++;
+					config.stats.bad++;
+					config.moduleStats.bad++;
+				}
+			}
+
+			// store result when possible
+			if ( QUnit.config.reorder && defined.sessionStorage ) {
+				if (bad) {
+					sessionStorage.setItem("qunit-" + this.module + "-" + this.testName, bad);
+				} else {
+					sessionStorage.removeItem("qunit-" + this.module + "-" + this.testName);
+				}
+			}
+
+			if (bad == 0) {
+				ol.style.display = "none";
+			}
+
+			var b = document.createElement("strong");
+			b.innerHTML = this.name + " <b class='counts'>(<b class='failed'>" + bad + "</b>, <b class='passed'>" + good + "</b>, " + this.assertions.length + ")</b>";
+
+			var a = document.createElement("a");
+			a.innerHTML = "Rerun";
+			a.href = QUnit.url({ filter: getText([b]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") });
+
+			addEvent(b, "click", function() {
+				var next = b.nextSibling.nextSibling,
+					display = next.style.display;
+				next.style.display = display === "none" ? "block" : "none";
+			});
+
+			addEvent(b, "dblclick", function(e) {
+				var target = e && e.target ? e.target : window.event.srcElement;
+				if ( target.nodeName.toLowerCase() == "span" || target.nodeName.toLowerCase() == "b" ) {
+					target = target.parentNode;
+				}
+				if ( window.location && target.nodeName.toLowerCase() === "strong" ) {
+					window.location = QUnit.url({ filter: getText([target]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") });
+				}
+			});
+
+			var li = id(this.id);
+			li.className = bad ? "fail" : "pass";
+			li.removeChild( li.firstChild );
+			li.appendChild( b );
+			li.appendChild( a );
+			li.appendChild( ol );
+
+		} else {
+			for ( var i = 0; i < this.assertions.length; i++ ) {
+				if ( !this.assertions[i].result ) {
+					bad++;
+					config.stats.bad++;
+					config.moduleStats.bad++;
+				}
+			}
+		}
+
+		try {
+			QUnit.reset();
+		} catch(e) {
+			fail("reset() failed, following Test " + this.testName + ", exception and reset fn follows", e, QUnit.reset);
+		}
+
+		runLoggingCallbacks( 'testDone', QUnit, {
+			name: this.testName,
+			module: this.module,
+			failed: bad,
+			passed: this.assertions.length - bad,
+			total: this.assertions.length
+		} );
+	},
+
+	queue: function() {
+		var test = this;
+		synchronize(function() {
+			test.init();
+		});
+		function run() {
+			// each of these can by async
+			synchronize(function() {
+				test.setup();
+			});
+			synchronize(function() {
+				test.run();
+			});
+			synchronize(function() {
+				test.teardown();
+			});
+			synchronize(function() {
+				test.finish();
+			});
+		}
+		// defer when previous test run passed, if storage is available
+		var bad = QUnit.config.reorder && defined.sessionStorage && +sessionStorage.getItem("qunit-" + this.module + "-" + this.testName);
+		if (bad) {
+			run();
+		} else {
+			synchronize(run, true);
+		};
+	}
+
+};
+
+var QUnit = {
+
+	// call on start of module test to prepend name to all tests
+	module: function(name, testEnvironment) {
+		config.currentModule = name;
+		config.currentModuleTestEnviroment = testEnvironment;
+	},
+
+	asyncTest: function(testName, expected, callback) {
+		if ( arguments.length === 2 ) {
+			callback = expected;
+			expected = null;
+		}
+
+		QUnit.test(testName, expected, callback, true);
+	},
+
+	test: function(testName, expected, callback, async) {
+		var name = '<span class="test-name">' + testName + '</span>', testEnvironmentArg;
+
+		if ( arguments.length === 2 ) {
+			callback = expected;
+			expected = null;
+		}
+		// is 2nd argument a testEnvironment?
+		if ( expected && typeof expected === 'object') {
+			testEnvironmentArg = expected;
+			expected = null;
+		}
+
+		if ( config.currentModule ) {
+			name = '<span class="module-name">' + config.currentModule + "</span>: " + name;
+		}
+
+		if ( !validTest(config.currentModule + ": " + testName) ) {
+			return;
+		}
+
+		var test = new Test(name, testName, expected, testEnvironmentArg, async, callback);
+		test.module = config.currentModule;
+		test.moduleTestEnvironment = config.currentModuleTestEnviroment;
+		test.queue();
+	},
+
+	/**
+	 * Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through.
+	 */
+	expect: function(asserts) {
+		config.current.expected = asserts;
+	},
+
+	/**
+	 * Asserts true.
+	 * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" );
+	 */
+	ok: function(a, msg) {
+		a = !!a;
+		var details = {
+			result: a,
+			message: msg
+		};
+		msg = escapeInnerText(msg);
+		runLoggingCallbacks( 'log', QUnit, details );
+		config.current.assertions.push({
+			result: a,
+			message: msg
+		});
+	},
+
+	/**
+	 * Checks that the first two arguments are equal, with an optional message.
+	 * Prints out both actual and expected values.
+	 *
+	 * Prefered to ok( actual == expected, message )
+	 *
+	 * @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." );
+	 *
+	 * @param Object actual
+	 * @param Object expected
+	 * @param String message (optional)
+	 */
+	equal: function(actual, expected, message) {
+		QUnit.push(expected == actual, actual, expected, message);
+	},
+
+	notEqual: function(actual, expected, message) {
+		QUnit.push(expected != actual, actual, expected, message);
+	},
+
+	deepEqual: function(actual, expected, message) {
+		QUnit.push(QUnit.equiv(actual, expected), actual, expected, message);
+	},
+
+	notDeepEqual: function(actual, expected, message) {
+		QUnit.push(!QUnit.equiv(actual, expected), actual, expected, message);
+	},
+
+	strictEqual: function(actual, expected, message) {
+		QUnit.push(expected === actual, actual, expected, message);
+	},
+
+	notStrictEqual: function(actual, expected, message) {
+		QUnit.push(expected !== actual, actual, expected, message);
+	},
+
+	raises: function(block, expected, message) {
+		var actual, ok = false;
+
+		if (typeof expected === 'string') {
+			message = expected;
+			expected = null;
+		}
+
+		try {
+			block();
+		} catch (e) {
+			actual = e;
+		}
+
+		if (actual) {
+			// we don't want to validate thrown error
+			if (!expected) {
+				ok = true;
+			// expected is a regexp
+			} else if (QUnit.objectType(expected) === "regexp") {
+				ok = expected.test(actual);
+			// expected is a constructor
+			} else if (actual instanceof expected) {
+				ok = true;
+			// expected is a validation function which returns true is validation passed
+			} else if (expected.call({}, actual) === true) {
+				ok = true;
+			}
+		}
+
+		QUnit.ok(ok, message);
+	},
+
+	start: function(count) {
+		config.semaphore -= count || 1;
+		if (config.semaphore > 0) {
+			// don't start until equal number of stop-calls
+			return;
+		}
+		if (config.semaphore < 0) {
+			// ignore if start is called more often then stop
+			config.semaphore = 0;
+		}
+		// A slight delay, to avoid any current callbacks
+		if ( defined.setTimeout ) {
+			window.setTimeout(function() {
+				if (config.semaphore > 0) {
+					return;
+				}
+				if ( config.timeout ) {
+					clearTimeout(config.timeout);
+				}
+
+				config.blocking = false;
+				process(true);
+			}, 13);
+		} else {
+			config.blocking = false;
+			process(true);
+		}
+	},
+
+	stop: function(count) {
+		config.semaphore += count || 1;
+		config.blocking = true;
+
+		if ( config.testTimeout && defined.setTimeout ) {
+			clearTimeout(config.timeout);
+			config.timeout = window.setTimeout(function() {
+				QUnit.ok( false, "Test timed out" );
+				config.semaphore = 1;
+				QUnit.start();
+			}, config.testTimeout);
+		}
+	}
+};
+
+//We want access to the constructor's prototype
+(function() {
+	function F(){};
+	F.prototype = QUnit;
+	QUnit = new F();
+	//Make F QUnit's constructor so that we can add to the prototype later
+	QUnit.constructor = F;
+})();
+
+// Backwards compatibility, deprecated
+QUnit.equals = QUnit.equal;
+QUnit.same = QUnit.deepEqual;
+
+// Maintain internal state
+var config = {
+	// The queue of tests to run
+	queue: [],
+
+	// block until document ready
+	blocking: true,
+
+	// when enabled, show only failing tests
+	// gets persisted through sessionStorage and can be changed in UI via checkbox
+	hidepassed: false,
+
+	// by default, run previously failed tests first
+	// very useful in combination with "Hide passed tests" checked
+	reorder: true,
+
+	// by default, modify document.title when suite is done
+	altertitle: true,
+
+	urlConfig: ['noglobals', 'notrycatch'],
+
+	//logging callback queues
+	begin: [],
+	done: [],
+	log: [],
+	testStart: [],
+	testDone: [],
+	moduleStart: [],
+	moduleDone: []
+};
+
+// Load paramaters
+(function() {
+	var location = window.location || { search: "", protocol: "file:" },
+		params = location.search.slice( 1 ).split( "&" ),
+		length = params.length,
+		urlParams = {},
+		current;
+
+	if ( params[ 0 ] ) {
+		for ( var i = 0; i < length; i++ ) {
+			current = params[ i ].split( "=" );
+			current[ 0 ] = decodeURIComponent( current[ 0 ] );
+			// allow just a key to turn on a flag, e.g., test.html?noglobals
+			current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true;
+			urlParams[ current[ 0 ] ] = current[ 1 ];
+		}
+	}
+
+	QUnit.urlParams = urlParams;
+	config.filter = urlParams.filter;
+
+	// Figure out if we're running the tests from a server or not
+	QUnit.isLocal = !!(location.protocol === 'file:');
+})();
+
+// Expose the API as global variables, unless an 'exports'
+// object exists, in that case we assume we're in CommonJS
+if ( typeof exports === "undefined" || typeof require === "undefined" ) {
+	extend(window, QUnit);
+	window.QUnit = QUnit;
+} else {
+	extend(exports, QUnit);
+	exports.QUnit = QUnit;
+}
+
+// define these after exposing globals to keep them in these QUnit namespace only
+extend(QUnit, {
+	config: config,
+
+	// Initialize the configuration options
+	init: function() {
+		extend(config, {
+			stats: { all: 0, bad: 0 },
+			moduleStats: { all: 0, bad: 0 },
+			started: +new Date,
+			updateRate: 1000,
+			blocking: false,
+			autostart: true,
+			autorun: false,
+			filter: "",
+			queue: [],
+			semaphore: 0
+		});
+
+		var tests = id( "qunit-tests" ),
+			banner = id( "qunit-banner" ),
+			result = id( "qunit-testresult" );
+
+		if ( tests ) {
+			tests.innerHTML = "";
+		}
+
+		if ( banner ) {
+			banner.className = "";
+		}
+
+		if ( result ) {
+			result.parentNode.removeChild( result );
+		}
+
+		if ( tests ) {
+			result = document.createElement( "p" );
+			result.id = "qunit-testresult";
+			result.className = "result";
+			tests.parentNode.insertBefore( result, tests );
+			result.innerHTML = 'Running...<br/>&nbsp;';
+		}
+	},
+
+	/**
+	 * Resets the test setup. Useful for tests that modify the DOM.
+	 *
+	 * If jQuery is available, uses jQuery's html(), otherwise just innerHTML.
+	 */
+	reset: function() {
+		if ( window.jQuery ) {
+			jQuery( "#qunit-fixture" ).html( config.fixture );
+		} else {
+			var main = id( 'qunit-fixture' );
+			if ( main ) {
+				main.innerHTML = config.fixture;
+			}
+		}
+	},
+
+	/**
+	 * Trigger an event on an element.
+	 *
+	 * @example triggerEvent( document.body, "click" );
+	 *
+	 * @param DOMElement elem
+	 * @param String type
+	 */
+	triggerEvent: function( elem, type, event ) {
+		if ( document.createEvent ) {
+			event = document.createEvent("MouseEvents");
+			event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView,
+				0, 0, 0, 0, 0, false, false, false, false, 0, null);
+			elem.dispatchEvent( event );
+
+		} else if ( elem.fireEvent ) {
+			elem.fireEvent("on"+type);
+		}
+	},
+
+	// Safe object type checking
+	is: function( type, obj ) {
+		return QUnit.objectType( obj ) == type;
+	},
+
+	objectType: function( obj ) {
+		if (typeof obj === "undefined") {
+				return "undefined";
+
+		// consider: typeof null === object
+		}
+		if (obj === null) {
+				return "null";
+		}
+
+		var type = toString.call( obj ).match(/^\[object\s(.*)\]$/)[1] || '';
+
+		switch (type) {
+				case 'Number':
+						if (isNaN(obj)) {
+								return "nan";
+						} else {
+								return "number";
+						}
+				case 'String':
+				case 'Boolean':
+				case 'Array':
+				case 'Date':
+				case 'RegExp':
+				case 'Function':
+						return type.toLowerCase();
+		}
+		if (typeof obj === "object") {
+				return "object";
+		}
+		return undefined;
+	},
+
+	push: function(result, actual, expected, message) {
+		var details = {
+			result: result,
+			message: message,
+			actual: actual,
+			expected: expected
+		};
+
+		message = escapeInnerText(message) || (result ? "okay" : "failed");
+		message = '<span class="test-message">' + message + "</span>";
+		expected = escapeInnerText(QUnit.jsDump.parse(expected));