Import a new version of django-selectable
authorMagnus Hagander <magnus@hagander.net>
Wed, 6 Jan 2016 15:27:59 +0000 (16:27 +0100)
committerMagnus Hagander <magnus@hagander.net>
Wed, 6 Jan 2016 15:27:59 +0000 (16:27 +0100)
Needed for django 1.8

41 files changed:
dep/django-selectable/AUTHORS.txt
dep/django-selectable/LICENSE.txt
dep/django-selectable/PKG-INFO [new file with mode: 0644]
dep/django-selectable/README.rst
dep/django-selectable/selectable/__init__.py
dep/django-selectable/selectable/apps.py [new file with mode: 0644]
dep/django-selectable/selectable/base.py
dep/django-selectable/selectable/compat.py
dep/django-selectable/selectable/forms/base.py
dep/django-selectable/selectable/forms/fields.py
dep/django-selectable/selectable/forms/widgets.py
dep/django-selectable/selectable/locale/en/LC_MESSAGES/django.mo
dep/django-selectable/selectable/locale/en/LC_MESSAGES/django.po
dep/django-selectable/selectable/locale/es/LC_MESSAGES/django.mo
dep/django-selectable/selectable/locale/es/LC_MESSAGES/django.po
dep/django-selectable/selectable/locale/fr/LC_MESSAGES/django.mo [new file with mode: 0644]
dep/django-selectable/selectable/locale/fr/LC_MESSAGES/django.po [new file with mode: 0644]
dep/django-selectable/selectable/locale/pl/LC_MESSAGES/django.mo
dep/django-selectable/selectable/locale/pl/LC_MESSAGES/django.po
dep/django-selectable/selectable/locale/pt_BR/LC_MESSAGES/django.mo
dep/django-selectable/selectable/locale/pt_BR/LC_MESSAGES/django.po
dep/django-selectable/selectable/locale/zh_CN/LC_MESSAGES/django.mo [new file with mode: 0644]
dep/django-selectable/selectable/locale/zh_CN/LC_MESSAGES/django.po [new file with mode: 0644]
dep/django-selectable/selectable/registry.py
dep/django-selectable/selectable/static/selectable/css/dj.selectable.css
dep/django-selectable/selectable/static/selectable/js/jquery.dj.selectable.js
dep/django-selectable/selectable/tests/__init__.py
dep/django-selectable/selectable/tests/base.py
dep/django-selectable/selectable/tests/test_base.py [new file with mode: 0644]
dep/django-selectable/selectable/tests/test_decorators.py [new file with mode: 0644]
dep/django-selectable/selectable/tests/test_fields.py [new file with mode: 0644]
dep/django-selectable/selectable/tests/test_forms.py [new file with mode: 0644]
dep/django-selectable/selectable/tests/test_functional.py [new file with mode: 0644]
dep/django-selectable/selectable/tests/test_templatetags.py [new file with mode: 0644]
dep/django-selectable/selectable/tests/test_views.py [new file with mode: 0644]
dep/django-selectable/selectable/tests/test_widgets.py [new file with mode: 0644]
dep/django-selectable/selectable/tests/urls.py
dep/django-selectable/selectable/tests/views.py
dep/django-selectable/selectable/urls.py
dep/django-selectable/setup.cfg [new file with mode: 0644]
dep/django-selectable/setup.py

index 3c1e4d2e6757a961f2f3211625f5c59b4883fedd..1c0422f2ab849d407318c41488e01e1b3a1792ea 100644 (file)
@@ -17,5 +17,11 @@ Rick Testore
 Karen Tracey
 Manuel Alvarez
 Ustun Ozgur
+@leo-the-manic
+Calvin Spealman
+Rebecca Lovewell
+Thomas Güttler
+Yuri Khrustalev
+@SaeX
 
 Thanks for all of your work!
index a208c39c6e1e48eb705f0b59b78424b2f4397698..c50fbf06b0b6176c9d15006039ea4ecd90d553d8 100644 (file)
@@ -1,4 +1,4 @@
-Copyright (c) 2010-2013, Mark Lavin
+Copyright (c) 2010-2014, Mark Lavin
 All rights reserved.
 
 Redistribution and use in source and binary forms, with or without modification,
diff --git a/dep/django-selectable/PKG-INFO b/dep/django-selectable/PKG-INFO
new file mode 100644 (file)
index 0000000..4a7072d
--- /dev/null
@@ -0,0 +1,92 @@
+Metadata-Version: 1.1
+Name: django-selectable
+Version: 0.9.0
+Summary: Auto-complete selection widgets using Django and jQuery UI.
+Home-page: https://github.com/mlavin/django-selectable
+Author: Mark Lavin
+Author-email: markdlavin@gmail.com
+License: BSD
+Description: django-selectable
+        ===================
+        
+        Tools and widgets for using/creating auto-complete selection widgets using Django and jQuery UI.
+        
+        .. image:: https://travis-ci.org/mlavin/django-selectable.svg?branch=master
+            :target: https://travis-ci.org/mlavin/django-selectable
+        
+        
+        Features
+        -----------------------------------
+        
+        - Works with the latest jQuery UI Autocomplete library
+        - Auto-discovery/registration pattern for defining lookups
+        
+        
+        Installation Requirements
+        -----------------------------------
+        
+        - Python 2.6-2.7, 3.2+
+        - `Django <http://www.djangoproject.com/>`_ >= 1.5
+        - `jQuery <http://jquery.com/>`_ >= 1.7
+        - `jQuery UI <http://jqueryui.com/>`_ >= 1.8
+        
+        To install::
+        
+            pip install django-selectable
+        
+        Next add `selectable` to your `INSTALLED_APPS` to include the related css/js::
+        
+            INSTALLED_APPS = (
+                'contrib.staticfiles',
+                # Other apps here
+                'selectable',
+            )
+        
+        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.
+        
+        Once installed you should add the urls to your root url patterns::
+        
+            urlpatterns = patterns('',
+                # Other patterns go here
+                (r'^selectable/', include('selectable.urls')),
+            )
+        
+        
+        Documentation
+        -----------------------------------
+        
+        Documentation for django-selectable is available on `Read The Docs <http://readthedocs.org/docs/django-selectable>`_.
+        
+        
+        Additional Help/Support
+        -----------------------------------
+        
+        You can find additional help or support on the mailing list: http://groups.google.com/group/django-selectable
+        
+        
+        Contributing
+        --------------------------------------
+        
+        If you think you've found a bug or are interested in contributing to this project
+        check out our `contributing guide <http://readthedocs.org/docs/django-selectable/en/latest/contribute.html>`_.
+        
+        If you are interested in translating django-selectable into your native language
+        you can join the `Transifex project <https://www.transifex.com/projects/p/django-selectable/>`_.
+        
+        
+Platform: UNKNOWN
+Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: BSD License
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 2.6
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.2
+Classifier: Programming Language :: Python :: 3.3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Framework :: Django
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Operating System :: OS Independent
index 6930524e24b73c2b858bd4c5138c63473036408a..0219c2cb6158c76f0262e611b9d72d24c57a08b8 100644 (file)
@@ -3,6 +3,10 @@ django-selectable
 
 Tools and widgets for using/creating auto-complete selection widgets using Django and jQuery UI.
 
+.. image:: https://travis-ci.org/mlavin/django-selectable.svg?branch=master
+    :target: https://travis-ci.org/mlavin/django-selectable
+
+
 Features
 -----------------------------------
 
@@ -13,18 +17,11 @@ Features
 Installation Requirements
 -----------------------------------
 
-- Python 2.6 or Python 2.7
-- `Django <http://www.djangoproject.com/>`_ >= 1.3
-- `jQuery <http://jquery.com/>`_ >= 1.4.4
+- Python 2.6-2.7, 3.2+
+- `Django <http://www.djangoproject.com/>`_ >= 1.5
+- `jQuery <http://jquery.com/>`_ >= 1.7
 - `jQuery UI <http://jqueryui.com/>`_ >= 1.8
 
-.. note::
-
-    Begining with version django-selectable version 0.6, Django 1.2 is no longer supported.
-    While it may continue to work, bugs related to Django 1.2 support will not be fixed.
-
-    Version 0.7 adds experimental support for Python 3.2+ when used with Django 1.5+.
-
 To install::
 
     pip install django-selectable
index 7c38b678a56314dc7ff637866c248ceaaeb7bdb8..b4232fc2b8b6edbee4dbe59339eb19c7b425e6d1 100644 (file)
@@ -1,4 +1,6 @@
 "Auto-complete selection widgets using Django and jQuery UI."
 
 
-__version__ = '0.7.0'
\ No newline at end of file
+__version__ = '0.9.0'
+
+default_app_config = 'selectable.apps.SelectableConfig'
diff --git a/dep/django-selectable/selectable/apps.py b/dep/django-selectable/selectable/apps.py
new file mode 100644 (file)
index 0000000..f65c5e1
--- /dev/null
@@ -0,0 +1,14 @@
+try:
+    from django.apps import AppConfig
+except ImportError:
+    AppConfig = object
+
+
+class SelectableConfig(AppConfig):
+    """App configuration for django-selectable."""
+
+    name = 'selectable'
+
+    def ready(self):
+        from . import registry
+        registry.autodiscover()
index fe7c5eea23d7feaff6b7ea35ae1bd3608ebb94ee..8335ddd796a1277b597e941d7d499eb148b0fc88 100644 (file)
@@ -146,7 +146,10 @@ class ModelLookup(LookupBase):
         return qs
 
     def get_queryset(self):
-        qs = self.model._default_manager.get_query_set()
+        try:
+            qs = self.model._default_manager.get_queryset()
+        except AttributeError:  # Django <= 1.5.
+            qs = self.model._default_manager.get_query_set()
         if self.filters:
             qs = qs.filter(**self.filters)
         return qs
index 7356db127c94242a84809e70458950d6c0bdef54..a63ab6a12e4d786297732b6e3ce4a58dadc384fd 100644 (file)
@@ -12,9 +12,25 @@ except ImportError:
     from django.utils.encoding import smart_unicode as smart_text
     from django.utils.encoding import force_unicode as force_text
 
+try:
+    from django.forms.utils import flatatt
+except ImportError:
+    from django.forms.util import flatatt
+
 PY3 = sys.version_info[0] == 3
 
 if PY3:
     string_types = str,
 else:
-    string_types = basestring,
\ No newline at end of file
+    string_types = basestring,
+
+try:
+    from importlib import import_module
+except ImportError:
+    from django.utils.importlib import import_module
+
+try:
+    from django.apps import AppConfig
+    LEGACY_AUTO_DISCOVER = False
+except ImportError:
+    LEGACY_AUTO_DISCOVER = True
index 21ee8894881599fdc607eeaa75e5627db8a75123..c39c2f731061c43710d505ba59aa45c90b92f582 100644 (file)
@@ -2,9 +2,8 @@ from __future__ import unicode_literals
 
 from django import forms
 from django.conf import settings
-from django.utils.importlib import import_module
 
-from selectable.compat import string_types
+from selectable.compat import string_types, import_module
 
 
 __all__ = (
index f6004ccb6582e05cabbe2c336da59b775d6efe12..21109c4160fc8ac121ded32f00edf63db1e36c4b 100644 (file)
@@ -4,6 +4,7 @@ from django import forms
 from django.core.exceptions import ValidationError
 from django.core.validators import EMPTY_VALUES
 from django.utils.translation import ugettext_lazy as _
+from django.db.models import Model
 
 from selectable.forms.base import import_lookup_class
 from selectable.forms.widgets import AutoCompleteSelectWidget
@@ -15,7 +16,32 @@ __all__ = (
 )
 
 
-class AutoCompleteSelectField(forms.Field):
+def model_vars(obj):
+    fields = dict(
+        (field.name, getattr(obj, field.name))
+        for field in obj._meta.fields
+    )
+    return fields
+
+
+class BaseAutoCompleteField(forms.Field):
+
+    def _has_changed(self, initial, data):
+        "Detects if the data was changed. This is added in 1.6."
+        if initial is None and data is None:
+            return False
+        if data and not hasattr(data, '__iter__'):
+            data = self.widget.decompress(data)
+        initial = self.to_python(initial)
+        data = self.to_python(data)
+        if hasattr(self, '_coerce'):
+            data = self._coerce(data)
+        if isinstance(data, Model) and isinstance(initial, Model):
+            return model_vars(data) != model_vars(initial)
+        else:
+            return data != initial
+
+class AutoCompleteSelectField(BaseAutoCompleteField):
     widget = AutoCompleteSelectWidget
 
     default_error_messages = {
@@ -32,6 +58,7 @@ class AutoCompleteSelectField(forms.Field):
         super(AutoCompleteSelectField, self).__init__(*args, **kwargs)
 
 
+
     def to_python(self, value):
         if value in EMPTY_VALUES:
             return None
@@ -62,7 +89,7 @@ class AutoCompleteSelectField(forms.Field):
         return value
 
 
-class AutoCompleteSelectMultipleField(forms.Field):
+class AutoCompleteSelectMultipleField(BaseAutoCompleteField):
     widget = AutoCompleteSelectMultipleWidget
 
     default_error_messages = {
index f0d30873d687c721dad839a950d1328fef504b3d..f23332c60d0cae8ccdb74aae2ab27d0fdfa7c5ee 100644 (file)
@@ -2,14 +2,13 @@ from __future__ import unicode_literals
 
 import json
 
-from django import forms
+from django import forms, VERSION as DJANGO_VERSION
 from django.conf import settings
-from django.forms.util import flatatt
 from django.utils.http import urlencode
 from django.utils.safestring import mark_safe
 
 from selectable import __version__
-from selectable.compat import force_text
+from selectable.compat import force_text, flatatt
 from selectable.forms.base import import_lookup_class
 
 __all__ = (
@@ -33,6 +32,7 @@ class SelectableMediaMixin(object):
         }
         js = ('%sjs/jquery.dj.selectable.js?v=%s' % (STATIC_PREFIX, __version__),)
 
+
 class AutoCompleteWidget(forms.TextInput, SelectableMediaMixin):
 
     def __init__(self, lookup_class, *args, **kwargs):
@@ -65,13 +65,14 @@ class SelectableMultiWidget(forms.MultiWidget):
     def update_query_parameters(self, qs_dict):
         self.widgets[0].update_query_parameters(qs_dict)
 
-    def _has_changed(self, initial, data):
-        "Decects if the widget was changed. This is removed in 1.6."
-        if initial is None and data is None:
-            return False
-        if data and not hasattr(data, '__iter__'):
-            data = self.decompress(data)
-        return super(SelectableMultiWidget, self)._has_changed(initial, data)
+    if DJANGO_VERSION < (1, 6):
+        def _has_changed(self, initial, data):
+            "Detects if the widget was changed. This is removed in Django 1.6."
+            if initial is None and data is None:
+                return False
+            if data and not hasattr(data, '__iter__'):
+                data = self.decompress(data)
+            return super(SelectableMultiWidget, self)._has_changed(initial, data)
 
     def decompress(self, value):
         if value:
@@ -86,8 +87,29 @@ class SelectableMultiWidget(forms.MultiWidget):
             return [item_value, value]
         return [None, None]
 
+    def get_compatible_postdata(self, data, name):
+        """Get postdata built for a normal <select> element.
+
+        Django MultiWidgets create post variables like ``foo_0`` and ``foo_1``,
+        and this behavior is not cleanly overridable.  Non-multiwidgets, like
+        Select, get simple names like ``foo``. In order to keep this widget
+        compatible with requests designed for traditional select widgets,
+        search postdata for a name like ``foo`` and return that value.
+
+        This will return ``None`` if a ``<select>``-compatibile post variable
+        is not found.
+        """
+        return data.get(name, None)
 
-class AutoCompleteSelectWidget(SelectableMultiWidget, SelectableMediaMixin):
+
+class _BaseSingleSelectWidget(SelectableMultiWidget, SelectableMediaMixin):
+    """
+    Common base class for AutoCompleteSelectWidget and AutoComboboxSelectWidget
+    each which use one widget (primary_widget) to select text and a single
+    hidden input to hold the selected id.
+    """
+
+    primary_widget = None
 
     def __init__(self, lookup_class, *args, **kwargs):
         self.lookup_class = import_lookup_class(lookup_class)
@@ -95,22 +117,31 @@ class AutoCompleteSelectWidget(SelectableMultiWidget, SelectableMediaMixin):
         self.limit = kwargs.pop('limit', None)
         query_params = kwargs.pop('query_params', {})
         widgets = [
-            AutoCompleteWidget(
+            self.primary_widget(
                 lookup_class, allow_new=self.allow_new,
                 limit=self.limit, query_params=query_params,
                 attrs=kwargs.get('attrs'),
             ),
             forms.HiddenInput(attrs={'data-selectable-type': 'hidden'})
         ]
-        super(AutoCompleteSelectWidget, self).__init__(widgets, *args, **kwargs)
+        super(_BaseSingleSelectWidget, self).__init__(widgets, *args, **kwargs)
 
     def value_from_datadict(self, data, files, name):
-        value = super(AutoCompleteSelectWidget, self).value_from_datadict(data, files, name)
+        value = super(_BaseSingleSelectWidget, self).value_from_datadict(data, files, name)
+        if not value[1]:
+            compatible_postdata = self.get_compatible_postdata(data, name)
+            if compatible_postdata:
+                value[1] = compatible_postdata
         if not self.allow_new:
             return value[1]
         return value
 
 
+class AutoCompleteSelectWidget(_BaseSingleSelectWidget):
+
+    primary_widget = AutoCompleteWidget
+
+
 class AutoComboboxWidget(AutoCompleteWidget, SelectableMediaMixin):
 
     def build_attrs(self, extra_attrs=None, **kwargs):
@@ -119,28 +150,9 @@ class AutoComboboxWidget(AutoCompleteWidget, SelectableMediaMixin):
         return attrs
 
 
-class AutoComboboxSelectWidget(SelectableMultiWidget, SelectableMediaMixin):
+class AutoComboboxSelectWidget(_BaseSingleSelectWidget):
 
-    def __init__(self, lookup_class, *args, **kwargs):
-        self.lookup_class = import_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, query_params=query_params,
-                attrs=kwargs.get('attrs'),
-            ),
-            forms.HiddenInput(attrs={'data-selectable-type': 'hidden'})
-        ]
-        super(AutoComboboxSelectWidget, self).__init__(widgets, *args, **kwargs)
-
-    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
+    primary_widget = AutoComboboxWidget
 
 
 class LookupMultipleHiddenInput(forms.MultipleHiddenInput):
@@ -178,7 +190,14 @@ class LookupMultipleHiddenInput(forms.MultipleHiddenInput):
         return attrs
 
 
-class AutoCompleteSelectMultipleWidget(SelectableMultiWidget, SelectableMediaMixin):
+class _BaseMultipleSelectWidget(SelectableMultiWidget, SelectableMediaMixin):
+    """
+    Common base class for AutoCompleteSelectMultipleWidget and AutoComboboxSelectMultipleWidget
+    each which use one widget (primary_widget) to select text and a multiple
+    hidden inputs to hold the selected ids.
+    """
+
+    primary_widget = None
 
     def __init__(self, lookup_class, *args, **kwargs):
         self.lookup_class = import_lookup_class(lookup_class)
@@ -191,71 +210,44 @@ class AutoCompleteSelectMultipleWidget(SelectableMultiWidget, SelectableMediaMix
         attrs.update(kwargs.get('attrs', {}))
         query_params = kwargs.pop('query_params', {})
         widgets = [
-            AutoCompleteWidget(
+            self.primary_widget(
                 lookup_class, allow_new=False,
                 limit=self.limit, query_params=query_params, attrs=attrs
             ),
             LookupMultipleHiddenInput(lookup_class)
         ]
-        super(AutoCompleteSelectMultipleWidget, self).__init__(widgets, *args, **kwargs)
+        super(_BaseMultipleSelectWidget, self).__init__(widgets, *args, **kwargs)
 
     def value_from_datadict(self, data, files, name):
-        return self.widgets[1].value_from_datadict(data, files, name + '_1')
+        value = self.widgets[1].value_from_datadict(data, files, name + '_1')
+        if not value:
+            # Fall back to the compatible POST name
+            value = self.get_compatible_postdata(data, name)
+        return value
 
     def render(self, name, value, attrs=None):
         if value and not hasattr(value, '__iter__'):
             value = [value]
         value = ['', value]
-        return super(AutoCompleteSelectMultipleWidget, self).render(name, value, attrs)
-
-    def _has_changed(self, initial, data):
-        """"
-        Decects if the widget was changed. This is removed in 1.6.
-
-        For the multi-select case we only care if the hidden inputs changed.
-        """
-        initial = ['', initial]
-        data = ['', data]
-        return super(AutoCompleteSelectMultipleWidget, self)._has_changed(initial, data)
+        return super(_BaseMultipleSelectWidget, self).render(name, value, attrs)
 
+    if DJANGO_VERSION < (1, 6):
+        def _has_changed(self, initial, data):
+            """"
+            Detects if the widget was changed. This is removed in Django 1.6.
 
+            For the multi-select case we only care if the hidden inputs changed.
+            """
+            initial = ['', initial]
+            data = ['', data]
+            return super(_BaseMultipleSelectWidget, self)._has_changed(initial, data)
 
-class AutoComboboxSelectMultipleWidget(SelectableMultiWidget, SelectableMediaMixin):
 
-    def __init__(self, lookup_class, *args, **kwargs):
-        self.lookup_class = import_lookup_class(lookup_class)
-        self.limit = kwargs.pop('limit', None)
-        position = kwargs.pop('position', 'bottom')
-        attrs = {
-            'data-selectable-multiple': 'true',
-            'data-selectable-position': position
-        }
-        attrs.update(kwargs.get('attrs', {}))
-        query_params = kwargs.pop('query_params', {})
-        widgets = [
-            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)
+class AutoCompleteSelectMultipleWidget(_BaseMultipleSelectWidget):
 
-    def value_from_datadict(self, data, files, name):
-        return self.widgets[1].value_from_datadict(data, files, name + '_1')
+    primary_widget = AutoCompleteWidget
 
-    def render(self, name, value, attrs=None):
-        if value and not hasattr(value, '__iter__'):
-            value = [value]
-        value = ['', value]
-        return super(AutoComboboxSelectMultipleWidget, self).render(name, value, attrs)
 
-    def _has_changed(self, initial, data):
-        """"
-        Decects if the widget was changed. This is removed in 1.6.
+class AutoComboboxSelectMultipleWidget(_BaseMultipleSelectWidget):
 
-        For the multi-select case we only care if the hidden inputs changed.
-        """
-        initial = ['', initial]
-        data = ['', data]
-        return super(AutoComboboxSelectMultipleWidget, self)._has_changed(initial, data)
+    primary_widget = AutoComboboxWidget
index bd87d37359effc604c08909beda7721bcc777bc8..99d42adf6d4725d6cd040d73b3421b4c81cd49d5 100644 (file)
Binary files a/dep/django-selectable/selectable/locale/en/LC_MESSAGES/django.mo and b/dep/django-selectable/selectable/locale/en/LC_MESSAGES/django.mo differ
index 942993aede419563e24f23e697f83e2e5dc67805..0f1f17848abab74e9ca4dc7371e9d003d3d87951 100644 (file)
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2013-02-17 14:18-0500\n"
+"POT-Creation-Date: 2014-10-21 20:14-0400\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -21,6 +21,6 @@ msgstr ""
 msgid "Show more results"
 msgstr ""
 
-#: forms/fields.py:22 forms/fields.py:69
+#: forms/fields.py:48 forms/fields.py:96
 msgid "Select a valid choice. That choice is not one of the available choices."
 msgstr ""
index d08ce3531941458c7e872b3e6f42d5bbd50eea7a..96ac6a1de827b81edec48f96f29e7474a7b5174a 100644 (file)
Binary files a/dep/django-selectable/selectable/locale/es/LC_MESSAGES/django.mo and b/dep/django-selectable/selectable/locale/es/LC_MESSAGES/django.mo differ
index 438d095c5450566ccf508b31a14baaa25efdddc1..6a6bc0433c6f13039f27b0c05aa6d47f43d95803 100644 (file)
@@ -1,27 +1,27 @@
 # SOME DESCRIPTIVE TITLE.
 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
 # This file is distributed under the same license as the PACKAGE package.
-#
+# 
 # Translators:
 msgid ""
 msgstr ""
 "Project-Id-Version: django-selectable\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2013-02-17 14:18-0500\n"
-"PO-Revision-Date: 2013-02-13 14:21+0000\n"
-"Last-Translator: escriva <escriva@gmail.com>\n"
-"Language-Team: Spanish (http://www.transifex.com/projects/p/django-"
-"selectable/language/es/)\n"
-"Language: es\n"
+"POT-Creation-Date: 2012-10-06 15:02-0400\n"
+"PO-Revision-Date: 2013-11-20 10:18+0000\n"
+"Last-Translator: Manuel Alvarez <escriva@gmail.com>\n"
+"Language-Team: Spanish (http://www.transifex.com/projects/p/django-selectable/language/es/)\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
+"Language: es\n"
 "Plural-Forms: nplurals=2; plural=(n != 1);\n"
 
-#: base.py:117
+#: base.py:115
 msgid "Show more results"
 msgstr "Mostrar más resultados"
 
-#: forms/fields.py:22 forms/fields.py:69
-msgid "Select a valid choice. That choice is not one of the available choices."
+#: forms/fields.py:19 forms/fields.py:63
+msgid ""
+"Select a valid choice. That choice is not one of the available choices."
 msgstr "Seleccione una opción válida. La opción seleccionada no está disponible."
diff --git a/dep/django-selectable/selectable/locale/fr/LC_MESSAGES/django.mo b/dep/django-selectable/selectable/locale/fr/LC_MESSAGES/django.mo
new file mode 100644 (file)
index 0000000..651efbb
Binary files /dev/null and b/dep/django-selectable/selectable/locale/fr/LC_MESSAGES/django.mo differ
diff --git a/dep/django-selectable/selectable/locale/fr/LC_MESSAGES/django.po b/dep/django-selectable/selectable/locale/fr/LC_MESSAGES/django.po
new file mode 100644 (file)
index 0000000..c1e265f
--- /dev/null
@@ -0,0 +1,28 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# 
+# Translators:
+# Mark Lavin <markdlavin@gmail.com>, 2014
+msgid ""
+msgstr ""
+"Project-Id-Version: django-selectable\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2012-10-06 15:02-0400\n"
+"PO-Revision-Date: 2014-01-21 01:00+0000\n"
+"Last-Translator: Mark Lavin <markdlavin@gmail.com>\n"
+"Language-Team: French (http://www.transifex.com/projects/p/django-selectable/language/fr/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: fr\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: base.py:115
+msgid "Show more results"
+msgstr "Afficher plus de résultats"
+
+#: forms/fields.py:19 forms/fields.py:63
+msgid ""
+"Select a valid choice. That choice is not one of the available choices."
+msgstr "Sélectionnez un choix valide. Ce choix ne fait pas partie de ceux disponibles."
index 2e8eae72cda924a15ba8abda9f5971e01474dd52..8e47890e72bf91dcd2bfede64baed88b53a577ea 100644 (file)
Binary files a/dep/django-selectable/selectable/locale/pl/LC_MESSAGES/django.mo and b/dep/django-selectable/selectable/locale/pl/LC_MESSAGES/django.mo differ
index dbe73c935fd84224f996f324d7f305397112ca5f..7ab61a4a2a8c30c99b2fc15594ae1c238ad8c689 100644 (file)
@@ -1,29 +1,28 @@
 # SOME DESCRIPTIVE TITLE.
 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
 # This file is distributed under the same license as the PACKAGE package.
-#
+# 
 # Translators:
-#   <slafs.e@gmail.com>, 2012.
+# slafs <slafs.e@gmail.com>, 2012
 msgid ""
 msgstr ""
 "Project-Id-Version: django-selectable\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2013-02-17 14:18-0500\n"
-"PO-Revision-Date: 2012-10-11 12:50+0000\n"
+"POT-Creation-Date: 2012-10-06 15:02-0400\n"
+"PO-Revision-Date: 2013-11-20 10:18+0000\n"
 "Last-Translator: slafs <slafs.e@gmail.com>\n"
-"Language-Team: Polish (http://www.transifex.com/projects/p/django-selectable/"
-"language/pl/)\n"
-"Language: pl\n"
+"Language-Team: Polish (http://www.transifex.com/projects/p/django-selectable/language/pl/)\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
-"|| n%100>=20) ? 1 : 2);\n"
+"Language: pl\n"
+"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
 
-#: base.py:117
+#: base.py:115
 msgid "Show more results"
 msgstr "Pokaż więcej wyników"
 
-#: forms/fields.py:22 forms/fields.py:69
-msgid "Select a valid choice. That choice is not one of the available choices."
+#: forms/fields.py:19 forms/fields.py:63
+msgid ""
+"Select a valid choice. That choice is not one of the available choices."
 msgstr "Dokonaj poprawnego wyboru. Ten wybór nie jest jednym z dostępnych."
index 1d497c097d6d66cee10591dc6fdba84935e1309c..6afef1b6a3c19c040ddce60da88df74b1bb98101 100644 (file)
Binary files a/dep/django-selectable/selectable/locale/pt_BR/LC_MESSAGES/django.mo and b/dep/django-selectable/selectable/locale/pt_BR/LC_MESSAGES/django.mo differ
index 9591094e4793191f97670da2797fed6aee70d4d4..3999877ad882e5e82633634329803544ce15bc9b 100644 (file)
@@ -1,26 +1,27 @@
 # SOME DESCRIPTIVE TITLE.
 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
 # This file is distributed under the same license as the PACKAGE package.
-#
+# 
 # Translators:
 msgid ""
 msgstr ""
 "Project-Id-Version: django-selectable\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2013-02-17 14:18-0500\n"
-"PO-Revision-Date: 2012-10-06 19:19+0000\n"
-"Last-Translator: mlavin <markdlavin@gmail.com>\n"
-"Language-Team: LANGUAGE <LL@li.org>\n"
-"Language: pt_BR\n"
+"POT-Creation-Date: 2012-10-06 15:02-0400\n"
+"PO-Revision-Date: 2013-11-20 10:18+0000\n"
+"Last-Translator: Mark Lavin <markdlavin@gmail.com>\n"
+"Language-Team: Portuguese (Brazil) (http://www.transifex.com/projects/p/django-selectable/language/pt_BR/)\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
+"Language: pt_BR\n"
 "Plural-Forms: nplurals=2; plural=(n > 1);\n"
 
-#: base.py:117
+#: base.py:115
 msgid "Show more results"
 msgstr "Mostrar mais resultados"
 
-#: forms/fields.py:22 forms/fields.py:69
-msgid "Select a valid choice. That choice is not one of the available choices."
+#: forms/fields.py:19 forms/fields.py:63
+msgid ""
+"Select a valid choice. That choice is not one of the available choices."
 msgstr "Selecione uma escolha valida. Esta escolha não é uma das disponíveis."
diff --git a/dep/django-selectable/selectable/locale/zh_CN/LC_MESSAGES/django.mo b/dep/django-selectable/selectable/locale/zh_CN/LC_MESSAGES/django.mo
new file mode 100644 (file)
index 0000000..7bdf49c
Binary files /dev/null and b/dep/django-selectable/selectable/locale/zh_CN/LC_MESSAGES/django.mo differ
diff --git a/dep/django-selectable/selectable/locale/zh_CN/LC_MESSAGES/django.po b/dep/django-selectable/selectable/locale/zh_CN/LC_MESSAGES/django.po
new file mode 100644 (file)
index 0000000..ae693db
--- /dev/null
@@ -0,0 +1,28 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# 
+# Translators:
+# mozillazg <opensource.mozillazg@gmail.com>, 2013
+msgid ""
+msgstr ""
+"Project-Id-Version: django-selectable\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2012-10-06 15:02-0400\n"
+"PO-Revision-Date: 2013-11-21 05:08+0000\n"
+"Last-Translator: mozillazg <opensource.mozillazg@gmail.com>\n"
+"Language-Team: Chinese (China) (http://www.transifex.com/projects/p/django-selectable/language/zh_CN/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: zh_CN\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: base.py:115
+msgid "Show more results"
+msgstr "显示更多结果"
+
+#: forms/fields.py:19 forms/fields.py:63
+msgid ""
+"Select a valid choice. That choice is not one of the available choices."
+msgstr "请选择一个有效的选项。当前选项无效。"
index 93c77d82e6019453b7e9bae8f7a6a6774b33a1df..be1af0b8e9f1599e28a11f574b27794130aed3a0 100644 (file)
@@ -40,16 +40,23 @@ def autodiscover():
 
     import copy
     from django.conf import settings
-    from django.utils.importlib import import_module
-    from django.utils.module_loading import module_has_submodule
-
-    for app in settings.INSTALLED_APPS:
-        mod = import_module(app)
-        # Attempt to import the app's lookups module.
-        try:
-            before_import_registry = copy.copy(registry._registry)
-            import_module('%s.lookups' % app)
-        except:
-            registry._registry = before_import_registry
-            if module_has_submodule(mod, 'lookups'):
-                raise
+    
+    try:
+        from django.utils.module_loading import autodiscover_modules
+    except ImportError:
+        from django.utils.importlib import import_module
+        from django.utils.module_loading import module_has_submodule
+
+        def autodiscover_modules(submod, **kwargs):
+            for app_name in settings.INSTALLED_APPS:
+                mod = import_module(app_name)
+                try:
+                    before_import_registry = copy.copy(registry._registry)
+                    import_module('%s.lookups' % app_name)
+                except:
+                    registry._registry = before_import_registry
+                    if module_has_submodule(mod, 'lookups'):
+                        raise
+
+    # Attempt to import the app's lookups module.
+    autodiscover_modules('lookups', register_to=registry)
index 2f3e473b682103f3fbb451773a1a8de4efd1acad..4501ba6794369dab4bc663e0b43b29866ab9e0c1 100644 (file)
@@ -1,9 +1,9 @@
 /*
  * django-selectable UI widget CSS
- * Source: https://bitbucket.org/mlavin/django-selectable
+ * Source: https://github.com/mlavin/django-selectable
  * Docs: http://django-selectable.readthedocs.org/
  *
- * Copyright 2010-2013, Mark Lavin
+ * Copyright 2010-2014, Mark Lavin
  * BSD License
  *
 */
index c931cb191dea2ff25cc981422ddf833c4d85007c..1de6c5338b3a472bbc80abcdf1784ca20da07163 100644 (file)
@@ -1,14 +1,14 @@
 /*jshint trailing:true, indent:4*/
 /*
  * django-selectable UI widget
- * Source: https://bitbucket.org/mlavin/django-selectable
+ * Source: https://github.com/mlavin/django-selectable
  * Docs: http://django-selectable.readthedocs.org/
  *
  * Depends:
- *   - jQuery 1.4.4+
+ *   - jQuery 1.7+
  *   - jQuery UI 1.8 widget factory
  *
- * Copyright 2010-2013, Mark Lavin
+ * Copyright 2010-2014, Mark Lavin
  * BSD License
  *
 */
         },
         close: function (event) {
             var page = $(this.element).data('page');
-            if (page != null) {
+            if (page !== null) {
                 return;
             }
             // Call super trigger
     };
 
     function djangoAdminPatches() {
-        /* 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) {
-                    var options = $.extend({}, opts);
-                    var addedevent = function (row) {
-                        window.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); };
-                    }
-                    options.added = added || addedevent;
-                    return oldformset.call(this, options);
-                };
-            }
-        }
+        /* Listen for new rows being added to the dynamic inlines.
+        Requires Django 1.5+ */
+        $('body').on('click', '.add-row', function (e) {
+            var wrapper = $(this).parents('.inline-related'),
+                newRow = $('.form-row:not(.empty-form)', wrapper).last();
+            window.bindSelectables(newRow);
+        });
 
         /* Monkey-patch Django's dismissAddAnotherPopup(), if defined */
         if (typeof(dismissAddAnotherPopup) !== "undefined" &&
index 41f4398f2fe65384c28d7474d311a3e66bc768df..afac57e992592467bbb0a9d2b52efdb5ba360a1f 100644 (file)
@@ -1,30 +1,34 @@
 from django.db import models
+from django.utils.encoding import python_2_unicode_compatible
 
-from selectable.base import ModelLookup
-from selectable.registry import registry
+from ..base import ModelLookup
+from ..registry import registry
 
 
+@python_2_unicode_compatible
 class Thing(models.Model):
     name = models.CharField(max_length=100)
     description = models.CharField(max_length=100)
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
 
 
+@python_2_unicode_compatible
 class OtherThing(models.Model):
     name = models.CharField(max_length=100)
     thing = models.ForeignKey(Thing)
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
 
 
+@python_2_unicode_compatible
 class ManyThing(models.Model):
     name = models.CharField(max_length=100)
     things = models.ManyToManyField(Thing)
 
-    def __unicode__(self):
+    def __str__(self):
         return self.name
 
 
@@ -36,11 +40,11 @@ class ThingLookup(ModelLookup):
 registry.register(ThingLookup)
 
 
-from selectable.tests.base import *
-from selectable.tests.decorators import *
-from selectable.tests.fields import *
-from selectable.tests.functests import *
-from selectable.tests.forms import *
-from selectable.tests.templatetags import *
-from selectable.tests.views import *
-from selectable.tests.widgets import *
+from .test_base import *
+from .test_decorators import *
+from .test_fields import *
+from .test_functional import *
+from .test_forms import *
+from .test_templatetags import *
+from .test_views import *
+from .test_widgets import *
index dfaba10e9241fac9ada9bad08c3badb0f633d4ab..fdb4f16982f443122912b409ec567cc8e2e9aefa 100644 (file)
@@ -5,19 +5,10 @@ import string
 from xml.dom.minidom import parseString
 
 from django.conf import settings
-from django.core.urlresolvers import reverse
 from django.test import TestCase
-from django.utils.html import escape
-from django.utils.safestring import SafeData, mark_safe
 
-from selectable.base import ModelLookup
-from selectable.tests import Thing
-
-__all__ = (
-    'ModelLookupTestCase',
-    'MultiFieldLookupTestCase',
-    'LookupEscapingTestCase',
-)
+from ..base import ModelLookup
+from . import Thing
 
 
 def as_xml(html):
@@ -69,120 +60,4 @@ class BaseSelectableTestCase(TestCase):
 
 class SimpleModelLookup(ModelLookup):
     model = Thing
-    search_fields = ('name__icontains', )
-
-
-class ModelLookupTestCase(BaseSelectableTestCase):
-    lookup_cls = SimpleModelLookup
-
-    def get_lookup_instance(self):
-        return self.__class__.lookup_cls()
-
-    def test_get_name(self):
-        name = self.__class__.lookup_cls.name()
-        self.assertEqual(name, 'tests-simplemodellookup')
-
-    def test_get_url(self):
-        url = self.__class__.lookup_cls.url()
-        test_url = reverse('selectable-lookup', args=['tests-simplemodellookup'])
-        self.assertEqual(url, test_url)
-
-    def test_format_item(self):
-        lookup = self.get_lookup_instance()
-        thing = Thing()
-        item_info = lookup.format_item(thing)
-        self.assertTrue('id' in item_info)
-        self.assertTrue('value' in item_info)
-        self.assertTrue('label' in item_info)
-
-    def test_get_query(self):
-        lookup = self.get_lookup_instance()
-        thing = self.create_thing(data={'name': 'Thing'})
-        other_thing = self.create_thing(data={'name': 'Other Thing'})
-        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))
-
-    def test_create_item(self):
-        value = self.get_random_string()
-        lookup = self.get_lookup_instance()
-        thing = lookup.create_item(value)
-        self.assertEqual(thing.__class__, Thing)
-        self.assertEqual(thing.name, value)
-        self.assertFalse(thing.pk)
-
-    def test_get_item(self):
-        lookup = self.get_lookup_instance()
-        thing = self.create_thing(data={'name': 'Thing'})
-        item = lookup.get_item(thing.pk)
-        self.assertEqual(thing, item)
-
-    def test_format_item_escaping(self):
-        "Id, value and label should be escaped."
-        lookup = self.get_lookup_instance()
-        thing = self.create_thing(data={'name': 'Thing'})
-        item_info = lookup.format_item(thing)
-        self.assertFalse(isinstance(item_info['id'], SafeData))
-        self.assertFalse(isinstance(item_info['value'], SafeData))
-        self.assertTrue(isinstance(item_info['label'], SafeData))
-
-
-class MultiFieldLookup(ModelLookup):
-    model = Thing
-    search_fields = ('name__icontains', 'description__icontains', )
-
-
-class MultiFieldLookupTestCase(ModelLookupTestCase):
-    lookup_cls = MultiFieldLookup
-
-    def test_get_name(self):
-        name = self.__class__.lookup_cls.name()
-        self.assertEqual(name, 'tests-multifieldlookup')
-
-    def test_get_url(self):
-        url = self.__class__.lookup_cls.url()
-        test_url = reverse('selectable-lookup', args=['tests-multifieldlookup'])
-        self.assertEqual(url, test_url)
-
-    def test_description_search(self):
-        lookup = self.get_lookup_instance()
-        thing = self.create_thing(data={'description': 'Thing'})
-        other_thing = self.create_thing(data={'description': 'Other Thing'})
-        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 HTMLLookup(ModelLookup):
-    model = Thing
-    search_fields = ('name__icontains', )
-
-
-class SafeHTMLLookup(ModelLookup):
-    model = Thing
-    search_fields = ('name__icontains', )
-
-    def get_item_label(self, item):
-        "Mark label as safe."
-        return mark_safe(item.name)
-
-
-class LookupEscapingTestCase(BaseSelectableTestCase):
-
-    def test_escape_html(self):
-        "HTML should be escaped by default."
-        lookup = HTMLLookup()
-        bad_name = "<script>alert('hacked');</script>"
-        escaped_name = escape(bad_name)
-        thing = self.create_thing(data={'name': bad_name})
-        item_info = lookup.format_item(thing)
-        self.assertEqual(item_info['label'], escaped_name)
-
-    def test_conditional_escape(self):
-        "Methods should be able to mark values as safe."
-        lookup = SafeHTMLLookup()
-        bad_name = "<script>alert('hacked');</script>"
-        escaped_name = escape(bad_name)
-        thing = self.create_thing(data={'name': bad_name})
-        item_info = lookup.format_item(thing)
-        self.assertEqual(item_info['label'], bad_name)
+    search_fields = ('name__icontains', )
\ No newline at end of file
diff --git a/dep/django-selectable/selectable/tests/test_base.py b/dep/django-selectable/selectable/tests/test_base.py
new file mode 100644 (file)
index 0000000..2b1581f
--- /dev/null
@@ -0,0 +1,131 @@
+from __future__ import unicode_literals
+
+from django.core.urlresolvers import reverse
+from django.utils.html import escape
+from django.utils.safestring import SafeData, mark_safe
+
+from ..base import ModelLookup
+from . import Thing
+from .base import BaseSelectableTestCase, SimpleModelLookup
+
+__all__ = (
+    'ModelLookupTestCase',
+    'MultiFieldLookupTestCase',
+    'LookupEscapingTestCase',
+)
+
+
+class ModelLookupTestCase(BaseSelectableTestCase):
+    lookup_cls = SimpleModelLookup
+
+    def get_lookup_instance(self):
+        return self.__class__.lookup_cls()
+
+    def test_get_name(self):
+        name = self.__class__.lookup_cls.name()
+        self.assertEqual(name, 'tests-simplemodellookup')
+
+    def test_get_url(self):
+        url = self.__class__.lookup_cls.url()
+        test_url = reverse('selectable-lookup', args=['tests-simplemodellookup'])
+        self.assertEqual(url, test_url)
+
+    def test_format_item(self):
+        lookup = self.get_lookup_instance()
+        thing = Thing()
+        item_info = lookup.format_item(thing)
+        self.assertTrue('id' in item_info)
+        self.assertTrue('value' in item_info)
+        self.assertTrue('label' in item_info)
+
+    def test_get_query(self):
+        lookup = self.get_lookup_instance()
+        thing = self.create_thing(data={'name': 'Thing'})
+        other_thing = self.create_thing(data={'name': 'Other Thing'})
+        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))
+
+    def test_create_item(self):
+        value = self.get_random_string()
+        lookup = self.get_lookup_instance()
+        thing = lookup.create_item(value)
+        self.assertEqual(thing.__class__, Thing)
+        self.assertEqual(thing.name, value)
+        self.assertFalse(thing.pk)
+
+    def test_get_item(self):
+        lookup = self.get_lookup_instance()
+        thing = self.create_thing(data={'name': 'Thing'})
+        item = lookup.get_item(thing.pk)
+        self.assertEqual(thing, item)
+
+    def test_format_item_escaping(self):
+        "Id, value and label should be escaped."
+        lookup = self.get_lookup_instance()
+        thing = self.create_thing(data={'name': 'Thing'})
+        item_info = lookup.format_item(thing)
+        self.assertFalse(isinstance(item_info['id'], SafeData))
+        self.assertFalse(isinstance(item_info['value'], SafeData))
+        self.assertTrue(isinstance(item_info['label'], SafeData))
+
+
+class MultiFieldLookup(ModelLookup):
+    model = Thing
+    search_fields = ('name__icontains', 'description__icontains', )
+
+
+class MultiFieldLookupTestCase(ModelLookupTestCase):
+    lookup_cls = MultiFieldLookup
+
+    def test_get_name(self):
+        name = self.__class__.lookup_cls.name()
+        self.assertEqual(name, 'tests-multifieldlookup')
+
+    def test_get_url(self):
+        url = self.__class__.lookup_cls.url()
+        test_url = reverse('selectable-lookup', args=['tests-multifieldlookup'])
+        self.assertEqual(url, test_url)
+
+    def test_description_search(self):
+        lookup = self.get_lookup_instance()
+        thing = self.create_thing(data={'description': 'Thing'})
+        other_thing = self.create_thing(data={'description': 'Other Thing'})
+        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 HTMLLookup(ModelLookup):
+    model = Thing
+    search_fields = ('name__icontains', )
+
+
+class SafeHTMLLookup(ModelLookup):
+    model = Thing
+    search_fields = ('name__icontains', )
+
+    def get_item_label(self, item):
+        "Mark label as safe."
+        return mark_safe(item.name)
+
+
+class LookupEscapingTestCase(BaseSelectableTestCase):
+
+    def test_escape_html(self):
+        "HTML should be escaped by default."
+        lookup = HTMLLookup()
+        bad_name = "<script>alert('hacked');</script>"
+        escaped_name = escape(bad_name)
+        thing = self.create_thing(data={'name': bad_name})
+        item_info = lookup.format_item(thing)
+        self.assertEqual(item_info['label'], escaped_name)
+
+    def test_conditional_escape(self):
+        "Methods should be able to mark values as safe."
+        lookup = SafeHTMLLookup()
+        bad_name = "<script>alert('hacked');</script>"
+        escaped_name = escape(bad_name)
+        thing = self.create_thing(data={'name': bad_name})
+        item_info = lookup.format_item(thing)
+        self.assertEqual(item_info['label'], bad_name)
diff --git a/dep/django-selectable/selectable/tests/test_decorators.py b/dep/django-selectable/selectable/tests/test_decorators.py
new file mode 100644 (file)
index 0000000..a9f569f
--- /dev/null
@@ -0,0 +1,94 @@
+try:
+    from unittest.mock import Mock
+except ImportError:
+    from mock import Mock
+
+from ..decorators import ajax_required, login_required, staff_member_required
+from .base import BaseSelectableTestCase, SimpleModelLookup
+
+
+__all__ = (
+    'AjaxRequiredLookupTestCase',
+    'LoginRequiredLookupTestCase',
+    'StaffRequiredLookupTestCase',
+)
+
+
+class AjaxRequiredLookupTestCase(BaseSelectableTestCase):
+
+    def setUp(self):
+        self.lookup = ajax_required(SimpleModelLookup)()
+
+    def test_ajax_call(self):
+        "Ajax call should yield a successful response."
+        request = Mock()
+        request.is_ajax = lambda: True
+        response = self.lookup.results(request)
+        self.assertTrue(response.status_code, 200)
+
+    def test_non_ajax_call(self):
+        "Non-Ajax call should yield a bad request response."
+        request = Mock()
+        request.is_ajax = lambda: False
+        response = self.lookup.results(request)
+        self.assertEqual(response.status_code, 400)
+
+
+class LoginRequiredLookupTestCase(BaseSelectableTestCase):
+
+    def setUp(self):
+        self.lookup = login_required(SimpleModelLookup)()
+    
+    def test_authenicated_call(self):
+        "Authenicated call should yield a successful response."
+        request = Mock()
+        user = Mock()
+        user.is_authenticated = lambda: True
+        request.user = user
+        response = self.lookup.results(request)
+        self.assertTrue(response.status_code, 200)
+
+    def test_non_authenicated_call(self):
+        "Non-Authenicated call should yield an unauthorized response."
+        request = Mock()
+        user = Mock()
+        user.is_authenticated = lambda: False
+        request.user = user
+        response = self.lookup.results(request)
+        self.assertEqual(response.status_code, 401)
+
+
+class StaffRequiredLookupTestCase(BaseSelectableTestCase):
+
+    def setUp(self):
+        self.lookup = staff_member_required(SimpleModelLookup)()
+
+    def test_staff_member_call(self):
+        "Staff member call should yield a successful response."
+        request = Mock()
+        user = Mock()
+        user.is_authenticated = lambda: True
+        user.is_staff = True
+        request.user = user
+        response = self.lookup.results(request)
+        self.assertTrue(response.status_code, 200)
+
+    def test_authenicated_but_not_staff(self):
+        "Authenicated but non staff call should yield a forbidden response."
+        request = Mock()
+        user = Mock()
+        user.is_authenticated = lambda: True
+        user.is_staff = False
+        request.user = user
+        response = self.lookup.results(request)
+        self.assertTrue(response.status_code, 403)
+
+    def test_non_authenicated_call(self):
+        "Non-Authenicated call should yield an unauthorized response."
+        request = Mock()
+        user = Mock()
+        user.is_authenticated = lambda: False
+        user.is_staff = False
+        request.user = user
+        response = self.lookup.results(request)
+        self.assertEqual(response.status_code, 401)
diff --git a/dep/django-selectable/selectable/tests/test_fields.py b/dep/django-selectable/selectable/tests/test_fields.py
new file mode 100644 (file)
index 0000000..e962003
--- /dev/null
@@ -0,0 +1,134 @@
+from django import forms
+
+from selectable.forms import fields, widgets
+from selectable.tests import ThingLookup
+from selectable.tests.base import BaseSelectableTestCase
+
+
+__all__ = (
+    'AutoCompleteSelectFieldTestCase',
+    'AutoCompleteSelectMultipleFieldTestCase',
+)
+
+class FieldTestMixin(object):
+    field_cls = None
+    lookup_cls = None
+
+    def get_field_instance(self, allow_new=False, limit=None, widget=None):
+        return self.field_cls(self.lookup_cls, allow_new=allow_new, limit=limit, widget=widget)
+
+    def test_init(self):
+        field = self.get_field_instance()
+        self.assertEqual(field.lookup_class, self.lookup_cls)
+
+    def test_init_with_limit(self):
+        field = self.get_field_instance(limit=10)
+        self.assertEqual(field.limit, 10)
+        self.assertEqual(field.widget.limit, 10)
+
+    def test_clean(self):
+        self.fail('This test has not yet been written')
+
+    def test_dotted_path(self):
+        """
+        Ensure lookup_class can be imported from a dotted path.
+        """
+        dotted_path = '.'.join([self.lookup_cls.__module__, self.lookup_cls.__name__])
+        field = self.field_cls(dotted_path)
+        self.assertEqual(field.lookup_class, self.lookup_cls)
+
+    def test_invalid_dotted_path(self):
+        """
+        An invalid lookup_class dotted path should raise an ImportError.
+        """
+        with self.assertRaises(ImportError):
+            self.field_cls('this.is.an.invalid.path')
+
+    def test_dotted_path_wrong_type(self):
+        """
+        lookup_class must be a subclass of LookupBase.
+        """
+        dotted_path = 'selectable.forms.fields.AutoCompleteSelectField'
+        with self.assertRaises(TypeError):
+            self.field_cls(dotted_path)
+
+class AutoCompleteSelectFieldTestCase(BaseSelectableTestCase, FieldTestMixin):
+    field_cls = fields.AutoCompleteSelectField
+    lookup_cls = ThingLookup
+
+    def test_clean(self):
+        thing = self.create_thing()
+        field = self.get_field_instance()
+        value = field.clean([thing.name, thing.id])
+        self.assertEqual(thing, value)
+
+    def test_new_not_allowed(self):
+        field = self.get_field_instance()
+        value = self.get_random_string()
+        self.assertRaises(forms.ValidationError, field.clean, [value, ''])
+
+    def test_new_allowed(self):
+        field = self.get_field_instance(allow_new=True)
+        value = self.get_random_string()
+        value = field.clean([value, ''])
+        self.assertTrue(isinstance(value, ThingLookup.model))
+
+    def test_default_widget(self):
+        field = self.get_field_instance()
+        self.assertTrue(isinstance(field.widget, widgets.AutoCompleteSelectWidget))
+
+    def test_alternate_widget(self):
+        widget_cls = widgets.AutoComboboxWidget
+        field = self.get_field_instance(widget=widget_cls)
+        self.assertTrue(isinstance(field.widget, widget_cls))
+
+    def test_alternate_widget_instance(self):
+        widget = widgets.AutoComboboxWidget(self.lookup_cls)
+        field = self.get_field_instance(widget=widget)
+        self.assertTrue(isinstance(field.widget, widgets.AutoComboboxWidget))
+
+    def test_invalid_pk(self):
+        field = self.get_field_instance()
+        value = self.get_random_string()
+        self.assertRaises(forms.ValidationError, field.clean, [value, 'XXX'])
+
+
+class AutoCompleteSelectMultipleFieldTestCase(BaseSelectableTestCase, FieldTestMixin):
+    field_cls = fields.AutoCompleteSelectMultipleField
+    lookup_cls = ThingLookup
+
+    def get_field_instance(self, limit=None, widget=None):
+        return self.field_cls(self.lookup_cls, limit=limit, widget=widget)
+
+    def test_clean(self):
+        thing = self.create_thing()
+        field = self.get_field_instance()
+        value = field.clean([thing.id])
+        self.assertEqual([thing], value)
+
+    def test_clean_multiple(self):
+        thing = self.create_thing()
+        other_thing = self.create_thing()
+        field = self.get_field_instance()
+        ids = [thing.id, other_thing.id]
+        value = field.clean(ids)
+        self.assertEqual([thing, other_thing], value)
+
+    def test_default_widget(self):
+        field = self.get_field_instance()
+        self.assertTrue(isinstance(field.widget, widgets.AutoCompleteSelectMultipleWidget))
+
+    def test_alternate_widget(self):
+        widget_cls = widgets.AutoComboboxSelectMultipleWidget
+        field = self.get_field_instance(widget=widget_cls)
+        self.assertTrue(isinstance(field.widget, widget_cls))
+
+    def test_alternate_widget_instance(self):
+        widget = widgets.AutoComboboxSelectMultipleWidget(self.lookup_cls)
+        field = self.get_field_instance(widget=widget)
+        self.assertTrue(isinstance(field.widget, widgets.AutoComboboxSelectMultipleWidget))
+
+    def test_invalid_pk(self):
+        field = self.get_field_instance()
+        value = self.get_random_string()
+        self.assertRaises(forms.ValidationError, field.clean, ['XXX', ])
diff --git a/dep/django-selectable/selectable/tests/test_forms.py b/dep/django-selectable/selectable/tests/test_forms.py
new file mode 100644 (file)
index 0000000..7bce96e
--- /dev/null
@@ -0,0 +1,86 @@
+from django.conf import settings
+
+from ..forms import BaseLookupForm
+from .base import BaseSelectableTestCase, PatchSettingsMixin
+
+
+__all__ = (
+    'BaseLookupFormTestCase',
+)
+
+
+class BaseLookupFormTestCase(PatchSettingsMixin, BaseSelectableTestCase):
+
+    def get_valid_data(self):
+        data = {
+            'term': 'foo',
+            'limit': 10,
+        }
+        return data
+
+    def test_valid_data(self):
+        data = self.get_valid_data()
+        form = BaseLookupForm(data)
+        self.assertTrue(form.is_valid(), "%s" % form.errors)
+
+    def test_invalid_limit(self):
+        """
+        Test giving the form an invalid limit.
+        """
+
+        data = self.get_valid_data()
+        data['limit'] = 'bar'
+        form = BaseLookupForm(data)
+        self.assertFalse(form.is_valid())
+
+    def test_no_limit(self):
+        """
+        If SELECTABLE_MAX_LIMIT is set and limit is not given then
+        the form will return SELECTABLE_MAX_LIMIT.
+        """
+
+        data = self.get_valid_data()
+        if 'limit' in data:
+            del data['limit']
+        form = BaseLookupForm(data)
+        self.assertTrue(form.is_valid(), "%s" % form.errors)
+        self.assertEqual(form.cleaned_data['limit'], settings.SELECTABLE_MAX_LIMIT)
+
+    def test_no_max_set(self):
+        """
+        If SELECTABLE_MAX_LIMIT is not set but given then the form
+        will return the given limit.
+        """
+
+        settings.SELECTABLE_MAX_LIMIT = None
+        data = self.get_valid_data()
+        form = BaseLookupForm(data)
+        self.assertTrue(form.is_valid(), "%s" % form.errors)
+        if 'limit' in data:
+            self.assertTrue(form.cleaned_data['limit'], data['limit'])
+
+    def test_no_max_set_not_given(self):
+        """
+        If SELECTABLE_MAX_LIMIT is not set and not given then the form
+        will return no limit.
+        """
+
+        settings.SELECTABLE_MAX_LIMIT = None
+        data = self.get_valid_data()
+        if 'limit' in data:
+            del data['limit']
+        form = BaseLookupForm(data)
+        self.assertTrue(form.is_valid(), "%s" % form.errors)
+        self.assertFalse(form.cleaned_data.get('limit'))
+
+    def test_over_limit(self):
+        """
+        If SELECTABLE_MAX_LIMIT is set and limit given is greater then
+        the form will return SELECTABLE_MAX_LIMIT.
+        """
+
+        data = self.get_valid_data()
+        data['limit'] = settings.SELECTABLE_MAX_LIMIT + 100
+        form = BaseLookupForm(data)
+        self.assertTrue(form.is_valid(), "%s" % form.errors)
+        self.assertEqual(form.cleaned_data['limit'], settings.SELECTABLE_MAX_LIMIT)
diff --git a/dep/django-selectable/selectable/tests/test_functional.py b/dep/django-selectable/selectable/tests/test_functional.py
new file mode 100644 (file)
index 0000000..01bf9bf
--- /dev/null
@@ -0,0 +1,538 @@
+"""
+Larger functional tests for fields and widgets.
+"""
+from __future__ import unicode_literals
+
+from django import forms
+
+from ..forms import AutoCompleteSelectField, AutoCompleteSelectMultipleField
+from ..forms import AutoCompleteSelectWidget, AutoComboboxSelectWidget
+from . import ManyThing, OtherThing, ThingLookup
+from .base import BaseSelectableTestCase, parsed_inputs
+
+
+__all__ = (
+    'FuncAutoCompleteSelectTestCase',
+    'FuncSelectModelChoiceTestCase',
+    'FuncComboboxModelChoiceTestCase',
+    'FuncManytoManyMultipleSelectTestCase',
+    'FuncFormTestCase',
+)
+
+
+class OtherThingForm(forms.ModelForm):
+
+    thing = AutoCompleteSelectField(lookup_class=ThingLookup)
+
+    class Meta(object):
+        model = OtherThing
+
+
+class FuncAutoCompleteSelectTestCase(BaseSelectableTestCase):
+
+    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 = OtherThingForm(data=data)
+        self.assertTrue(form.is_valid(), str(form.errors))
+
+    def test_invalid_form_missing_selected_pk(self):
+        "Invalid form using an AutoCompleteSelectField."
+        data = {
+            'name': self.get_random_string(),
+            'thing_0': self.test_thing.name, # Text input
+            'thing_1': '', # Hidden input
+        }
+        form = OtherThingForm(data=data)
+        self.assertFalse(form.is_valid(), 'Form should not be valid')
+        self.assertFalse('name' in form.errors)
+        self.assertTrue('thing' in form.errors)
+
+    def test_invalid_form_missing_name(self):
+        "Invalid form using an AutoCompleteSelectField."
+        data = {
+            'name': '',
+            'thing_0': self.test_thing.name, # Text input
+            'thing_1': self.test_thing.pk, # Hidden input
+        }
+        form = OtherThingForm(data=data)
+        self.assertFalse(form.is_valid(), 'Form should not be valid')
+        self.assertTrue('name' in form.errors)
+        self.assertFalse('thing' in form.errors)
+
+    def test_invalid_but_still_selected(self):
+        "Invalid form should keep selected item."
+        data = {
+            'name': '',
+            'thing_0': self.test_thing.name, # Text input
+            'thing_1': self.test_thing.pk, # Hidden input
+        }
+        form = OtherThingForm(data=data)
+        self.assertFalse(form.is_valid(), 'Form should not be valid')
+        rendered_form = form.as_p()
+        inputs = parsed_inputs(rendered_form)
+        # Selected text should be populated
+        thing_0 = inputs['thing_0'][0]
+        self.assertEqual(thing_0.attributes['value'].value, self.test_thing.name)
+        # Selected pk should be populated
+        thing_1 = inputs['thing_1'][0]
+        self.assertEqual(int(thing_1.attributes['value'].value), self.test_thing.pk)
+
+    def test_populate_from_model(self):
+        "Populate from existing model."
+        other_thing = OtherThing.objects.create(thing=self.test_thing, name='a')
+        form = OtherThingForm(instance=other_thing)
+        rendered_form = form.as_p()
+        inputs = parsed_inputs(rendered_form)
+        # Selected text should be populated
+        thing_0 = inputs['thing_0'][0]
+        self.assertEqual(thing_0.attributes['value'].value, self.test_thing.name)
+        # Selected pk should be populated
+        thing_1 = inputs['thing_1'][0]
+        self.assertEqual(int(thing_1.attributes['value'].value), self.test_thing.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 AutoCompleteSelectWidget."
+        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))
+
+    def test_missing_pk(self):
+        "Invalid form (missing required pk) using an AutoCompleteSelectWidget."
+        data = {
+            'name': self.get_random_string(),
+            'thing_0': self.test_thing.name, # Text input
+            'thing_1': '', # Hidden input missing
+        }
+        form = SelectWidgetForm(data=data)
+        self.assertFalse(form.is_valid())
+        self.assertTrue('thing' in form.errors)
+
+    def test_invalid_pk(self):
+        "Invalid form (invalid pk value) using an AutoCompleteSelectWidget."
+        data = {
+            'name': self.get_random_string(),
+            'thing_0': self.test_thing.name, # Text input
+            'thing_1': 'XXX', # Hidden input doesn't match a PK
+        }
+        form = SelectWidgetForm(data=data)
+        self.assertFalse(form.is_valid())
+        self.assertTrue('thing' in form.errors)
+
+    def test_post_compatibility(self):
+        """
+        If new items are not allowed then the original field
+        name can be included in the POST with the selected id.
+        """
+        data = {
+            'name': self.get_random_string(),
+            'thing': self.test_thing.pk,
+        }
+        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 AutoComboboxSelectWidget."
+        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))
+
+    def test_missing_pk(self):
+        "Invalid form (missing required pk) using an AutoComboboxSelectWidget."
+        data = {
+            'name': self.get_random_string(),
+            'thing_0': self.test_thing.name, # Text input
+            'thing_1': '', # Hidden input missing
+        }
+        form = ComboboxSelectWidgetForm(data=data)
+        self.assertFalse(form.is_valid())
+        self.assertTrue('thing' in form.errors)
+
+    def test_invalid_pk(self):
+        "Invalid form (invalid pk value) using an AutoComboboxSelectWidget."
+        data = {
+            'name': self.get_random_string(),
+            'thing_0': self.test_thing.name, # Text input
+            'thing_1': 'XXX', # Hidden input doesn't match a PK
+        }
+        form = ComboboxSelectWidgetForm(data=data)
+        self.assertFalse(form.is_valid())
+        self.assertTrue('thing' in form.errors)
+
+    def test_post_compatibility(self):
+        """
+        If new items are not allowed then the original field
+        name can be included in the POST with the selected id.
+        """
+        data = {
+            'name': self.get_random_string(),
+            'thing': self.test_thing.pk,
+        }
+        form = ComboboxSelectWidgetForm(data=data)
+        self.assertTrue(form.is_valid(), str(form.errors))
+
+
+class ManyThingForm(forms.ModelForm):
+
+    things = AutoCompleteSelectMultipleField(lookup_class=ThingLookup)
+
+    class Meta(object):
+        model = ManyThing
+
+
+class FuncManytoManyMultipleSelectTestCase(BaseSelectableTestCase):
+    """
+    Functional tests for AutoCompleteSelectMultipleField compatibility
+    with a ManyToManyField.
+    """
+
+    def setUp(self):
+        self.test_thing = self.create_thing()
+
+    def test_valid_form(self):
+        "Valid form using an AutoCompleteSelectMultipleField."
+        data = {
+            'name': self.get_random_string(),
+            'things_0': '', # Text input
+            'things_1': [self.test_thing.pk, ], # Hidden inputs
+        }
+        form = ManyThingForm(data=data)
+        self.assertTrue(form.is_valid(), str(form.errors))
+
+    def test_valid_save(self):
+        "Saving data from a valid form."
+        data = {
+            'name': self.get_random_string(),
+            'things_0': '', # Text input
+            'things_1': [self.test_thing.pk, ], # Hidden inputs
+        }
+        form = ManyThingForm(data=data)
+        manything = form.save()
+        self.assertEqual(manything.name, data['name'])
+        things = manything.things.all()
+        self.assertEqual(things.count(), 1)
+        self.assertTrue(self.test_thing in things)
+
+    def test_not_required(self):
+        "Valid form where many to many is not required."
+        data = {
+            'name': self.get_random_string(),
+            'things_0': '', # Text input
+            'things_1': [], # Hidden inputs
+        }
+        form = ManyThingForm(data=data)
+        form.fields['things'].required = False
+        self.assertTrue(form.is_valid(), str(form.errors))
+
+    def test_not_required_save(self):
+        "Saving data when many to many is not required."
+        data = {
+            'name': self.get_random_string(),
+            'things_0': '', # Text input
+            'things_1': [], # Hidden inputs
+        }
+        form = ManyThingForm(data=data)
+        form.fields['things'].required = False
+        manything = form.save()
+        self.assertEqual(manything.name, data['name'])
+        things = manything.things.all()
+        self.assertEqual(things.count(), 0)
+
+    def test_has_changed(self):
+        "Populate intial data from a model."
+        manything = ManyThing.objects.create(name='Foo')
+        thing_1 = self.create_thing()
+        manything.things.add(thing_1)
+        data = {
+            'name': manything.name,
+            'things_0': '', # Text input
+            'things_1': [thing_1.pk], # Hidden inputs
+        }
+        form = ManyThingForm(data=data, instance=manything)
+        self.assertFalse(form.has_changed(), str(form.changed_data))
+
+    def test_post_compatibility(self):
+        """
+        If new items are not allowed then the original field
+        name can be included in the POST with the selected ids.
+        """
+        data = {
+            'name': self.get_random_string(),
+            'things': [self.test_thing.pk, ],
+        }
+        form = ManyThingForm(data=data)
+        self.assertTrue(form.is_valid(), str(form.errors))
+
+
+class SimpleForm(forms.Form):
+    "Non-model form usage."
+    thing = AutoCompleteSelectField(lookup_class=ThingLookup)
+    new_thing = AutoCompleteSelectField(lookup_class=ThingLookup, allow_new=True)
+    things = AutoCompleteSelectMultipleField(lookup_class=ThingLookup)
+
+
+class FuncFormTestCase(BaseSelectableTestCase):
+    """
+    Functional tests for using AutoCompleteSelectField
+    and AutoCompleteSelectMultipleField outside the context
+    of a ModelForm.
+    """
+
+    def setUp(self):
+        self.test_thing = self.create_thing()
+
+    def test_blank_new_item(self):
+        "Regression test for #91. new_thing is required but both are blank."
+        data = {
+            'thing_0': self.test_thing.name,
+            'thing_1': self.test_thing.pk,
+            'new_thing_0': '',
+            'new_thing_1': '',
+            'things_0': '',
+            'things_1': [self.test_thing.pk, ]
+        }
+        form = SimpleForm(data=data)
+        self.assertFalse(form.is_valid())
+        self.assertTrue('new_thing' in form.errors)
+
+    def test_has_changed_with_empty_permitted(self):
+        """
+        Regression test for #92. has_changed fails when there is no initial and
+        allow_new=False.
+        """
+        data = {
+            'thing_0': '',
+            'thing_1': self.test_thing.pk,
+            'new_thing_0': self.test_thing.name,
+            'new_thing_1': self.test_thing.pk,
+            'things_0': '',
+            'things_1': [self.test_thing.pk, ]
+        }
+        form = SimpleForm(data=data, empty_permitted=True)
+        self.assertTrue(form.has_changed())
+        self.assertTrue(form.is_valid(), str(form.errors))
+
+    def test_not_changed(self):
+        """
+        Regression test for #92. has_changed fails when there is no initial and
+        allow_new=False.
+        """
+        data = {
+            'thing_0': self.test_thing.name,
+            'thing_1': self.test_thing.pk,
+            'new_thing_0': self.test_thing.name,
+            'new_thing_1': self.test_thing.pk,
+            'things_0': '',
+            'things_1': [self.test_thing.pk, ]
+        }
+        initial = {
+            'thing': self.test_thing.pk,
+            'new_thing': self.test_thing.pk,
+            'things': [self.test_thing.pk, ]
+        }
+        form = SimpleForm(data=data, initial=initial)
+        self.assertFalse(form.has_changed())
+        self.assertTrue(form.is_valid(), str(form.errors))
+
+    def test_not_changed_with_empty_permitted(self):
+        """
+        Regression test for #92. has_changed fails when there is no initial and
+        allow_new=False.
+        """
+        data = {
+            'thing_0': '',
+            'thing_1': '',
+            'new_thing_0': '',
+            'new_thing_1': '',
+            'things_0': '',
+            'things_1': '',
+        }
+        initial = {
+            'thing': '',
+            'new_thing': '',
+            'things': '',
+        }
+        form = SimpleForm(data=data, initial=initial, empty_permitted=True)
+        self.assertFalse(form.has_changed(), str(form.changed_data))
+        self.assertTrue(form.is_valid(), str(form.errors))
+
+    def test_no_initial_with_empty_permitted(self):
+        """
+        If empty data is submitted and allowed with no initial then
+        the form should not be seen as changed.
+        """
+        data = {
+            'thing_0': '',
+            'thing_1': '',
+            'new_thing_0': '',
+            'new_thing_1': '',
+            'things_0': '',
+            'things_1': '',
+        }
+        form = SimpleForm(data=data, empty_permitted=True)
+        self.assertFalse(form.has_changed(), str(form.changed_data))
+        self.assertTrue(form.is_valid(), str(form.errors))
+
+    def test_no_data_with_empty_permitted(self):
+        """
+        If no data is submitted and allowed with no initial then
+        the form should not be seen as changed.
+        """
+        form = SimpleForm(data={}, empty_permitted=True)
+        self.assertFalse(form.has_changed(), str(form.changed_data))
+        self.assertTrue(form.is_valid(), str(form.errors))
+
+    def test_select_multiple_changed(self):
+        """
+        Detect changes for a multiple select input with and without
+        initial data.
+        """
+        data = {
+            'thing_0': '',
+            'thing_1': '',
+            'new_thing_0': '',
+            'new_thing_1': '',
+            'things_0': '',
+            'things_1': [self.test_thing.pk, ]
+        }
+        form = SimpleForm(data=data)
+        self.assertTrue(form.has_changed())
+        self.assertTrue('things' in form.changed_data)
+
+        initial = {
+            'thing': '',
+            'new_thing': '',
+            'things': [self.test_thing.pk, ],
+        }
+        form = SimpleForm(data=data, initial=initial)
+        self.assertFalse(form.has_changed(), str(form.changed_data))
+
+        initial = {
+            'thing': '',
+            'new_thing': '',
+            'things': [],
+        }
+        form = SimpleForm(data=data, initial=initial)
+        self.assertTrue(form.has_changed())
+        self.assertTrue('things' in form.changed_data)
+
+    def test_single_select_changed(self):
+        """
+        Detect changes for a single select input with and without
+        initial data.
+        """
+        data = {
+            'thing_0': '',
+            'thing_1': self.test_thing.pk,
+            'new_thing_0': '',
+            'new_thing_1': '',
+            'things_0': '',
+            'things_1': ''
+        }
+        form = SimpleForm(data=data)
+        self.assertTrue(form.has_changed())
+        self.assertTrue('thing' in form.changed_data)
+
+        initial = {
+            'thing': self.test_thing.pk,
+            'new_thing': '',
+            'things': '',
+        }
+        form = SimpleForm(data=data, initial=initial)
+        self.assertFalse(form.has_changed(), str(form.changed_data))
+
+        initial = {
+            'thing': '',
+            'new_thing': '',
+            'things': '',
+        }
+        form = SimpleForm(data=data, initial=initial)
+        self.assertTrue(form.has_changed())
+        self.assertTrue('thing' in form.changed_data)
+
+    def test_new_select_changed(self):
+        """
+        Detect changes for a single select input which allows new items
+        with and without initial data.
+        """
+        data = {
+            'thing_0': '',
+            'thing_1': '',
+            'new_thing_0': 'Foo',
+            'new_thing_1': '',
+            'things_0': '',
+            'things_1': ''
+        }
+        form = SimpleForm(data=data)
+        self.assertTrue(form.has_changed())
+        self.assertTrue('new_thing' in form.changed_data)
+
+        initial = {
+            'thing': '',
+            'new_thing': ['Foo', None],
+            'things': '',
+        }
+        form = SimpleForm(data=data, initial=initial)
+        self.assertFalse(form.has_changed(), str(form.changed_data))
+
+        initial = {
+            'thing': '',
+            'new_thing': '',
+            'things': '',
+        }
+        form = SimpleForm(data=data, initial=initial)
+        self.assertTrue(form.has_changed())
+        self.assertTrue('new_thing' in form.changed_data)
diff --git a/dep/django-selectable/selectable/tests/test_templatetags.py b/dep/django-selectable/selectable/tests/test_templatetags.py
new file mode 100644 (file)
index 0000000..c0042fb
--- /dev/null
@@ -0,0 +1,115 @@
+from django.template import Template, Context
+
+from .base import BaseSelectableTestCase
+
+__all__ = (
+    'JqueryTagTestCase',
+    'ThemeTagTestCase',
+)
+
+
+class JqueryTagTestCase(BaseSelectableTestCase):
+
+    def assertJQueryVersion(self, result, version):
+        expected = "//ajax.googleapis.com/ajax/libs/jquery/%s/jquery.min.js" % version
+        self.assertTrue(expected in result)
+
+    def assertUIVersion(self, result, version):
+        expected = "//ajax.googleapis.com/ajax/libs/jqueryui/%s/jquery-ui.js" % version
+        self.assertTrue(expected in result)
+
+    def test_render(self):
+        "Render template tag with default versions."
+        template = Template("{% load selectable_tags %}{% include_jquery_libs %}")
+        context = Context({})
+        result = template.render(context)
+        self.assertJQueryVersion(result, '1.7.2')
+        self.assertUIVersion(result, '1.8.23')
+
+    def test_render_jquery_version(self):
+        "Render template tag with specified jQuery version."
+        template = Template("{% load selectable_tags %}{% include_jquery_libs '1.4.3' %}")
+        context = Context({})
+        result = template.render(context)
+        self.assertJQueryVersion(result, '1.4.3')
+
+    def test_render_variable_jquery_version(self):
+        "Render using jQuery version from the template context."
+        version = '1.4.3'
+        template = Template("{% load selectable_tags %}{% include_jquery_libs version %}")
+        context = Context({'version': version})
+        result = template.render(context)
+        self.assertJQueryVersion(result, '1.4.3')
+
+    def test_render_jquery_ui_version(self):
+        "Render template tag with specified jQuery UI version."
+        template = Template("{% load selectable_tags %}{% include_jquery_libs '1.4.3' '1.8.13' %}")
+        context = Context({})
+        result = template.render(context)
+        self.assertUIVersion(result, '1.8.13')
+
+    def test_render_variable_jquery_ui_version(self):
+        "Render using jQuery UI version from the template context."
+        version = '1.8.13'
+        template = Template("{% load selectable_tags %}{% include_jquery_libs '1.4.3' version %}")
+        context = Context({'version': version})
+        result = template.render(context)
+        self.assertUIVersion(result, '1.8.13')
+
+    def test_render_no_jquery(self):
+        "Render template tag without jQuery."
+        template = Template("{% load selectable_tags %}{% include_jquery_libs '' %}")
+        context = Context({})
+        result = template.render(context)
+        self.assertTrue('jquery.min.js' not in result)
+
+    def test_render_no_jquery_ui(self):
+        "Render template tag without jQuery UI."
+        template = Template("{% load selectable_tags %}{% include_jquery_libs '1.7.2' '' %}")
+        context = Context({})
+        result = template.render(context)
+        self.assertTrue('jquery-ui.js' not in result)
+
+
+class ThemeTagTestCase(BaseSelectableTestCase):
+
+    def assertUICSS(self, result, theme, version):
+        expected = "//ajax.googleapis.com/ajax/libs/jqueryui/%s/themes/%s/jquery-ui.css" % (version, theme)
+        self.assertTrue(expected in result)
+
+    def test_render(self):
+        "Render template tag with default settings."
+        template = Template("{% load selectable_tags %}{% include_ui_theme %}")
+        context = Context({})
+        result = template.render(context)
+        self.assertUICSS(result, 'base', '1.8.23')
+
+    def test_render_version(self):
+        "Render template tag with alternate version."
+        template = Template("{% load selectable_tags %}{% include_ui_theme 'base' '1.8.13' %}")
+        context = Context({})
+        result = template.render(context)
+        self.assertUICSS(result, 'base', '1.8.13')
+        
+    def test_variable_version(self):
+        "Render using version from content variable."
+        version = '1.8.13'
+        template = Template("{% load selectable_tags %}{% include_ui_theme 'base' version %}")
+        context = Context({'version': version})
+        result = template.render(context)
+        self.assertUICSS(result, 'base', version)
+
+    def test_render_theme(self):
+        "Render template tag with alternate theme."
+        template = Template("{% load selectable_tags %}{% include_ui_theme 'ui-lightness' %}")
+        context = Context({})
+        result = template.render(context)
+        self.assertUICSS(result, 'ui-lightness', '1.8.23')
+        
+    def test_variable_theme(self):
+        "Render using theme from content variable."
+        theme = 'ui-lightness'
+        template = Template("{% load selectable_tags %}{% include_ui_theme theme %}")
+        context = Context({'theme': theme})
+        result = template.render(context)
+        self.assertUICSS(result, theme, '1.8.23')
diff --git a/dep/django-selectable/selectable/tests/test_views.py b/dep/django-selectable/selectable/tests/test_views.py
new file mode 100644 (file)
index 0000000..4dc20c4
--- /dev/null
@@ -0,0 +1,91 @@
+from __future__ import division
+
+import json
+
+from django.conf import settings
+from django.core.urlresolvers import reverse
+
+from . import ThingLookup
+from .base import BaseSelectableTestCase, PatchSettingsMixin
+
+
+__all__ = (
+    'SelectableViewTest',
+)
+
+
+class SelectableViewTest(PatchSettingsMixin, BaseSelectableTestCase):
+
+    def setUp(self):
+        super(SelectableViewTest, self).setUp()
+        self.url = ThingLookup.url()
+        self.lookup = ThingLookup()
+        self.thing = self.create_thing()
+        self.other_thing = self.create_thing()
+
+    def test_response_type(self):
+        response = self.client.get(self.url)
+        self.assertEqual(response['Content-Type'], 'application/json')
+
+    def test_response_keys(self):
+        response = self.client.get(self.url)
+        data = json.loads(response.content.decode('utf-8'))
+        for result in data.get('data'):
+            self.assertTrue('id' in result)
+            self.assertTrue('value' in result)
+            self.assertTrue('label' in result)
+
+    def test_no_term_lookup(self):
+        data = {}
+        response = self.client.get(self.url, data)
+        data = json.loads(response.content.decode('utf-8'))
+        self.assertEqual(len(data), 2)
+
+    def test_simple_term_lookup(self):
+        data = {'term': self.thing.name}
+        response = self.client.get(self.url, data)
+        data = json.loads(response.content.decode('utf-8'))
+        self.assertEqual(len(data), 2)
+        self.assertEqual(len(data.get('data')), 1)
+
+    def test_unknown_lookup(self):
+        unknown_url = reverse('selectable-lookup', args=["XXXXXXX"])
+        response = self.client.get(unknown_url)
+        self.assertEqual(response.status_code, 404)
+
+    def test_basic_limit(self):
+        for i in range(settings.SELECTABLE_MAX_LIMIT):
+            self.create_thing(data={'name': 'Thing%s' % i})
+        response = self.client.get(self.url)
+        data = json.loads(response.content.decode('utf-8'))
+        self.assertEqual(len(data.get('data')), settings.SELECTABLE_MAX_LIMIT)
+        meta = data.get('meta')
+        self.assertTrue('next_page' in meta)
+
+    def test_get_next_page(self):
+        for i in range(settings.SELECTABLE_MAX_LIMIT * 2):
+            self.create_thing(data={'name': 'Thing%s' % i})
+        data = {'term': 'Thing', 'page': 2}
+        response = self.client.get(self.url, data)
+        data = json.loads(response.content.decode('utf-8'))
+        self.assertEqual(len(data.get('data')), settings.SELECTABLE_MAX_LIMIT)
+        # No next page
+        meta = data.get('meta')
+        self.assertFalse('next_page' in meta)
+
+    def test_request_more_than_max(self):
+        for i in range(settings.SELECTABLE_MAX_LIMIT):
+            self.create_thing(data={'name': 'Thing%s' % i})
+        data = {'term': '', 'limit': settings.SELECTABLE_MAX_LIMIT * 2}
+        response = self.client.get(self.url)
+        data = json.loads(response.content.decode('utf-8'))
+        self.assertEqual(len(data.get('data')), settings.SELECTABLE_MAX_LIMIT)
+
+    def test_request_less_than_max(self):
+        for i in range(settings.SELECTABLE_MAX_LIMIT):
+            self.create_thing(data={'name': 'Thing%s' % i})
+        new_limit = settings.SELECTABLE_MAX_LIMIT // 2
+        data = {'term': '', 'limit': new_limit}
+        response = self.client.get(self.url, data)
+        data = json.loads(response.content.decode('utf-8'))
+        self.assertEqual(len(data.get('data')), new_limit)
diff --git a/dep/django-selectable/selectable/tests/test_widgets.py b/dep/django-selectable/selectable/tests/test_widgets.py
new file mode 100644 (file)
index 0000000..58a7d3a
--- /dev/null
@@ -0,0 +1,477 @@
+import json
+
+from django import forms
+from django.utils.http import urlencode
+
+from ..compat import urlparse
+from ..forms import widgets
+from . import Thing, ThingLookup
+from .base import BaseSelectableTestCase, parsed_inputs
+
+
+__all__ = (
+    'AutoCompleteWidgetTestCase',
+    'AutoCompleteSelectWidgetTestCase',
+    'AutoComboboxWidgetTestCase',
+    'AutoComboboxSelectWidgetTestCase',
+    'AutoCompleteSelectMultipleWidgetTestCase',
+    'AutoComboboxSelectMultipleWidgetTestCase',
+)
+
+
+class WidgetTestMixin(object):
+    widget_cls = None
+    lookup_cls = None
+
+    def get_widget_instance(self, **kwargs):
+        return self.__class__.widget_cls(self.__class__.lookup_cls, **kwargs)
+
+    def test_init(self):
+        widget = self.get_widget_instance()
+        self.assertEqual(widget.lookup_class, self.__class__.lookup_cls)
+
+    def test_dotted_path(self):
+        """
+        Ensure lookup_class can be imported from a dotted path.
+        """
+        dotted_path = '.'.join([self.__class__.lookup_cls.__module__, self.__class__.lookup_cls.__name__])
+        widget = self.__class__.widget_cls(dotted_path)
+        self.assertEqual(widget.lookup_class, self.__class__.lookup_cls)
+
+    def test_invalid_dotted_path(self):
+        """
+        An invalid lookup_class dotted path should raise an ImportError.
+        """
+        with self.assertRaises(ImportError):
+            self.__class__.widget_cls('this.is.an.invalid.path')
+
+    def test_dotted_path_wrong_type(self):
+        """
+        lookup_class must be a subclass of LookupBase.
+        """
+        dotted_path = 'selectable.forms.widgets.AutoCompleteWidget'
+        with self.assertRaises(TypeError):
+            self.__class__.widget_cls(dotted_path)
+
+
+class AutoCompleteWidgetTestCase(BaseSelectableTestCase, WidgetTestMixin):
+    widget_cls = widgets.AutoCompleteWidget
+    lookup_cls = ThingLookup
+
+    def test_build_attrs(self):
+        widget = self.get_widget_instance()
+        attrs = widget.build_attrs()
+        self.assertTrue('data-selectable-url' in attrs)
+        self.assertTrue('data-selectable-type' in attrs)
+        self.assertTrue('data-selectable-allow-new' in attrs)
+
+    def test_update_query_parameters(self):
+        params = {'active': 1}
+        widget = self.get_widget_instance()
+        widget.update_query_parameters(params)
+        attrs = widget.build_attrs()
+        url = attrs['data-selectable-url']
+        parse = urlparse(url)
+        query = parse.query
+        self.assertEqual(query, urlencode(params))
+
+    def test_limit_paramter(self):
+        widget = self.get_widget_instance(limit=10)
+        attrs = widget.build_attrs()
+        url = attrs['data-selectable-url']
+        parse = urlparse(url)
+        query = parse.query
+        self.assertTrue('limit=10' in query)
+
+    def test_initial_query_parameters(self):
+        params = {'active': 1}
+        widget = self.get_widget_instance(query_params=params)
+        attrs = widget.build_attrs()
+        url = attrs['data-selectable-url']
+        parse = urlparse(url)
+        query = parse.query
+        self.assertEqual(query, urlencode(params))
+
+    def test_build_selectable_options(self):
+        "Serialize selectable options as json in data attribute."
+        options = {'autoFocus': True}
+        widget = self.get_widget_instance(attrs={'data-selectable-options': options})
+        attrs = widget.build_attrs()
+        self.assertTrue('data-selectable-options' in attrs)
+        self.assertEqual(attrs['data-selectable-options'], json.dumps(options))
+
+
+class AutoCompleteSelectWidgetTestCase(BaseSelectableTestCase, WidgetTestMixin):
+    widget_cls = widgets.AutoCompleteSelectWidget
+    lookup_cls = ThingLookup
+
+    def test_has_complete_widget(self):
+        widget = self.get_widget_instance()
+        self.assertEqual(widget.widgets[0].__class__, widgets.AutoCompleteWidget)
+
+    def test_has_hidden_widget(self):
+        widget = self.get_widget_instance()
+        self.assertEqual(widget.widgets[1].__class__, forms.HiddenInput)
+
+    def test_hidden_type(self):
+        widget = self.get_widget_instance()
+        sub_widget = widget.widgets[1]
+        attrs = sub_widget.build_attrs()
+        self.assertTrue('data-selectable-type' in attrs)
+        self.assertEqual(attrs['data-selectable-type'], 'hidden')
+
+    def test_update_query_parameters(self):
+        params = {'active': 1}
+        widget = self.get_widget_instance()
+        widget.update_query_parameters(params)
+        sub_widget = widget.widgets[0]
+        attrs = sub_widget.build_attrs()
+        url = attrs['data-selectable-url']
+        parse = urlparse(url)
+        query = parse.query
+        self.assertEqual(query, urlencode(params))
+
+    def test_limit_paramter(self):
+        widget = self.get_widget_instance(limit=10)
+        sub_widget = widget.widgets[0]
+        attrs = sub_widget.build_attrs()
+        url = attrs['data-selectable-url']
+        parse = urlparse(url)
+        query = parse.query
+        self.assertTrue('limit=10' in query)
+
+    def test_initial_query_parameters(self):
+        params = {'active': 1}
+        widget = self.get_widget_instance(query_params=params)
+        sub_widget = widget.widgets[0]
+        attrs = sub_widget.build_attrs()
+        url = attrs['data-selectable-url']
+        parse = urlparse(url)
+        query = parse.query
+        self.assertEqual(query, urlencode(params))
+
+    def test_build_selectable_options(self):
+        "Serialize selectable options as json in data attribute."
+        options = {'autoFocus': True}
+        widget = self.get_widget_instance(attrs={'data-selectable-options': options})
+        sub_widget = widget.widgets[0]
+        attrs = sub_widget.build_attrs()
+        self.assertTrue('data-selectable-options' in attrs)
+        self.assertEqual(attrs['data-selectable-options'], json.dumps(options))
+
+    def test_postdata_compatible_with_select(self):
+        "Checks postdata for values that a select widget would generate."
+        postdata = {'fruit': '1'}
+        widget = self.get_widget_instance()
+        widget_val = widget.value_from_datadict(postdata, [], 'fruit')
+        self.assertEquals(widget_val, '1')
+
+
+class AutoComboboxWidgetTestCase(BaseSelectableTestCase, WidgetTestMixin):
+    widget_cls = widgets.AutoComboboxWidget
+    lookup_cls = ThingLookup
+
+    def test_build_attrs(self):
+        widget = self.get_widget_instance()
+        attrs = widget.build_attrs()
+        self.assertTrue('data-selectable-url' in attrs)
+        self.assertTrue('data-selectable-type' in attrs)
+        self.assertTrue('data-selectable-allow-new' in attrs)
+
+    def test_update_query_parameters(self):
+        params = {'active': 1}
+        widget = self.get_widget_instance()
+        widget.update_query_parameters(params)
+        attrs = widget.build_attrs()
+        url = attrs['data-selectable-url']
+        parse = urlparse(url)
+        query = parse.query
+        self.assertEqual(query, urlencode(params))
+
+    def test_limit_paramter(self):
+        widget = self.get_widget_instance(limit=10)
+        attrs = widget.build_attrs()
+        url = attrs['data-selectable-url']
+        parse = urlparse(url)
+        query = parse.query
+        self.assertTrue('limit=10' in query)
+
+    def test_initial_query_parameters(self):
+        params = {'active': 1}
+        widget = self.get_widget_instance(query_params=params)
+        attrs = widget.build_attrs()
+        url = attrs['data-selectable-url']
+        parse = urlparse(url)
+        query = parse.query
+        self.assertEqual(query, urlencode(params))
+
+    def test_build_selectable_options(self):
+        "Serialize selectable options as json in data attribute."
+        options = {'autoFocus': True}
+        widget = self.get_widget_instance(attrs={'data-selectable-options': options})
+        attrs = widget.build_attrs()
+        self.assertTrue('data-selectable-options' in attrs)
+        self.assertEqual(attrs['data-selectable-options'], json.dumps(options))
+
+
+class AutoComboboxSelectWidgetTestCase(BaseSelectableTestCase, WidgetTestMixin):
+    widget_cls = widgets.AutoComboboxSelectWidget
+    lookup_cls = ThingLookup
+
+    def test_has_complete_widget(self):
+        widget = self.get_widget_instance()
+        self.assertEqual(widget.widgets[0].__class__, widgets.AutoComboboxWidget)
+
+    def test_has_hidden_widget(self):
+        widget = self.get_widget_instance()
+        self.assertEqual(widget.widgets[1].__class__, forms.HiddenInput)
+
+    def test_hidden_type(self):
+        widget = self.get_widget_instance()
+        sub_widget = widget.widgets[1]
+        attrs = sub_widget.build_attrs()
+        self.assertTrue('data-selectable-type' in attrs)
+        self.assertEqual(attrs['data-selectable-type'], 'hidden')
+
+    def test_update_query_parameters(self):
+        params = {'active': 1}
+        widget = self.get_widget_instance()
+        widget.update_query_parameters(params)
+        sub_widget = widget.widgets[0]
+        attrs = sub_widget.build_attrs()
+        url = attrs['data-selectable-url']
+        parse = urlparse(url)
+        query = parse.query
+        self.assertEqual(query, urlencode(params))
+
+    def test_limit_paramter(self):
+        widget = self.get_widget_instance(limit=10)
+        sub_widget = widget.widgets[0]
+        attrs = sub_widget.build_attrs()
+        url = attrs['data-selectable-url']
+        parse = urlparse(url)
+        query = parse.query
+        self.assertTrue('limit=10' in query)
+
+    def test_initial_query_parameters(self):
+        params = {'active': 1}
+        widget = self.get_widget_instance(query_params=params)
+        sub_widget = widget.widgets[0]
+        attrs = sub_widget.build_attrs()
+        url = attrs['data-selectable-url']
+        parse = urlparse(url)
+        query = parse.query
+        self.assertEqual(query, urlencode(params))
+
+    def test_build_selectable_options(self):
+        "Serialize selectable options as json in data attribute."
+        options = {'autoFocus': True}
+        widget = self.get_widget_instance(attrs={'data-selectable-options': options})
+        sub_widget = widget.widgets[0]
+        attrs = sub_widget.build_attrs()
+        self.assertTrue('data-selectable-options' in attrs)
+        self.assertEqual(attrs['data-selectable-options'], json.dumps(options))
+
+
+class AutoCompleteSelectMultipleWidgetTestCase(BaseSelectableTestCase, WidgetTestMixin):
+    widget_cls = widgets.AutoCompleteSelectMultipleWidget
+    lookup_cls = ThingLookup
+
+    def test_has_complete_widget(self):
+        widget = self.get_widget_instance()
+        self.assertEqual(widget.widgets[0].__class__, widgets.AutoCompleteWidget)
+
+    def test_multiple_attr(self):
+        widget = self.get_widget_instance()
+        sub_widget = widget.widgets[0]
+        attrs = sub_widget.build_attrs()
+        self.assertTrue('data-selectable-multiple' in attrs)
+        self.assertEqual(attrs['data-selectable-multiple'], 'true')
+
+    def test_has_hidden_widget(self):
+        widget = self.get_widget_instance()
+        self.assertEqual(widget.widgets[1].__class__, widgets.LookupMultipleHiddenInput)
+
+    def test_hidden_type(self):
+        widget = self.get_widget_instance()
+        sub_widget = widget.widgets[1]
+        attrs = sub_widget.build_attrs()
+        self.assertTrue('data-selectable-type' in attrs)
+        self.assertEqual(attrs['data-selectable-type'], 'hidden-multiple')
+
+    def test_render_single(self):
+        widget = self.get_widget_instance()
+        val = 4
+        rendered_value = widget.render('field_name', val)
+        inputs = parsed_inputs(rendered_value)
+        field = inputs['field_name_1'][0]
+        self.assertEqual(field.attributes['data-selectable-type'].value, 'hidden-multiple')
+        self.assertEqual(field.attributes['type'].value, 'hidden')
+        self.assertEqual(int(field.attributes['value'].value), val)
+
+    def test_render_list(self):
+        widget = self.get_widget_instance()
+        list_val = [8, 5]
+        rendered_value = widget.render('field_name', list_val)
+        inputs = parsed_inputs(rendered_value)
+        found_values = []
+        for field in inputs['field_name_1']:
+            self.assertEqual(field.attributes['data-selectable-type'].value, 'hidden-multiple')
+            self.assertEqual(field.attributes['type'].value, 'hidden')
+            found_values.append(int(field.attributes['value'].value))
+        self.assertListEqual(found_values, list_val)
+
+    def test_render_qs(self):
+        widget = self.get_widget_instance()
+        t1 = self.create_thing()
+        t2 = self.create_thing()
+        qs_val = Thing.objects.filter(pk__in=[t1.pk, t2.pk]).values_list('pk', flat=True)
+        rendered_value = widget.render('field_name', qs_val)
+        inputs = parsed_inputs(rendered_value)
+        found_values = []
+        for field in inputs['field_name_1']:
+            self.assertEqual(field.attributes['data-selectable-type'].value, 'hidden-multiple')
+            self.assertEqual(field.attributes['type'].value, 'hidden')
+            found_values.append(int(field.attributes['value'].value))
+        self.assertListEqual(found_values, [t1.pk, t2.pk])
+
+    def test_update_query_parameters(self):
+        params = {'active': 1}
+        widget = self.get_widget_instance()
+        widget.update_query_parameters(params)
+        sub_widget = widget.widgets[0]
+        attrs = sub_widget.build_attrs()
+        url = attrs['data-selectable-url']
+        parse = urlparse(url)
+        query = parse.query
+        self.assertEqual(query, urlencode(params))
+
+    def test_limit_paramter(self):
+        widget = self.get_widget_instance(limit=10)
+        sub_widget = widget.widgets[0]
+        attrs = sub_widget.build_attrs()
+        url = attrs['data-selectable-url']
+        parse = urlparse(url)
+        query = parse.query
+        self.assertTrue('limit=10' in query)
+
+    def test_initial_query_parameters(self):
+        params = {'active': 1}
+        widget = self.get_widget_instance(query_params=params)
+        sub_widget = widget.widgets[0]
+        attrs = sub_widget.build_attrs()
+        url = attrs['data-selectable-url']
+        parse = urlparse(url)
+        query = parse.query
+        self.assertEqual(query, urlencode(params))
+
+    def test_build_selectable_options(self):
+        "Serialize selectable options as json in data attribute."
+        options = {'autoFocus': True}
+        widget = self.get_widget_instance(attrs={'data-selectable-options': options})
+        sub_widget = widget.widgets[0]
+        attrs = sub_widget.build_attrs()
+        self.assertTrue('data-selectable-options' in attrs)
+        self.assertEqual(attrs['data-selectable-options'], json.dumps(options))
+
+
+class AutoComboboxSelectMultipleWidgetTestCase(BaseSelectableTestCase, WidgetTestMixin):
+    widget_cls = widgets.AutoComboboxSelectMultipleWidget
+    lookup_cls = ThingLookup
+
+    def test_has_complete_widget(self):
+        widget = self.get_widget_instance()
+        self.assertEqual(widget.widgets[0].__class__, widgets.AutoComboboxWidget)
+
+    def test_multiple_attr(self):
+        widget = self.get_widget_instance()
+        sub_widget = widget.widgets[0]
+        attrs = sub_widget.build_attrs()
+        self.assertTrue('data-selectable-multiple' in attrs)
+        self.assertEqual(attrs['data-selectable-multiple'], 'true')
+
+    def test_has_hidden_widget(self):
+        widget = self.get_widget_instance()
+        self.assertEqual(widget.widgets[1].__class__, widgets.LookupMultipleHiddenInput)
+
+    def test_hidden_type(self):
+        widget = self.get_widget_instance()
+        sub_widget = widget.widgets[1]
+        attrs = sub_widget.build_attrs()
+        self.assertTrue('data-selectable-type' in attrs)
+        self.assertEqual(attrs['data-selectable-type'], 'hidden-multiple')
+
+    def test_render_single(self):
+        widget = self.get_widget_instance()
+        val = 4
+        rendered_value = widget.render('field_name', val)
+        inputs = parsed_inputs(rendered_value)
+        field = inputs['field_name_1'][0]
+        self.assertEqual(field.attributes['data-selectable-type'].value, 'hidden-multiple')
+        self.assertEqual(field.attributes['type'].value, 'hidden')
+        self.assertEqual(field.attributes['value'].value, str(val))
+
+    def test_render_list(self):
+        widget = self.get_widget_instance()
+        list_val = [8, 5]
+        rendered_value = widget.render('field_name', list_val)
+        inputs = parsed_inputs(rendered_value)
+        found_values = []
+        for field in inputs['field_name_1']:
+            self.assertEqual(field.attributes['data-selectable-type'].value, 'hidden-multiple')
+            self.assertEqual(field.attributes['type'].value, 'hidden')
+            found_values.append(int(field.attributes['value'].value))
+        self.assertListEqual(found_values, list_val)
+
+    def test_render_qs(self):
+        widget = self.get_widget_instance()
+        t1 = self.create_thing()
+        t2 = self.create_thing()
+        qs_val = Thing.objects.filter(pk__in=[t1.pk, t2.pk]).values_list('pk', flat=True)
+        rendered_value = widget.render('field_name', qs_val)
+        inputs = parsed_inputs(rendered_value)
+        found_values = []
+        for field in inputs['field_name_1']:
+            self.assertEqual(field.attributes['data-selectable-type'].value, 'hidden-multiple')
+            self.assertEqual(field.attributes['type'].value, 'hidden')
+            found_values.append(int(field.attributes['value'].value))
+        self.assertListEqual(found_values, [t1.pk, t2.pk])
+
+    def test_update_query_parameters(self):
+        params = {'active': 1}
+        widget = self.get_widget_instance()
+        widget.update_query_parameters(params)
+        sub_widget = widget.widgets[0]
+        attrs = sub_widget.build_attrs()
+        url = attrs['data-selectable-url']
+        parse = urlparse(url)
+        query = parse.query
+        self.assertEqual(query, urlencode(params))
+
+    def test_limit_paramter(self):
+        widget = self.get_widget_instance(limit=10)
+        sub_widget = widget.widgets[0]
+        attrs = sub_widget.build_attrs()
+        url = attrs['data-selectable-url']
+        parse = urlparse(url)
+        query = parse.query
+        self.assertTrue('limit=10' in query)
+
+    def test_initial_query_parameters(self):
+        params = {'active': 1}
+        widget = self.get_widget_instance(query_params=params)
+        sub_widget = widget.widgets[0]
+        attrs = sub_widget.build_attrs()
+        url = attrs['data-selectable-url']
+        parse = urlparse(url)
+        query = parse.query
+        self.assertEqual(query, urlencode(params))
+
+    def test_build_selectable_options(self):
+        "Serialize selectable options as json in data attribute."
+        options = {'autoFocus': True}
+        widget = self.get_widget_instance(attrs={'data-selectable-options': options})
+        sub_widget = widget.widgets[0]
+        attrs = sub_widget.build_attrs()
+        self.assertTrue('data-selectable-options' in attrs)
+        self.assertEqual(attrs['data-selectable-options'], json.dumps(options))
index ce1a383f1121cf6ca9399b782d774bd9da6c344c..ed7cf46293909c662af037fff20930ca6a2552d4 100644 (file)
@@ -1,12 +1,9 @@
-try:
-    from django.conf.urls import handler404, handler500, patterns, include
-except ImportError:
-    # Django < 1.4
-    from django.conf.urls.defaults import handler404, handler500, patterns, include
+from django.conf.urls import handler404, handler500, include, url
+
 
 handler404 = 'selectable.tests.views.test_404'
 handler500 = 'selectable.tests.views.test_500'
 
-urlpatterns = patterns('',
-    (r'^selectable-tests/', include('selectable.urls')),
-)
+urlpatterns = [
+    url(r'^selectable-tests/', include('selectable.urls')),
+]
index b83651f07842835a26f17b06f475d5870046c09b..f747967f1e57469dda9646f7724de30cd7b34d4d 100644 (file)
@@ -1,100 +1,8 @@
-from __future__ import division
-
-import json
-
-from django.conf import settings
-from django.core.urlresolvers import reverse
 from django.http import HttpResponseNotFound, HttpResponseServerError
 
-from selectable.tests import ThingLookup
-from selectable.tests.base import BaseSelectableTestCase, PatchSettingsMixin
-
-
-__all__ = (
-    'SelectableViewTest',
-)
-
-
 def test_404(request):
     return HttpResponseNotFound()
 
 
 def test_500(request):
     return HttpResponseServerError()
-
-
-class SelectableViewTest(PatchSettingsMixin, BaseSelectableTestCase):
-
-    def setUp(self):
-        super(SelectableViewTest, self).setUp()
-        self.url = ThingLookup.url()
-        self.lookup = ThingLookup()
-        self.thing = self.create_thing()
-        self.other_thing = self.create_thing()
-
-    def test_response_type(self):
-        response = self.client.get(self.url)
-        self.assertEqual(response['Content-Type'], 'application/json')
-
-    def test_response_keys(self):
-        response = self.client.get(self.url)
-        data = json.loads(response.content.decode('utf-8'))
-        for result in data.get('data'):
-            self.assertTrue('id' in result)
-            self.assertTrue('value' in result)
-            self.assertTrue('label' in result)
-
-    def test_no_term_lookup(self):
-        data = {}
-        response = self.client.get(self.url, data)
-        data = json.loads(response.content.decode('utf-8'))
-        self.assertEqual(len(data), 2)
-
-    def test_simple_term_lookup(self):
-        data = {'term': self.thing.name}
-        response = self.client.get(self.url, data)
-        data = json.loads(response.content.decode('utf-8'))
-        self.assertEqual(len(data), 2)
-        self.assertEqual(len(data.get('data')), 1)
-
-    def test_unknown_lookup(self):
-        unknown_url = reverse('selectable-lookup', args=["XXXXXXX"])
-        response = self.client.get(unknown_url)
-        self.assertEqual(response.status_code, 404)
-
-    def test_basic_limit(self):
-        for i in range(settings.SELECTABLE_MAX_LIMIT):
-            self.create_thing(data={'name': 'Thing%s' % i})
-        response = self.client.get(self.url)
-        data = json.loads(response.content.decode('utf-8'))
-        self.assertEqual(len(data.get('data')), settings.SELECTABLE_MAX_LIMIT)
-        meta = data.get('meta')
-        self.assertTrue('next_page' in meta)
-
-    def test_get_next_page(self):
-        for i in range(settings.SELECTABLE_MAX_LIMIT * 2):
-            self.create_thing(data={'name': 'Thing%s' % i})
-        data = {'term': 'Thing', 'page': 2}
-        response = self.client.get(self.url, data)
-        data = json.loads(response.content.decode('utf-8'))
-        self.assertEqual(len(data.get('data')), settings.SELECTABLE_MAX_LIMIT)
-        # No next page
-        meta = data.get('meta')
-        self.assertFalse('next_page' in meta)
-
-    def test_request_more_than_max(self):
-        for i in range(settings.SELECTABLE_MAX_LIMIT):
-            self.create_thing(data={'name': 'Thing%s' % i})
-        data = {'term': '', 'limit': settings.SELECTABLE_MAX_LIMIT * 2}
-        response = self.client.get(self.url)
-        data = json.loads(response.content.decode('utf-8'))
-        self.assertEqual(len(data.get('data')), settings.SELECTABLE_MAX_LIMIT)
-
-    def test_request_less_than_max(self):
-        for i in range(settings.SELECTABLE_MAX_LIMIT):
-            self.create_thing(data={'name': 'Thing%s' % i})
-        new_limit = settings.SELECTABLE_MAX_LIMIT // 2
-        data = {'term': '', 'limit': new_limit}
-        response = self.client.get(self.url, data)
-        data = json.loads(response.content.decode('utf-8'))
-        self.assertEqual(len(data.get('data')), new_limit)
index ec6a73db5d5830228d79a87e148f046499850fc3..02444b997c0feddd33a8ead993e87dfd6bd00cb5 100644 (file)
@@ -1,14 +1,15 @@
-try:
-    from django.conf.urls import handler404, handler500, patterns, url
-except ImportError:
-    # Django < 1.4
-    from django.conf.urls.defaults import handler404, handler500, patterns, url
+from django.conf.urls import url
 
-from selectable import registry
+from . import views
+from .compat import LEGACY_AUTO_DISCOVER 
 
+if LEGACY_AUTO_DISCOVER:
+    # Auto-discovery is now handled by the app configuration
+    from . import registry
 
-registry.autodiscover()
+    registry.autodiscover()
 
-urlpatterns = patterns('selectable.views',
-    url(r'^(?P<lookup_name>[-\w]+)/$', 'get_lookup', name="selectable-lookup"),
-)
+
+urlpatterns = [
+    url(r'^(?P<lookup_name>[-\w]+)/$', views.get_lookup, name="selectable-lookup"),
+]
diff --git a/dep/django-selectable/setup.cfg b/dep/django-selectable/setup.cfg
new file mode 100644 (file)
index 0000000..6f08d0e
--- /dev/null
@@ -0,0 +1,8 @@
+[bdist_wheel]
+universal = 1
+
+[egg_info]
+tag_build = 
+tag_date = 0
+tag_svn_revision = 0
+
index db7a6d55d15eb86f332269f259b7ab1df2562126..77ed45758d6d36c4844073f25c3d93f8fc81bffe 100644 (file)
@@ -19,7 +19,7 @@ setup(
     author_email='markdlavin@gmail.com',
     packages=find_packages(exclude=['example']),
     include_package_data=True,
-    url='http://bitbucket.org/mlavin/django-selectable',
+    url='https://github.com/mlavin/django-selectable',
     license='BSD',
     description=' '.join(__import__('selectable').__doc__.splitlines()).strip(),
     classifiers=[
@@ -32,6 +32,7 @@ setup(
         'Programming Language :: Python :: 3',
         'Programming Language :: Python :: 3.2',
         'Programming Language :: Python :: 3.3',
+        'Programming Language :: Python :: 3.4',
         'Framework :: Django',
         'Development Status :: 5 - Production/Stable',
         'Operating System :: OS Independent',