Upgrade django-selectable to v1.1.0
authorMagnus Hagander <[email protected]>
Wed, 21 Feb 2018 21:05:18 +0000 (22:05 +0100)
committerMagnus Hagander <[email protected]>
Wed, 21 Feb 2018 21:05:18 +0000 (22:05 +0100)
Needed for newer django

42 files changed:
dep/django-selectable/AUTHORS.txt
dep/django-selectable/LICENSE.txt
dep/django-selectable/Makefile [new file with mode: 0644]
dep/django-selectable/README.rst
dep/django-selectable/docs/admin.rst
dep/django-selectable/docs/advanced.rst
dep/django-selectable/docs/conf.py
dep/django-selectable/docs/contribute.rst
dep/django-selectable/docs/fields.rst
dep/django-selectable/docs/lookups.rst
dep/django-selectable/docs/quick-start.rst
dep/django-selectable/docs/releases.rst
dep/django-selectable/docs/settings.rst
dep/django-selectable/docs/testing.rst
dep/django-selectable/docs/widgets.rst
dep/django-selectable/runtests.py
dep/django-selectable/selectable/__init__.py
dep/django-selectable/selectable/apps.py
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/registry.py
dep/django-selectable/selectable/static/selectable/js/jquery.dj.selectable.js
dep/django-selectable/selectable/templatetags/selectable_tags.py
dep/django-selectable/selectable/tests/__init__.py
dep/django-selectable/selectable/tests/base.py
dep/django-selectable/selectable/tests/qunit/jquery-loader.js
dep/django-selectable/selectable/tests/qunit/test-methods.js
dep/django-selectable/selectable/tests/test_fields.py
dep/django-selectable/selectable/tests/test_forms.py
dep/django-selectable/selectable/tests/test_functional.py
dep/django-selectable/selectable/tests/test_templatetags.py
dep/django-selectable/selectable/tests/test_views.py
dep/django-selectable/selectable/tests/test_widgets.py
dep/django-selectable/selectable/tests/views.py
dep/django-selectable/selectable/urls.py
dep/django-selectable/selectable/views.py
dep/django-selectable/setup.cfg
dep/django-selectable/setup.py
dep/django-selectable/tox.ini [new file with mode: 0644]

index 1c0422f2ab849d407318c41488e01e1b3a1792ea..965073b01e25521463d1ed65df674b1deb92e271 100644 (file)
@@ -23,5 +23,10 @@ Rebecca Lovewell
 Thomas Güttler
 Yuri Khrustalev
 @SaeX
+Tam Huynh
+Raphael Merx
+Josh Addington
+Tobias Zanke
+Petr Dlouhy
 
 Thanks for all of your work!
index c50fbf06b0b6176c9d15006039ea4ecd90d553d8..b06f6132e1b23491b096cb8515f34030d8fe04a5 100644 (file)
@@ -1,4 +1,4 @@
-Copyright (c) 2010-2014, Mark Lavin
+Copyright (c) 2010-2018, Mark Lavin
 All rights reserved.
 
 Redistribution and use in source and binary forms, with or without modification,
diff --git a/dep/django-selectable/Makefile b/dep/django-selectable/Makefile
new file mode 100644 (file)
index 0000000..d82053a
--- /dev/null
@@ -0,0 +1,21 @@
+STATIC_DIR = ./selectable/static/selectable
+QUNIT_TESTS = file://`pwd`/selectable/tests/qunit/index.html
+
+test-js:
+       # Run JS tests
+       # Requires phantomjs
+       phantomjs run-qunit.js ${QUNIT_TESTS}?jquery=1.11.2&ui=1.11.4
+       phantomjs run-qunit.js ${QUNIT_TESTS}?jquery=1.11.2&ui=1.10.4
+       phantomjs run-qunit.js ${QUNIT_TESTS}?jquery=1.10.2&ui=1.11.4
+       phantomjs run-qunit.js ${QUNIT_TESTS}?jquery=1.10.2&ui=1.10.4
+       phantomjs run-qunit.js ${QUNIT_TESTS}?jquery=1.9.1&ui=1.11.4
+       phantomjs run-qunit.js ${QUNIT_TESTS}?jquery=1.9.1&ui=1.10.4
+
+
+lint-js:
+       # Check JS for any problems     
+       # Requires jshint
+       jshint ${STATIC_DIR}/js/jquery.dj.selectable.js
+
+
+.PHONY: lint-js test-js
index 0219c2cb6158c76f0262e611b9d72d24c57a08b8..86f8aa086dde154ec640b5829c166454fc89a168 100644 (file)
@@ -6,6 +6,22 @@ Tools and widgets for using/creating auto-complete selection widgets using Djang
 .. image:: https://travis-ci.org/mlavin/django-selectable.svg?branch=master
     :target: https://travis-ci.org/mlavin/django-selectable
 
+.. image:: https://codecov.io/github/mlavin/django-selectable/coverage.svg?branch=master
+    :target: https://codecov.io/github/mlavin/django-selectable?branch=master
+
+
+.. note::
+
+    This project is looking for additional maintainers to help with Django/jQuery compatibility
+    issues as well as addressing support issues/questions. If you are looking to help out
+    on this project and take a look at the open
+    `help-wanted <https://github.com/mlavin/django-selectable/issues?q=is%3Aissue+is%3Aopen+label%3Ahelp-wanted>`_
+    or `question <https://github.com/mlavin/django-selectable/issues?q=is%3Aissue+is%3Aopen+label%3Aquestion>`_
+    and see if you can contribute a fix. Be bold! If you want to take a larger role on
+    the project, please reach out on the
+    `mailing list <http://groups.google.com/group/django-selectable>`_. I'm happy to work
+    with you to get you going on an issue.
+
 
 Features
 -----------------------------------
@@ -17,10 +33,10 @@ Features
 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
+- Python 2.7, 3.3+
+- `Django <http://www.djangoproject.com/>`_ >= 1.7, <= 1.11
+- `jQuery <http://jquery.com/>`_ >= 1.9, < 3.0
+- `jQuery UI <http://jqueryui.com/>`_ >= 1.10, < 1.12
 
 To install::
 
@@ -40,16 +56,16 @@ Google CDN.
 
 Once installed you should add the urls to your root url patterns::
 
-    urlpatterns = patterns('',
+    urlpatterns = [
         # Other patterns go here
-        (r'^selectable/', include('selectable.urls')),
-    )
+        url(r'^selectable/', include('selectable.urls')),
+    ]
 
 
 Documentation
 -----------------------------------
 
-Documentation for django-selectable is available on `Read The Docs <http://readthedocs.org/docs/django-selectable>`_.
+Documentation for django-selectable is available on `Read The Docs <http://django-selectable.readthedocs.io/en/latest/>`_.
 
 
 Additional Help/Support
@@ -66,4 +82,3 @@ check out our `contributing guide <http://readthedocs.org/docs/django-selectable
 
 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/>`_.
-
index 07cba698541f97d10e9ae777ec7af8537096e0e2..e0c02fa3d741db81c2bda9d4f696e599a2a3f4ea 100644 (file)
@@ -6,9 +6,9 @@ Overview
 
 Django-Selectables will work in the admin. To get started on integrated the
 fields and widgets in the admin make sure you are familiar with the Django
-documentation on the `ModelAdmin.form <http://docs.djangoproject.com/en/1.3/ref/contrib/admin/#django.contrib.admin.ModelAdmin.form>`_
-and `ModelForms <http://docs.djangoproject.com/en/1.3/topics/forms/modelforms/>`_ particularly
-on `overriding the default widgets <http://docs.djangoproject.com/en/1.3/topics/forms/modelforms/#overriding-the-default-field-types-or-widgets>`_.
+documentation on the `ModelAdmin.form <http://docs.djangoproject.com/en/stable/ref/contrib/admin/#django.contrib.admin.ModelAdmin.form>`_
+and `ModelForms <http://docs.djangoproject.com/en/stable/topics/forms/modelforms/>`_ particularly
+on `overriding the default widgets <http://docs.djangoproject.com/en/stable/topics/forms/modelforms/#overriding-the-default-field-types-or-widgets>`_.
 As you will see integrating django-selectable in the adminis the same as working with regular forms.
 
 
@@ -24,12 +24,17 @@ Django admin that means overriding
 You can include this media in the block name `extrahead` which is defined in
 `admin/base.html <https://code.djangoproject.com/browser/django/trunk/django/contrib/admin/templates/admin/base.html>`_.
 
-    .. literalinclude:: ../example/example/templates/admin/base_site.html
-        :start-after: {% endblock title %}
-        :end-before: {% block branding %}
+    .. code-block:: html
+
+        {% block extrahead %}
+            {% load selectable_tags %}
+            {% include_ui_theme %}
+            {% include_jquery_libs %}
+            {{ block.super }}
+        {% endblock %}
 
 See the Django documentation on
-`overriding admin templates <https://docs.djangoproject.com/en/1.3/ref/contrib/admin/#overriding-admin-templates>`_.
+`overriding admin templates <https://docs.djangoproject.com/en/stable/ref/contrib/admin/#overriding-admin-templates>`_.
 See the example project for the full template example.
 
 
@@ -38,8 +43,6 @@ See the example project for the full template example.
 Using Grappelli
 --------------------------------------
 
-.. versionadded:: 0.7
-
 `Grappelli <https://django-grappelli.readthedocs.org>`_ is a popular customization of the Django
 admin interface. It includes a number of interface improvements which are also built on top of
 jQuery UI. When using Grappelli you do not need to make any changes to the ``admin/base_site.html``
@@ -52,19 +55,78 @@ and make use of them.
 Basic Example
 --------------------------------------
 
-In our sample project we have a ``Farm`` model with a foreign key to ``auth.User`` and
+For example, we may have a ``Farm`` model with a foreign key to ``auth.User`` and
 a many to many relation to our ``Fruit`` model.
 
-    .. literalinclude:: ../example/core/models.py
-       :pyobject: Farm
+    .. code-block:: python
+
+        from __future__ import unicode_literals
+
+        from django.db import models
+        from django.utils.encoding import python_2_unicode_compatible
+
+
+        @python_2_unicode_compatible
+        class Fruit(models.Model):
+            name = models.CharField(max_length=200)
+
+            def __str__(self):
+                return self.name
+
+
+        @python_2_unicode_compatible
+        class Farm(models.Model):
+            name = models.CharField(max_length=200)
+            owner = models.ForeignKey('auth.User', related_name='farms')
+            fruit = models.ManyToManyField(Fruit)
+
+            def __str__(self):
+                return "%s's Farm: %s" % (self.owner.username, self.name)
 
 In `admin.py` we will define the form and associate it with the `FarmAdmin`.
 
-    .. literalinclude:: ../example/core/admin.py
-        :pyobject: FarmAdminForm
+    .. code-block:: python
+
+        from django.contrib import admin
+        from django.contrib.auth.admin import UserAdmin
+        from django.contrib.auth.models import User
+        from django import forms
+
+        from selectable.forms import AutoCompleteSelectField, AutoCompleteSelectMultipleWidget
+
+        from .models import Fruit, Farm
+        from .lookups import FruitLookup, OwnerLookup
+
+
+        class FarmAdminForm(forms.ModelForm):
+            owner = AutoCompleteSelectField(lookup_class=OwnerLookup, allow_new=True)
+
+            class Meta(object):
+                model = Farm
+                widgets = {
+                    'fruit': AutoCompleteSelectMultipleWidget(lookup_class=FruitLookup),
+                }
+                exclude = ('owner', )
+
+            def __init__(self, *args, **kwargs):
+                super(FarmAdminForm, self).__init__(*args, **kwargs)
+                if self.instance and self.instance.pk and self.instance.owner:
+                    self.initial['owner'] = self.instance.owner.pk
+
+            def save(self, *args, **kwargs):
+                owner = self.cleaned_data['owner']
+                if owner and not owner.pk:
+                    owner = User.objects.create_user(username=owner.username, email='')
+                self.instance.owner = owner
+                return super(FarmAdminForm, self).save(*args, **kwargs)
+
+
+        class FarmAdmin(admin.ModelAdmin):
+            form = FarmAdminForm
+
+
+        admin.site.register(Farm, FarmAdmin)
 
-    .. literalinclude:: ../example/core/admin.py
-        :pyobject: FarmAdmin
 
 You'll note this form also allows new users to be created and associated with the
 farm, if no user is found matching the given name. To make use of this feature we
@@ -76,7 +138,7 @@ items you'll see these steps are not necessary.
 The django-selectable widgets are compatitible with the add another popup in the
 admin. It's that little green plus sign that appears next to ``ForeignKey`` or
 ``ManyToManyField`` items. This makes django-selectable a user friendly replacement
-for the `ModelAdmin.raw_id_fields <https://docs.djangoproject.com/en/1.3/ref/contrib/admin/#django.contrib.admin.ModelAdmin.raw_id_fields>`_
+for the `ModelAdmin.raw_id_fields <https://docs.djangoproject.com/en/stable/ref/contrib/admin/#django.contrib.admin.ModelAdmin.raw_id_fields>`_
 when the default select box grows too long.
 
 
@@ -87,13 +149,26 @@ Inline Example
 
 With our ``Farm`` model we can also associate the ``UserAdmin`` with a ``Farm``
 by making use of the `InlineModelAdmin
-<http://docs.djangoproject.com/en/1.3/ref/contrib/admin/#inlinemodeladmin-objects>`_.
+<http://docs.djangoproject.com/en/stable/ref/contrib/admin/#inlinemodeladmin-objects>`_.
 We can even make use of the same ``FarmAdminForm``.
 
-    .. literalinclude:: ../example/core/admin.py
-        :pyobject: FarmInline
-    .. literalinclude:: ../example/core/admin.py
-        :pyobject: NewUserAdmin
+    .. code-block:: python
+
+        # continued from above
+
+        class FarmInline(admin.TabularInline):
+            model = Farm
+            form = FarmAdminForm
+
+
+        class NewUserAdmin(UserAdmin):
+            inlines = [
+                FarmInline,
+            ]
+
+
+        admin.site.unregister(User)
+        admin.site.register(User, NewUserAdmin)
 
 The auto-complete functions will be bound as new forms are added dynamically.
 
index bbaa3df4e0562beec05dc1c4e7d4e16c323629e9..d3ffe2b5a5a30b6999a19ba1d34702b40cc8b0bf 100644 (file)
@@ -2,7 +2,7 @@ Advanced Usage
 ==========================
 
 We've gone through the most command and simple use cases for django-selectable. Now
-we'll take a lot at some of the more advanced features of this project. This assumes
+we'll take a look at some of the more advanced features of this project. This assumes
 that you are comfortable reading and writing a little bit of Javascript making
 use of jQuery.
 
@@ -16,7 +16,7 @@ The basic lookup is based on handling a search based on a single term string.
 If additional filtering is needed it can be inside the lookup ``get_query`` but
 you would need to define this when the lookup is defined. While this fits a fair
 number of use cases there are times when you need to define additional query
-parameters that won't be know until the either the form is bound or until selections
+parameters that won't be known until either the form is bound or until selections
 are made on the client side. This section will detail how to handle both of these
 cases.
 
@@ -24,8 +24,8 @@ cases.
 How Parameters are Passed
 _______________________________________
 
-As with the search term the additional parameters you define will be passed in
-``request.GET``. Since ``get_query`` gets the current request so you will have access to
+As with the search term, the additional parameters you define will be passed in
+``request.GET``. Since ``get_query`` gets the current request, you will have access to
 them. Since they can be manipulated on the client side, these parameters should be
 treated like all user input. It should be properly validated and sanitized.
 
@@ -63,7 +63,7 @@ most common way to use this would be in the form ``__init__``.
 
 You can also pass the query parameters into the widget using the ``query_params``
 keyword argument. It depends on your use case as to whether the parameters are
-know when the form is defined or when an instance of the form is created.
+known when the form is defined or when an instance of the form is created.
 
 
 .. _client-side-parameters:
@@ -109,28 +109,90 @@ to write a little javascript.
 
 Suppose we have city model
 
-    .. literalinclude:: ../example/core/models.py
-        :pyobject: City
+    .. code-block:: python
+
+        from __future__ import unicode_literals
+
+        from django.db import models
+        from django.utils.encoding import python_2_unicode_compatible
+
+        from localflavor.us.models import USStateField
+
+
+        @python_2_unicode_compatible
+        class City(models.Model):
+            name = models.CharField(max_length=200)
+            state = USStateField()
+
+            def __str__(self):
+                return self.name
+
+Then in our lookup we will grab the state value and filter our results on it:
+
+    .. code-block:: python
+
+        from __future__ import unicode_literals
+
+        from selectable.base import ModelLookup
+        from selectable.registry import registry
+
+        from .models import City
+
+
+        class CityLookup(ModelLookup):
+            model = City
+            search_fields = ('name__icontains', )
+
+            def get_query(self, request, term):
+                results = super(CityLookup, self).get_query(request, term)
+                state = request.GET.get('state', '')
+                if state:
+                    results = results.filter(state=state)
+                return results
+
+            def get_item_label(self, item):
+                return "%s, %s" % (item.name, item.state)
+
+
+        registry.register(CityLookup)
 
 and a simple form
 
-    .. literalinclude:: ../example/core/forms.py
-        :pyobject: ChainedForm
+    .. code-block:: python
+
+        from django import forms
+
+        from localflavor.us.forms import USStateField, USStateSelect
+
+        from selectable.forms import AutoCompleteSelectField, AutoComboboxSelectWidget
+
+        from .lookups import CityLookup
+
+
+        class ChainedForm(forms.Form):
+            city = AutoCompleteSelectField(
+                lookup_class=CityLookup,
+                label='City',
+                required=False,
+                widget=AutoComboboxSelectWidget
+            )
+            state = USStateField(widget=USStateSelect, required=False)
+
 
 We want our users to select a city and if they choose a state then we will only
 show them cities in that state. To do this we will pass back chosen state as
 addition parameter with the following javascript:
 
-    .. literalinclude:: ../example/core/templates/advanced.html
-        :language: html
-        :start-after: {% block extra-js %}
-        :end-before: {% endblock %}
-
-
-Then in our lookup we will grab the state value and filter our results on it:
+    .. code-block:: html
 
-    .. literalinclude:: ../example/core/lookups.py
-        :pyobject: CityLookup
+        <script type="text/javascript">
+            $(document).ready(function() {
+                function newParameters(query) {
+                    query.state = $('#id_state').val();
+                }
+                $('#id_city_0').djselectable('option', 'prepareQuery', newParameters);
+            });
+        </script>
 
 And that's it! We now have a working chained selection example. The full source
 is included in the example project.
@@ -153,12 +215,6 @@ expose the events defined by the plugin.
     - djselectableclose
     - djselectablechange
 
-.. note::
-
-    Prior to v0.7 these event names were under the ``autocomplete`` namespace. If you
-    are upgrading from a previous version and had customizations using these events
-    you should be sure to update the names.
-
 For the most part these event names should be self-explanatory. If you need additional
 detail you should refer to the `jQuery UI docs on these events <http://jqueryui.com/demos/autocomplete/#events>`_.
 
@@ -230,7 +286,7 @@ then you can make django-selectable work by passing ``bindSelectables`` to the
         </script>
 
 Currently you must include the django-selectable javascript below this formset initialization
-code for this to work. See django-selectable `issue #31 <https://bitbucket.org/mlavin/django-selectable/issue/31/>`_
+code for this to work. See django-selectable `issue #31 <https://github.com/mlavin/django-selectable/issues/31>`_
 for some additional detail on this problem.
 
 
@@ -250,18 +306,9 @@ The item is a dictionary object matching what is returned by the lookup's
 :ref:`format_item <lookup-format-item>`. ``formatLabel`` should return the string
 which should be used for the label.
 
-.. note::
-
-    In v0.7 the scope of ``formatLabel`` was updated so that ``this`` refers to the
-    current ``djselectable`` plugin instance. Previously ``this`` refered to the
-    plugin ``options`` instance.
-
 Going back to the ``CityLookup`` we can adjust the label to wrap the city and state
 portions with their own classes for additional styling:
 
-    .. literalinclude:: ../example/core/lookups.py
-        :pyobject: CityLookup
-
     .. code-block:: html
 
         <script type="text/javascript">
@@ -331,4 +378,4 @@ create the buttons. An example is given below.
                 // Return button link with the chosen icon
                 return $("<a>").append(icon).addClass("btn btn-small pull-right");
             };
-        </script>
\ No newline at end of file
+        </script>
index 19349e4371f0b3b9861894bcc0c45e4dd8b885c7..73438490e8ea9680bd0c45efc8d11994e572da2f 100644 (file)
@@ -11,7 +11,9 @@
 # All configuration values have a default; values that are commented out
 # serve to show the default.
 
+import datetime
 import sys, os
+import selectable
 
 # If extensions (or modules to document with autodoc) are in another directory,
 # add these directories to sys.path here. If the directory is relative to the
@@ -41,16 +43,16 @@ master_doc = 'index'
 
 # General information about the project.
 project = u'Django-Selectable'
-copyright = u'2011-2013, Mark Lavin'
+copyright = u'2011-%s, Mark Lavin' % datetime.date.today().year
 
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
 # built documents.
 #
 # The short X.Y version.
-version = '0.7'
+version = '.'.join(selectable.__version__.split('.')[0:2])
 # The full version, including alpha/beta/rc tags.
-release = '0.7.0'
+release = selectable.__version__
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
index 513b981f6548e43dbcf131998e4eb5949f7b2f62..67bdba5a2a4e036d08231d10c17c610dd087e02c 100644 (file)
@@ -12,10 +12,10 @@ you can add to our example project.
 Getting the Source
 --------------------------------------
 
-The source code is hosted on `Bitbucket <https://bitbucket.org/mlavin/django-selectable>`_.
-You can download the full source by cloning the hg repo::
+The source code is hosted on `Github <https://github.com/mlavin/django-selectable>`_.
+You can download the full source by cloning the git repo::
 
-    hg clone https://bitbucket.org/mlavin/django-selectable
+    git clone git://github.com/mlavin/django-selectable.git
 
 Feel free to fork the project and make your own changes. If you think that it would
 be helpful for other then please submit a pull request to have it merged in.
@@ -24,7 +24,7 @@ be helpful for other then please submit a pull request to have it merged in.
 Submit an Issue
 --------------------------------------
 
-The issues are also managed on `Bitbucket issue page <https://bitbucket.org/mlavin/django-selectable/issues>`_.
+The issues are also managed on `Github issue page <https://github.com/mlavin/django-selectable/issues>`_.
 If you think you've found a bug it's helpful if you indicate the version of django-selectable
 you are using the ticket version flag. If you think your bug is javascript related it is
 also helpful to know the version of jQuery, jQuery UI, and the browser you are using.
@@ -58,28 +58,26 @@ against multiple versions of Django/Python. With tox installed you can run::
 to run all the version combinations. You can also run tox against a subset of supported
 environments::
 
-    tox -e py26-1.4.X
+    tox -e py27-django15
 
-This example will run the test against the latest 1.5.X, 1.4.X, and 1.3.X releases
-using Python 2.6 and 3.2 for 1.5+. For more information on running/installing tox please see the
+For more information on running/installing tox please see the
 tox documentation: http://tox.readthedocs.org/en/latest/index.html
 
 Client side tests are written using `QUnit <http://docs.jquery.com/QUnit>`_. They
 can be found in ``selectable/tests/qunit/index.html``. The test suite also uses
-`Grunt <https://github.com/gruntjs/grunt>`_ and `PhantomJS <http://phantomjs.org/>`_ to
-run the tests. You can install Grunt and PhantomJS from NPM::
+`PhantomJS <http://phantomjs.org/>`_ to
+run the tests. You can install PhantomJS from NPM::
 
-    # Install grunt requirements
-    npm install -g grunt phantomjs jshint
-    # Execute default grunt tasks
-    grunt
+    # Install requirements
+    npm install -g phantomjs jshint
+    make test-js
 
 
 Building the Documentation
 --------------------------------------
 
 The documentation is built using `Sphinx <http://sphinx.pocoo.org/>`_
-and available on `Read the Docs <http://django-selectable.readthedocs.org/>`_. With
+and available on `Read the Docs <http://django-selectable.readthedocs.io/>`_. With
 Sphinx installed you can build the documentation by running::
 
     make html
index 3b5ad438fa076f8e403ded025aab8e5a1f8c3c16..60cae8ff079cf46ba5ac0803b1db1d9123c69d9f 100644 (file)
@@ -1,7 +1,7 @@
 Fields
 ==========
 
-Django-Selectable defines a number of fields for selecting either single or mutliple
+Django-Selectable defines a number of fields for selecting either single or multiple
 lookup items. Item in this context corresponds to the object return by the underlying
 lookup ``get_item``. The single select select field :ref:`AutoCompleteSelectField`
 allows for the creation of new items. To use this feature the field's
@@ -20,18 +20,19 @@ Field tied to :ref:`AutoCompleteSelectWidget` to bind the selection to the form
 create new items, if allowed. The ``allow_new`` keyword argument (default: ``False``)
 which determines if the field allows new items. This field cleans to a single item.
 
-    .. literalinclude:: ../example/core/forms.py
-        :start-after: # AutoCompleteSelectField (no new items)
-        :end-before: # AutoCompleteSelectField (allows new items)
+    .. code-block:: python
 
+        from django import forms
 
-.. versionadded:: 0.7
+        from selectable.forms import AutoCompleteSelectField
 
-`lookup_class`` may also be a dotted path.
+        from .lookups import FruitLookup
 
-    .. code-block:: python
 
-           selectable.AutoCompleteSelectField(lookup_class='core.lookups.FruitLookup')
+        class FruitSelectionForm(forms.Form):
+            fruit = AutoCompleteSelectField(lookup_class=FruitLookup, label='Select a fruit')
+
+`lookup_class`` may also be a dotted path.
 
 
 .. _AutoCompleteSelectMultipleField:
@@ -43,6 +44,16 @@ Field tied to :ref:`AutoCompleteSelectMultipleWidget` to bind the selection to t
 This field cleans to a list of items. :ref:`AutoCompleteSelectMultipleField` does not
 allow for the creation of new items.
 
-    .. literalinclude:: ../example/core/forms.py
-        :start-after: # AutoCompleteSelectMultipleField
-        :end-before: # AutoComboboxSelectMultipleField
+
+    .. code-block:: python
+
+        from django import forms
+
+        from selectable.forms import AutoCompleteSelectMultipleField
+
+        from .lookups import FruitLookup
+
+
+        class FruitsSelectionForm(forms.Form):
+            fruits = AutoCompleteSelectMultipleField(lookup_class=FruitLookup,
+                label='Select your favorite fruits')
index 20ba1c0d712a238af49abca1f3606cb67e53da71..5bb7cda5ecb2980418cca26094ab714fa545afaf 100644 (file)
@@ -25,7 +25,7 @@ you must register in with django-selectable. All lookups must extend from
         class MyLookup(LookupBase):
             def get_query(self, request, term):
                 data = ['Foo', 'Bar']
-                return filter(lambda x: x.startswith(term), data)
+                return [x for x in data if x.startswith(term)]
 
         registry.register(MyLookup)
 
@@ -139,24 +139,14 @@ than jQuery UI. Most users will not need to override these methods.
     If :ref:`SELECTABLE_MAX_LIMIT` is defined or ``limit`` is passed in request.GET
     then ``paginate_results`` will return the current page using Django's
     built in pagination. See the Django docs on
-    `pagination <https://docs.djangoproject.com/en/1.3/topics/pagination/>`_
+    `pagination <https://docs.djangoproject.com/en/stable/topics/pagination/>`_
     for more info.
 
     :param results: The set of all matched results.
     :param options: Dictionary of ``cleaned_data`` from the lookup form class.
-    :return: The current `Page object <https://docs.djangoproject.com/en/1.3/topics/pagination/#page-objects>`_
+    :return: The current `Page object <https://docs.djangoproject.com/en/stable/topics/pagination/#page-objects>`_
         of results.
 
-.. _lookup-serialize-results:
-
-.. py:method:: LookupBase.serialize_results(self, results)
-
-    Returns serialized results for sending via http. You may choose to override
-    this if you are making use of 
-
-    :param results: a python structure to be serialized e.g. the one returned by :ref:`format_results<lookup-format-results>`
-    :returns: JSON string.
-
 
 .. _ModelLookup:
 
@@ -167,11 +157,24 @@ Perhaps the most common use case is to define a lookup based on a given Django m
 For this you can extend ``selectable.base.ModelLookup``. To extend ``ModelLookup`` you
 should set two class attributes: ``model`` and ``search_fields``.
 
-    .. literalinclude:: ../example/core/lookups.py
-        :pyobject: FruitLookup
+    .. code-block:: python
+
+        from __future__ import unicode_literals
+
+        from selectable.base import ModelLookup
+        from selectable.registry import registry
+
+        from .models import Fruit
+
+
+        class FruitLookup(ModelLookup):
+            model = Fruit
+            search_fields = ('name__icontains', )
+
+        registry.register(FruitLookup)
 
 The syntax for ``search_fields`` is the same as the Django
-`field lookup syntax <http://docs.djangoproject.com/en/1.3/ref/models/querysets/#field-lookups>`_.
+`field lookup syntax <http://docs.djangoproject.com/en/stable/ref/models/querysets/#field-lookups>`_.
 Each of these lookups are combined as OR so any one of them matching will return a
 result. You may optionally define a third class attribute ``filters`` which is a dictionary of
 filters to be applied to the model queryset. The keys should be a string defining a field lookup
@@ -183,7 +186,7 @@ User Lookup Example
 --------------------------------------
 
 Below is a larger model lookup example using multiple search fields, filters
-and display options for the `auth.User <https://docs.djangoproject.com/en/1.3/topics/auth/#users>`_
+and display options for the `auth.User <https://docs.djangoproject.com/en/stable/topics/auth/#users>`_
 model.
 
     .. code-block:: python
@@ -218,8 +221,6 @@ model.
 Lookup Decorators
 --------------------------------------
 
-.. versionadded:: 0.5
-
 Registering lookups with django-selectable creates a small API for searching the
 lookup data. While the amount of visible data is small there are times when you want
 to restrict the set of requests which can view the data. For this purpose there are
index 7d14927cd2d0afad34a382c7ad294eb1eb39dff1..d5a570d75dde0bea2d2846f03b95b33f1c9bf8c3 100644 (file)
@@ -7,8 +7,8 @@ The workflow for using `django-selectable` involves two main parts:
 
 This guide assumes that you have a basic knowledge of creating Django models and
 forms. If not you should first read through the documentation on
-`defining models <http://docs.djangoproject.com/en/1.3/topics/db/models/>`_
-and `using forms <http://docs.djangoproject.com/en/1.3/topics/forms/>`_.
+`defining models <http://docs.djangoproject.com/en/stable/topics/db/models/>`_
+and `using forms <http://docs.djangoproject.com/en/stable/topics/forms/>`_.
 
 .. _start-include-jquery:
 
@@ -16,11 +16,11 @@ Including jQuery & jQuery UI
 --------------------------------------
 
 The widgets in django-selectable define the media they need as described in the
-Django documentation on `Form Media <https://docs.djangoproject.com/en/1.3/topics/forms/media/>`_.
+Django documentation on `Form Media <https://docs.djangoproject.com/en/stable/topics/forms/media/>`_.
 That means to include the javascript and css you need to make the widgets work you
 can include ``{{ form.media.css }}`` and ``{{ form.media.js }}`` in your template. This is
 assuming your form is called `form` in the template context. For more information
-please check out the `Django documentation <https://docs.djangoproject.com/en/1.3/topics/forms/media/>`_.
+please check out the `Django documentation <https://docs.djangoproject.com/en/stable/topics/forms/media/>`_.
 
 The jQuery and jQuery UI libraries are not included in the distribution but must be included
 in your templates. However there is a template tag to easily add these libraries from
@@ -31,17 +31,17 @@ the  from the `Google CDN <http://code.google.com/apis/libraries/devguide.html#j
         {% load selectable_tags %}
         {% include_jquery_libs %}
 
-By default these will use jQuery v1.7.2 and jQuery UI v1.8.23. You can customize the versions
+By default these will use jQuery v1.11.2 and jQuery UI v1.11.3. You can customize the versions
 used by pass them to the tag. The first version is the jQuery version and the second is the
 jQuery UI version.
 
     .. code-block:: html
 
         {% load selectable_tags %}
-        {% include_jquery_libs '1.4.4' '1.8.13' %}
+        {% include_jquery_libs '1.11.2' '1.11.3' %}
 
-Django-Selectable should work with `jQuery <http://jquery.com/>`_ >= 1.4.4 and
-`jQuery UI <http://jqueryui.com/>`_ >= 1.8.13.
+Django-Selectable should work with `jQuery <http://jquery.com/>`_ >= 1.9 and
+`jQuery UI <http://jqueryui.com/>`_ >= 1.10.
 
 You must also include a `jQuery UI theme <http://jqueryui.com/themeroller/>`_ stylesheet. There
 is also a template tag to easily add this style sheet from the Google CDN.
@@ -51,13 +51,13 @@ is also a template tag to easily add this style sheet from the Google CDN.
         {% load selectable_tags %}
         {% include_ui_theme %}
 
-By default this will use the `base <http://jqueryui.com/themeroller/>`_ theme for jQuery UI v1.8.23.
+By default this will use the `base <http://jqueryui.com/themeroller/>`_ theme for jQuery UI v1.11.4.
 You can configure the theme and version by passing them in the tag.
 
     .. code-block:: html
 
         {% load selectable_tags %}
-        {% include_ui_theme 'ui-lightness' '1.8.13' %}
+        {% include_ui_theme 'ui-lightness' '1.11.4' %}
 
 Or only change the theme.
 
@@ -72,11 +72,11 @@ Of course you can choose to include these rescources manually::
 
     .. code-block:: html
 
-        <link rel="stylesheet" href="//ajax.googleapis.com/ajax/libs/jqueryui/1.10.0/themes/base/jquery-ui.css" type="text/css">
-        <link href="{{ STATIC_URL }}selectable/css/dj.selectable.css" type="text/css" media="all" rel="stylesheet">
-        <script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
-        <script src="//ajax.googleapis.com/ajax/libs/jqueryui/1.10.0/jquery-ui.js"></script>
-        <script type="text/javascript" src="{{ STATIC_URL }}selectable/js/jquery.dj.selectable.js"></script>
+        <link rel="stylesheet" href="//ajax.googleapis.com/ajax/libs/jqueryui/1.11.3/themes/base/jquery-ui.css" type="text/css">
+        <link href="{% static 'selectable/css/dj.selectable.css' %}" type="text/css" media="all" rel="stylesheet">
+        <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
+        <script src="//ajax.googleapis.com/ajax/libs/jqueryui/1.11.3/jquery-ui.js"></script>
+        <script type="text/javascript" src="{% static 'selectable/js/jquery.dj.selectable.js' %}"></script>
 
 .. note::
 
@@ -91,17 +91,41 @@ Defining a Lookup
 The lookup classes define the backend views. The most common case is defining a
 lookup which searchs models based on a particular field. Let's define a simple model:
 
-    .. literalinclude:: ../example/core/models.py
-        :pyobject: Fruit
+    .. code-block:: python
+
+        from __future__ import unicode_literals
+
+        from django.db import models
+        from django.utils.encoding import python_2_unicode_compatible
+
+
+        @python_2_unicode_compatible
+        class Fruit(models.Model):
+            name = models.CharField(max_length=200)
+
+            def __str__(self):
+                return self.name
 
 In a `lookups.py` we will define our lookup:
 
-    .. literalinclude:: ../example/core/lookups.py
-        :pyobject: FruitLookup
+    .. code-block:: python
+
+        from __future__ import unicode_literals
+
+        from selectable.base import ModelLookup
+        from selectable.registry import registry
+
+        from .models import Fruit
+
+
+        class FruitLookup(ModelLookup):
+            model = Fruit
+            search_fields = ('name__icontains', )
+
 
 This lookups extends ``selectable.base.ModelLookup`` and defines two things: one is
 the model on which we will be searching and the other is the field which we are searching.
-This syntax should look familiar as it is the same as the `field lookup syntax <http://docs.djangoproject.com/en/1.3/ref/models/querysets/#field-lookups>`_
+This syntax should look familiar as it is the same as the `field lookup syntax <http://docs.djangoproject.com/en/stable/ref/models/querysets/#field-lookups>`_
 for making queries in Django.
 
 Below this definition we will register our lookup class.
@@ -128,9 +152,22 @@ Defining Forms
 
 Now that we have a working lookup we will define a form which uses it:
 
-    .. literalinclude:: ../example/core/forms.py
-        :pyobject: FruitForm
-        :end-before: newautocomplete
+    .. code-block:: python
+
+        from django import forms
+
+        from selectable.forms import AutoCompleteWidget
+
+        from .lookups import FruitLookup
+
+
+        class FruitForm(forms.Form):
+            autocomplete = forms.CharField(
+                label='Type the name of a fruit (AutoCompleteWidget)',
+                widget=AutoCompleteWidget(FruitLookup),
+                required=False,
+            )
+
 
 This replaces the default widget for the ``CharField`` with the ``AutoCompleteWidget``.
 This will allow the user to fill this field with values taken from the names of
index 925ebd663c8c5212176825c334937b5e82b65574..6acd244b7ca700cc8a704ebc87af2187d714f76f 100644 (file)
@@ -1,10 +1,69 @@
 Release Notes
 ==================
 
-v0.7.1 (Released TBD)
+
+v1.1.0 (Released 2018-01-12)
+--------------------------------------
+
+- Updated admin docs.
+- Added support for Django 1.11
+
+Special thanks to Luke Plant for contributing the fixes to support Django 1.11.
+
+
+v1.0.0 (Released 2017-04-14)
+--------------------------------------
+
+This project has been stable for quite some time and finally declaring a 1.0 release. With
+that comes new policies on official supported versions for Django, Python, jQuery, and jQuery UI.
+
+- New translations for German and Czech.
+- Various bug and compatibility fixes.
+- Updated example project.
+
+Special thanks to Raphael Merx for helping track down issues related to this release
+and an updating the example project to work on Django 1.10.
+
+Backwards Incompatible Changes
+________________________________
+
+- Dropped support Python 2.6 and 3.2
+- Dropped support for Django < 1.7. Django 1.11 is not yet supported.
+- ``LookupBase.serialize_results`` had been removed. This is now handled by the built-in ``JsonResponse`` in Django.
+- jQuery and jQuery UI versions for the ``include_jquery_libs`` and ``include_ui_theme`` template tags have been increased to 1.12.4 and 1.11.4 respectively.
+- Dropped testing support for jQuery < 1.9 and jQuery UI < 1.10. Earlier versions may continue to work but it is recommended to upgrade.
+
+
+v0.9.0 (Released 2014-10-21)
+--------------------------------------
+
+This release primarily addresses incompatibility with Django 1.7. The app-loading refactor both
+broke the previous registration and at the same time provided better utilities in Django core to
+make it more robust.
+
+- Compatibility with Django 1.7. Thanks to Calvin Spealman for the fixes.
+- Fixes for Python 3 support.
+
+Backwards Incompatible Changes
+________________________________
+
+- Dropped support for jQuery < 1.7
+
+
+v0.8.0 (Released 2014-01-20)
 --------------------------------------
 
 - Widget media references now include a version string for cache-busting when upgrading django-selectable. Thanks to Ustun Ozgur.
+- Added compatibility code for \*SelectWidgets to handle POST data for the default SelectWidget. Thanks to leo-the-manic.
+- Development moved from Bitbucket to Github.
+- Update test suite compatibility with new test runner in Django 1.6. Thanks to Dan Poirier for the report and fix.
+- Tests now run on Travis CI.
+- Added French and Chinese translations.
+
+Backwards Incompatible Changes
+________________________________
+
+- Support for Django < 1.5 has been dropped. Most pieces should continue to work but there was an ugly JS hack to make django-selectable work nicely in the admin which too flakey to continue to maintain. If you aren't using the selectable widgets in inline-forms in the admin you can most likely continue to use Django 1.4 without issue.
 
 
 v0.7.0 (Released 2013-03-01)
@@ -210,7 +269,7 @@ _________________
 Bug Fixes
 _________________
 
-- Fixed issue with Enter key removing items from select multiple widgets `#24 <https://bitbucket.org/mlavin/django-selectable/issue/24/pressing-enter-when-autocomplete-input-box>`_
+- Fixed issue with Enter key removing items from select multiple widgets `#24 <https://github.com/mlavin/django-selectable/issues/24>`_
 
 
 Backwards Incompatible Changes
@@ -241,7 +300,7 @@ v0.1.2 (Released 2011-05-25)
 Bug Fixes
 _________________
 
-- Fixed issue `#17 <https://bitbucket.org/mlavin/django-selectable/issue/17/update-not-working>`_
+- Fixed issue `#17 <https://github.com/mlavin/django-selectable/issues/17>`_
 
 
 v0.1.1 (Release 2011-03-21)
index ea20475a3c8b5c27d360b0355327ff72624ebf64..80cf04b45821f538500b0ab65c5abaefa7a05ca6 100644 (file)
@@ -16,8 +16,6 @@ the query string to retrive more values.
 Default: ``25``
 
 
-.. versionadded:: 0.6
-
 .. _SELECTABLE_ESCAPED_KEYS:
 
 SELECTABLE_ESCAPED_KEYS
index 37ab83a81f52190b21e40d2a3d257071c1427c05..234b4ac2fb5f3a20c62c3110046428e6fa92434b 100644 (file)
@@ -8,7 +8,7 @@ own. This section contains some tips or techniques for testing your lookups.
 
 This guide assumes that you are reasonable familiar with the concepts of unit testing
 including Python's `unittest <http://docs.python.org/2/library/unittest.html>`_ module and
-Django's `testing guide <https://docs.djangoproject.com/en/1.4/topics/testing/>`_.
+Django's `testing guide <https://docs.djangoproject.com/en/stable/topics/testing/>`_.
 
 
 Testing Forms with django-selectable
@@ -17,7 +17,7 @@ Testing Forms with django-selectable
 For the most part testing forms which use django-selectable's custom fields
 and widgets is the same as testing any Django form. One point that is slightly
 different is that the select and multi-select widgets are
-`MultiWidgets <https://docs.djangoproject.com/en/1.4/ref/forms/widgets/#django.forms.MultiWidget>`_.
+`MultiWidgets <https://docs.djangoproject.com/en/stable/ref/forms/widgets/#django.forms.MultiWidget>`_.
 The effect of this is that there are two names in the post rather than one. Take the below
 form for example.
 
@@ -100,15 +100,19 @@ Here you will note that while there is only one field ``thing`` it requires
 two items in the POST the first is for the text input and the second is for
 the hidden input. This is again due to the use of MultiWidget for the selection.
 
+There is compatibility code in the widgets to lookup the original name
+from the POST. This makes it easier to transition to the the selectable widgets without
+breaking existing tests.
+
 
 Testing Lookup Results
 --------------------------------------------------
 
 Testing the lookups used by django-selectable is similar to testing your Django views.
 While it might be tempting to use the Django
-`test client <https://docs.djangoproject.com/en/1.4/topics/testing/#module-django.test.client>`_,
+`test client <https://docs.djangoproject.com/en/stable/topics/testing/#module-django.test.client>`_,
 it is slightly easier to use the
-`request factory <https://docs.djangoproject.com/en/1.4/topics/testing/#the-request-factory>`_.
+`request factory <https://docs.djangoproject.com/en/stable/topics/testing/#the-request-factory>`_.
 A simple example is given below.
 
     .. code-block:: python
index b9510c905846ca5b840552613629fcb4d5cedcf0..2235438658eae3057fe7c4bda96181ba5a82041b 100644 (file)
@@ -9,8 +9,6 @@ additional query parameters to the lookup search. See the section on
 :ref:`Adding Parameters on the Server Side <server-side-parameters>` for more
 information.
 
-.. versionadded:: 0.7
-
 You can configure the plugin options by passing the configuration dictionary in the ``data-selectable-options``
 attribute. The set of options availble include those define by the base
 `autocomplete plugin <http://api.jqueryui.com/1.9/autocomplete/>`_ as well as the
@@ -50,7 +48,7 @@ This widget should be used in conjunction with the :ref:`AutoCompleteSelectField
 return both the text entered by the user and the id (if an item was selected/matched).
 
 :ref:`AutoCompleteSelectWidget` works directly with Django's
-`ModelChoiceField <https://docs.djangoproject.com/en/1.3/ref/forms/fields/#modelchoicefield>`_.
+`ModelChoiceField <https://docs.djangoproject.com/en/stable/ref/forms/fields/#modelchoicefield>`_.
 You can simply replace the widget without replacing the entire field.
 
     .. code-block:: python
@@ -65,8 +63,6 @@ You can simply replace the widget without replacing the entire field.
 
 The one catch is that you must use ``allow_new=False`` which is the default.
 
-.. versionadded:: 0.7
-
 ``lookup_class`` may also be a dotted path.
 
     .. code-block:: python
@@ -82,7 +78,7 @@ AutoComboboxSelectWidget
 Similar to :ref:`AutoCompleteSelectWidget` but has a button to reveal all options.
 
 :ref:`AutoComboboxSelectWidget` works directly with Django's
-`ModelChoiceField <https://docs.djangoproject.com/en/1.3/ref/forms/fields/#modelchoicefield>`_.
+`ModelChoiceField <https://docs.djangoproject.com/en/stable/ref/forms/fields/#modelchoicefield>`_.
 You can simply replace the widget without replacing the entire field.
 
     .. code-block:: python
index dd91652d93ed37bed80f5b52dcd366cf4dacb7a2..82dc1590d72405ce70a2479ceed8fa1dc5c797c0 100644 (file)
@@ -13,19 +13,25 @@ if not settings.configured:
                 'NAME': ':memory:',
             }
         },
+        MIDDLEWARE_CLASSES=(),
         INSTALLED_APPS=(
             'selectable',
         ),
         SITE_ID=1,
         SECRET_KEY='super-secret',
         ROOT_URLCONF='selectable.tests.urls',
-    )
+        TEMPLATES=[{
+            'BACKEND': 'django.template.backends.django.DjangoTemplates',
+            'DIRS': [os.path.join(os.path.normpath(os.path.join(
+                os.path.dirname(__file__), 'selectable')), 'templates')]}])
 
 
+from django import setup
 from django.test.utils import get_runner
 
 
 def runtests():
+    setup()
     TestRunner = get_runner(settings)
     test_runner = TestRunner(verbosity=1, interactive=True, failfast=False)
     args = sys.argv[1:] or ['selectable', ]
@@ -35,4 +41,3 @@ def runtests():
 
 if __name__ == '__main__':
     runtests()
-
index b4232fc2b8b6edbee4dbe59339eb19c7b425e6d1..0412b9b014780ec7a1489412036ae9e449c0a997 100644 (file)
@@ -1,6 +1,6 @@
 "Auto-complete selection widgets using Django and jQuery UI."
 
 
-__version__ = '0.9.0'
+__version__ = '1.1.0'
 
 default_app_config = 'selectable.apps.SelectableConfig'
index f65c5e1a2fa5ab66c25a07b1b25ee72c0e0c3c8c..579bf7814a3a0fb715871bb3294d9fc25b2774a4 100644 (file)
@@ -1,7 +1,4 @@
-try:
-    from django.apps import AppConfig
-except ImportError:
-    AppConfig = object
+from django.apps import AppConfig
 
 
 class SelectableConfig(AppConfig):
index 8335ddd796a1277b597e941d7d499eb148b0fc88..bc74204ce5190fa50e495003e272194d39a19b6e 100644 (file)
@@ -1,7 +1,6 @@
 "Base classes for lookup creation."
 from __future__ import unicode_literals
 
-import json
 import operator
 import re
 from functools import reduce
@@ -9,13 +8,12 @@ from functools import reduce
 from django.conf import settings
 from django.core.paginator import Paginator, InvalidPage, EmptyPage
 from django.core.urlresolvers import reverse
-from django.core.serializers.json import DjangoJSONEncoder
-from django.http import HttpResponse
-from django.db.models import Q
+from django.http import JsonResponse
+from django.db.models import Q, Model
+from django.utils.encoding import smart_text
 from django.utils.html import conditional_escape
 from django.utils.translation import ugettext as _
 
-from selectable.compat import smart_text
 from selectable.forms import BaseLookupForm
 
 
@@ -25,14 +23,6 @@ __all__ = (
 )
 
 
-class JsonResponse(HttpResponse):
-    "HttpResponse subclass for returning JSON data."
-
-    def __init__(self, *args, **kwargs):
-        kwargs['content_type'] = 'application/json'
-        super(JsonResponse, self).__init__(*args, **kwargs)
-
-
 class LookupBase(object):
     "Base class for all django-selectable lookups."
 
@@ -100,8 +90,7 @@ class LookupBase(object):
             term = options.get('term', '')
             raw_data = self.get_query(request, term)
             results = self.format_results(raw_data, options)
-        content = self.serialize_results(results)
-        return self.response(content)
+        return self.response(results)
 
     def format_results(self, raw_data, options):
         '''
@@ -123,10 +112,6 @@ class LookupBase(object):
         results['meta'] = meta
         return results
 
-    def serialize_results(self, results):
-        "Returns serialized results for sending via http."
-        return json.dumps(results, cls=DjangoJSONEncoder, ensure_ascii=False)
-
 
 class ModelLookup(LookupBase):
     "Lookup class for easily defining lookups based on Django models."
@@ -146,10 +131,7 @@ class ModelLookup(LookupBase):
         return qs
 
     def get_queryset(self):
-        try:
-            qs = self.model._default_manager.get_queryset()
-        except AttributeError:  # Django <= 1.5.
-            qs = self.model._default_manager.get_query_set()
+        qs = self.model._default_manager.get_queryset()
         if self.filters:
             qs = qs.filter(**self.filters)
         return qs
@@ -160,6 +142,7 @@ class ModelLookup(LookupBase):
     def get_item(self, value):
         item = None
         if value:
+            value = value.pk if isinstance(value, Model) else value
             try:
                 item = self.get_queryset().get(pk=value)
             except (ValueError, self.model.DoesNotExist):
index a63ab6a12e4d786297732b6e3ce4a58dadc384fd..448a798232611de72021347c0d2b8aead4110b20 100644 (file)
@@ -1,36 +1,6 @@
 "Compatibility utilites for Python/Django versions."
-import sys
 
 try:
     from urllib.parse import urlparse
 except ImportError:
     from urlparse import urlparse
-
-try:
-    from django.utils.encoding import smart_text, force_text
-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,
-
-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 c39c2f731061c43710d505ba59aa45c90b92f582..b2abb0dec4ab0298eac23d9270f361ef73d88a1e 100644 (file)
@@ -1,9 +1,10 @@
 from __future__ import unicode_literals
 
+from importlib import import_module
+
 from django import forms
 from django.conf import settings
-
-from selectable.compat import string_types, import_module
+from django.utils.six import string_types
 
 
 __all__ = (
index 21109c4160fc8ac121ded32f00edf63db1e36c4b..6336cb12bc10caed0bb24c7832f9e2ec7fd03234 100644 (file)
@@ -1,6 +1,6 @@
 from __future__ import unicode_literals
 
-from django import forms
+from django import forms, VERSION as DJANGO_VERSION
 from django.core.exceptions import ValidationError
 from django.core.validators import EMPTY_VALUES
 from django.utils.translation import ugettext_lazy as _
@@ -26,7 +26,7 @@ def model_vars(obj):
 
 class BaseAutoCompleteField(forms.Field):
 
-    def _has_changed(self, initial, data):
+    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
@@ -41,6 +41,11 @@ class BaseAutoCompleteField(forms.Field):
         else:
             return data != initial
 
+    if DJANGO_VERSION < (1, 8):
+        def _has_changed(self, initial, data):
+            return self.has_changed(initial, data)
+
+
 class AutoCompleteSelectField(BaseAutoCompleteField):
     widget = AutoCompleteSelectWidget
 
index f23332c60d0cae8ccdb74aae2ab27d0fdfa7c5ee..5edc0c7d0bda686d304ec4c3e4ef46d6a02d752c 100644 (file)
@@ -1,14 +1,16 @@
 from __future__ import unicode_literals
 
+import inspect
 import json
 
-from django import forms, VERSION as DJANGO_VERSION
+from django import forms
 from django.conf import settings
+from django.forms.utils import flatatt
+from django.utils.encoding import force_text
 from django.utils.http import urlencode
 from django.utils.safestring import mark_safe
 
 from selectable import __version__
-from selectable.compat import force_text, flatatt
 from selectable.forms.base import import_lookup_class
 
 __all__ = (
@@ -33,7 +35,59 @@ class SelectableMediaMixin(object):
         js = ('%sjs/jquery.dj.selectable.js?v=%s' % (STATIC_PREFIX, __version__),)
 
 
-class AutoCompleteWidget(forms.TextInput, SelectableMediaMixin):
+new_style_build_attrs = (
+    'base_attrs' in
+    inspect.getargs(forms.widgets.Widget.build_attrs.__code__).args)
+
+
+class BuildAttrsCompat(object):
+    """
+    Mixin to provide compatibility between old and new function
+    signatures for Widget.build_attrs, and a hook for adding our
+    own attributes.
+    """
+    # These are build_attrs definitions that make it easier for
+    # us to override, without having to worry about the signature,
+    # by adding a standard hook, `build_attrs_extra`.
+    # It has a different signature when we are running different Django
+    # versions.
+    if new_style_build_attrs:
+        def build_attrs(self, base_attrs, extra_attrs=None):
+            attrs = super(BuildAttrsCompat, self).build_attrs(
+                base_attrs, extra_attrs=extra_attrs)
+            return self.build_attrs_extra(attrs)
+    else:
+        def build_attrs(self, extra_attrs=None, **kwargs):
+            attrs = super(BuildAttrsCompat, self).build_attrs(
+                extra_attrs=extra_attrs, **kwargs)
+            return self.build_attrs_extra(attrs)
+
+    def build_attrs_extra(self, attrs):
+        # Default implementation, does nothing
+        return attrs
+
+    # These provide a standard interface for when we want to call build_attrs
+    # in our own `render` methods. In both cases it is the same as the Django
+    # 1.11 signature, but has a different implementation for different Django
+    # versions.
+    if new_style_build_attrs:
+        def build_attrs_compat(self, base_attrs, extra_attrs=None):
+            return self.build_attrs(base_attrs, extra_attrs=extra_attrs)
+
+    else:
+        def build_attrs_compat(self, base_attrs, extra_attrs=None):
+            # Implementation copied from Django 1.11, plus include our
+            # hook `build_attrs_extra`
+            attrs = base_attrs.copy()
+            if extra_attrs is not None:
+                attrs.update(extra_attrs)
+            return self.build_attrs_extra(attrs)
+
+
+CompatMixin = BuildAttrsCompat
+
+
+class AutoCompleteWidget(CompatMixin, forms.TextInput, SelectableMediaMixin):
 
     def __init__(self, lookup_class, *args, **kwargs):
         self.lookup_class = import_lookup_class(lookup_class)
@@ -45,8 +99,8 @@ class AutoCompleteWidget(forms.TextInput, SelectableMediaMixin):
     def update_query_parameters(self, qs_dict):
         self.qs.update(qs_dict)
 
-    def build_attrs(self, extra_attrs=None, **kwargs):
-        attrs = super(AutoCompleteWidget, self).build_attrs(extra_attrs, **kwargs)
+    def build_attrs_extra(self, attrs):
+        attrs = super(AutoCompleteWidget, self).build_attrs_extra(attrs)
         url = self.lookup_class.url()
         if self.limit and 'limit' not in self.qs:
             self.qs['limit'] = self.limit
@@ -60,20 +114,11 @@ class AutoCompleteWidget(forms.TextInput, SelectableMediaMixin):
         return attrs
 
 
-class SelectableMultiWidget(forms.MultiWidget):
+class SelectableMultiWidget(CompatMixin, forms.MultiWidget):
 
     def update_query_parameters(self, qs_dict):
         self.widgets[0].update_query_parameters(qs_dict)
 
-    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:
             lookup = self.lookup_class()
@@ -144,8 +189,8 @@ class AutoCompleteSelectWidget(_BaseSingleSelectWidget):
 
 class AutoComboboxWidget(AutoCompleteWidget, SelectableMediaMixin):
 
-    def build_attrs(self, extra_attrs=None, **kwargs):
-        attrs = super(AutoComboboxWidget, self).build_attrs(extra_attrs, **kwargs)
+    def build_attrs_extra(self, attrs):
+        attrs = super(AutoComboboxWidget, self).build_attrs_extra(attrs)
         attrs['data-selectable-type'] = 'combobox'
         return attrs
 
@@ -155,40 +200,75 @@ class AutoComboboxSelectWidget(_BaseSingleSelectWidget):
     primary_widget = AutoComboboxWidget
 
 
-class LookupMultipleHiddenInput(forms.MultipleHiddenInput):
+class LookupMultipleHiddenInput(CompatMixin, forms.MultipleHiddenInput):
 
     def __init__(self, lookup_class, *args, **kwargs):
         self.lookup_class = import_lookup_class(lookup_class)
         super(LookupMultipleHiddenInput, self).__init__(*args, **kwargs)
 
+    # This supports Django 1.11 and later
+    def get_context(self, name, value, attrs):
+        lookup = self.lookup_class()
+        values = self._normalize_value(value)
+        values = list(values)  # force evaluation
+
+        context = super(LookupMultipleHiddenInput, self).get_context(name, values, attrs)
+
+        # Now override/add to what super() did:
+        subwidgets = context['widget']['subwidgets']
+        for widget_ctx, item in zip(subwidgets, values):
+            input_value, title = self._lookup_value_and_title(lookup, item)
+            widget_ctx['value'] = input_value  # override what super() did
+            if title:
+                widget_ctx['attrs']['title'] = title
+        return context
+
+    # This supports Django 1.10 and earlier
     def render(self, name, value, attrs=None, choices=()):
         lookup = self.lookup_class()
-        if value is None: value = []
-        final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
-        id_ = final_attrs.get('id', None)
+        value = self._normalize_value(value)
+
+        base_attrs = dict(self.attrs, type=self.input_type, name=name)
+        combined_attrs = self.build_attrs_compat(base_attrs, attrs)
+        id_ = combined_attrs.get('id', None)
         inputs = []
-        model = getattr(self.lookup_class, 'model', None)
         for i, v in enumerate(value):
-            item = None
-            if model and isinstance(v, model):
-                item = v
-                v = lookup.get_item_id(item)
-            input_attrs = dict(value=force_text(v), **final_attrs)
+            input_attrs = combined_attrs.copy()
+            v_, title = self._lookup_value_and_title(lookup, v)
+            input_attrs.update(
+                value=v_,
+                title=title,
+            )
             if id_:
                 # An ID attribute was given. Add a numeric index as a suffix
                 # so that the inputs don't all have the same ID attribute.
                 input_attrs['id'] = '%s_%s' % (id_, i)
-            if v:
-                item = item or lookup.get_item(v)
-                input_attrs['title'] = lookup.get_item_value(item)
             inputs.append('<input%s />' % flatatt(input_attrs))
         return mark_safe('\n'.join(inputs))
 
-    def build_attrs(self, extra_attrs=None, **kwargs):
-        attrs = super(LookupMultipleHiddenInput, self).build_attrs(extra_attrs, **kwargs)
+    # These are used by both paths
+    def build_attrs_extra(self, attrs):
+        attrs = super(LookupMultipleHiddenInput, self).build_attrs_extra(attrs)
         attrs['data-selectable-type'] = 'hidden-multiple'
         return attrs
 
+    def _normalize_value(self, value):
+        if value is None:
+            value = []
+        return value
+
+    def _lookup_value_and_title(self, lookup, v):
+        model = getattr(self.lookup_class, 'model', None)
+        item = None
+        if model and isinstance(v, model):
+            item = v
+            v = lookup.get_item_id(item)
+        title = None
+        if v:
+            item = item or lookup.get_item(v)
+            title = lookup.get_item_value(item)
+        return force_text(v), title
+
 
 class _BaseMultipleSelectWidget(SelectableMultiWidget, SelectableMediaMixin):
     """
@@ -225,23 +305,18 @@ class _BaseMultipleSelectWidget(SelectableMultiWidget, SelectableMediaMixin):
             value = self.get_compatible_postdata(data, name)
         return value
 
+    def build_attrs_extra(self, attrs):
+        attrs = super(_BaseMultipleSelectWidget, self).build_attrs_extra(attrs)
+        if 'required' in attrs:
+            attrs.pop('required')
+        return attrs
+
     def render(self, name, value, attrs=None):
         if value and not hasattr(value, '__iter__'):
             value = [value]
         value = ['', value]
         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 AutoCompleteSelectMultipleWidget(_BaseMultipleSelectWidget):
 
index be1af0b8e9f1599e28a11f574b27794130aed3a0..5a475af7e7ce45463606a61be242a6cbd513843a 100644 (file)
@@ -1,7 +1,9 @@
 from __future__ import unicode_literals
 
-from selectable.base import LookupBase, ModelLookup
-from selectable.compat import force_text
+from django.utils.encoding import force_text
+from django.utils.module_loading import autodiscover_modules
+
+from selectable.base import LookupBase
 from selectable.exceptions import (LookupAlreadyRegistered, LookupNotRegistered,
                                     LookupInvalid)
 
@@ -37,26 +39,5 @@ registry = LookupRegistry()
 
 
 def autodiscover():
-
-    import copy
-    from django.conf import settings
-    
-    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 1de6c5338b3a472bbc80abcdf1784ca20da07163..c16cc64d43ddbbcdbba3b6a242fbbeca2baf0c66 100644 (file)
@@ -71,7 +71,8 @@
                     icons: {
                         primary: this.options.removeIcon
                     },
-                    text: false
+                    text: false,
+                    disabled: this.disabled
                 },
                 button = $('<a>')
                 .attr('href', '#')
                     icons: {
                         primary: this.options.comboboxIcon
                     },
-                    text: false
+                    text: false,
+                    disabled: this.disabled
                 },
                 button = $("<a>")
                     .html("&nbsp;")
             this.hiddenSelector = ':input[data-selectable-type=hidden][name=' + this.hiddenName + ']';
             this.hiddenMultipleSelector = ':input[data-selectable-type=hidden-multiple][name=' + this.hiddenName + ']';
             this.selectableType = data.selectableType || data['selectable-type'];
+            this.disabled = $input.prop('disabled');
             if (this.allowMultiple) {
                 this.allowNew = false;
                 $input.val("");
index faa4f9387a4b5bc0ee2a9a6d01d26ef0d8a81de3..c380e5306f439424dd2b4b398ed80ceb9cd9d8c6 100755 (executable)
@@ -1,17 +1,15 @@
 from __future__ import unicode_literals
 
 from django import template
-from django.conf import settings
-
 
 register = template.Library()
 
 
 @register.inclusion_tag('selectable/jquery-js.html')
-def include_jquery_libs(version='1.7.2', ui='1.8.23'):
+def include_jquery_libs(version='1.12.4', ui='1.11.4'):
     return {'version': version, 'ui': ui}
 
 
 @register.inclusion_tag('selectable/jquery-css.html')
-def include_ui_theme(theme='base', version='1.8.23'):
+def include_ui_theme(theme='smoothness', version='1.11.4'):
     return {'theme': theme, 'version': version}
index afac57e992592467bbb0a9d2b52efdb5ba360a1f..3227bb50e2eab126a97507c3946317113b085bc7 100644 (file)
@@ -13,6 +13,9 @@ class Thing(models.Model):
     def __str__(self):
         return self.name
 
+    class Meta:
+        ordering = ['id']
+
 
 @python_2_unicode_compatible
 class OtherThing(models.Model):
@@ -38,13 +41,3 @@ class ThingLookup(ModelLookup):
 
 
 registry.register(ThingLookup)
-
-
-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 fdb4f16982f443122912b409ec567cc8e2e9aefa..c77c96100ee855cb16737792e68f13344def6617 100644 (file)
@@ -2,13 +2,14 @@ from __future__ import unicode_literals
 
 import random
 import string
+from collections import defaultdict
 from xml.dom.minidom import parseString
 
-from django.conf import settings
-from django.test import TestCase
 
-from ..base import ModelLookup
+from django.test import TestCase, override_settings
+
 from . import Thing
+from ..base import ModelLookup
 
 
 def as_xml(html):
@@ -28,22 +29,8 @@ def parsed_inputs(html):
     return inputs
 
 
-class PatchSettingsMixin(object):
-    def setUp(self):
-        super(PatchSettingsMixin, self).setUp()
-        self.is_limit_set = hasattr(settings, 'SELECTABLE_MAX_LIMIT')
-        if self.is_limit_set:
-            self.original_limit = settings.SELECTABLE_MAX_LIMIT
-        settings.SELECTABLE_MAX_LIMIT = 25
-
-    def tearDown(self):
-        super(PatchSettingsMixin, self).tearDown()
-        if self.is_limit_set:
-            settings.SELECTABLE_MAX_LIMIT = self.original_limit
-
-
+@override_settings(ROOT_URLCONF='selectable.tests.urls')
 class BaseSelectableTestCase(TestCase):
-    urls = 'selectable.tests.urls'
 
     def get_random_string(self, length=10):
         return ''.join(random.choice(string.ascii_letters) for x in range(length))
@@ -60,4 +47,51 @@ class BaseSelectableTestCase(TestCase):
 
 class SimpleModelLookup(ModelLookup):
     model = Thing
-    search_fields = ('name__icontains', )
\ No newline at end of file
+    search_fields = ('name__icontains', )
+
+
+def parsed_widget_attributes(widget):
+    """
+    Get a dictionary-like object containing all HTML attributes
+    of the rendered widget.
+
+    Lookups on this object raise ValueError if there is more than one attribute
+    of the given name in the HTML, and they have different values.
+    """
+    # For the tests that use this, it generally doesn't matter what the value
+    # is, so we supply anything.
+    rendered = widget.render('a_name', 'a_value')
+    return AttrMap(rendered)
+
+
+class AttrMap(object):
+    def __init__(self, html):
+        dom = as_xml(html)
+        self._attrs = defaultdict(set)
+        self._build_attr_map(dom)
+
+    def _build_attr_map(self, dom):
+        for node in _walk_nodes(dom):
+            if node.attributes is not None:
+                for k, v in node.attributes.items():
+                    self._attrs[k].add(v)
+
+    def __contains__(self, key):
+        return key in self._attrs and len(self._attrs[key]) > 0
+
+    def __getitem__(self, key):
+        if key not in self:
+            raise KeyError(key)
+        vals = self._attrs[key]
+        if len(vals) > 1:
+            raise ValueError("More than one value for attribute {0}: {1}".
+                             format(key, ", ".join(vals)))
+        else:
+            return list(vals)[0]
+
+
+def _walk_nodes(dom):
+    yield dom
+    for child in dom.childNodes:
+        for item in _walk_nodes(child):
+            yield item
index 51160509235dcbf3ec44db2a7314a418a3505a0e..84ee0a9842b976cffb6d86eba061fa85b579cada 100644 (file)
@@ -3,8 +3,8 @@
   var jqversion = location.search.match(/[?&]jquery=(.*?)(?=&|$)/);
   var uiversion = location.search.match(/[?&]ui=(.*?)(?=&|$)/);
   var path;
-  window.jqversion = jqversion && jqversion[1] || '1.7.2';
-  window.uiversion = uiversion && uiversion[1] || '1.8.24';
+  window.jqversion = jqversion && jqversion[1] || '1.11.2';
+  window.uiversion = uiversion && uiversion[1] || '1.11.4';
   jqpath = 'http://code.jquery.com/jquery-' + window.jqversion + '.js';
   uipath = 'http://code.jquery.com/ui/' + window.uiversion + '/jquery-ui.js';
   // This is the only time I'll ever use document.write, I promise!
index 7b6086fa779a21eaff58ce40cd31028a598aad06..6720dd401c512021481c5a1566fab73a59c51e14 100644 (file)
@@ -2,11 +2,16 @@
 
 define(['selectable'], function ($) {
 
-    var expectedNamespace = 'djselectable';
+    var expectedNamespace = 'djselectable',
+        useData = true;
     if (window.uiversion.lastIndexOf('1.10', 0) === 0) {
         // jQuery UI 1.10 introduces a namespace change to include ui-prefix
         expectedNamespace = 'ui-' + expectedNamespace;
     }
+    if (window.uiversion.lastIndexOf('1.11', 0) === 0) {
+        // jQuery UI 1.11 introduces an instance method to get the current instance
+        useData = false;
+    }
 
     module("Autocomplete Text Methods Tests");
 
@@ -16,7 +21,11 @@ define(['selectable'], function ($) {
         $('#qunit-fixture').append(input);
         bindSelectables('#qunit-fixture');
         ok(input.hasClass('ui-autocomplete-input'), "input should be bound with djselecable widget");
-        ok(input.data(expectedNamespace), "input should be bound with djselecable widget");
+        if (useData) {
+            ok(input.data(expectedNamespace), "input should be bound with djselecable widget");
+        } else {
+            ok(input.djselectable('instance'), "input should be bound with djselecable widget");
+        }
     });
 
     test("Manual Selection", function () {
@@ -48,7 +57,11 @@ define(['selectable'], function ($) {
         bindSelectables('#qunit-fixture');
         button = $('.ui-combo-button', '#qunit-fixture');
         ok(input.hasClass('ui-autocomplete-input'), "input should be bound with djselecable widget");
-        ok(input.data(expectedNamespace), "input should be bound with djselecable widget");
+        if (useData) {
+            ok(input.data(expectedNamespace), "input should be bound with djselecable widget");
+        } else {
+            ok(input.djselectable('instance'), "input should be bound with djselecable widget");
+        }
         equal(button.length, 1, "combobox button should be created");
     });
 
@@ -81,7 +94,11 @@ define(['selectable'], function ($) {
         $('#qunit-fixture').append(hiddenInput);
         bindSelectables('#qunit-fixture');
         ok(textInput.hasClass('ui-autocomplete-input'), "input should be bound with djselecable widget");
-        ok(textInput.data(expectedNamespace), "input should be bound with djselecable widget");
+        if (useData) {
+            ok(textInput.data(expectedNamespace), "input should be bound with djselecable widget");
+        } else {
+            ok(textInput.djselectable('instance'), "input should be bound with djselecable widget");
+        }
     });
 
     test("Manual Selection", function () {
@@ -121,7 +138,11 @@ define(['selectable'], function ($) {
         bindSelectables('#qunit-fixture');
         button = $('.ui-combo-button', '#qunit-fixture');
         ok(textInput.hasClass('ui-autocomplete-input'), "input should be bound with djselecable widget");
-        ok(textInput.data(expectedNamespace), "input should be bound with djselecable widget");
+        if (useData) {
+            ok(textInput.data(expectedNamespace), "input should be bound with djselecable widget");
+        } else {
+            ok(textInput.djselectable('instance'), "input should be bound with djselecable widget");
+        }
         equal(button.length, 1, "combobox button should be created");
     });
 
@@ -161,7 +182,11 @@ define(['selectable'], function ($) {
         bindSelectables('#qunit-fixture');
         deck = $('.selectable-deck', '#qunit-fixture');
         ok(textInput.hasClass('ui-autocomplete-input'), "input should be bound with djselecable widget");
-        ok(textInput.data(expectedNamespace), "input should be bound with djselecable widget");
+        if (useData) {
+            ok(textInput.data(expectedNamespace), "input should be bound with djselecable widget");
+        } else {
+            ok(textInput.djselectable('instance'), "input should be bound with djselecable widget");
+        }
         equal($('li', deck).length, 0, "no initial deck items");
     });
 
@@ -204,7 +229,11 @@ define(['selectable'], function ($) {
         deck = $('.selectable-deck', '#qunit-fixture');
         button = $('.ui-combo-button', '#qunit-fixture');
         ok(textInput.hasClass('ui-autocomplete-input'), "input should be bound with djselecable widget");
-        ok(textInput.data(expectedNamespace), "input should be bound with djselecable widget");
+        if (useData) {
+            ok(textInput.data(expectedNamespace), "input should be bound with djselecable widget");
+        } else {
+            ok(textInput.djselectable('instance'), "input should be bound with djselecable widget");
+        }
         equal($('li', deck).length, 0, "no initial deck items");
         equal(button.length, 1, "combobox button should be created");
     });
index e962003e0db1c86bb37a7a2841ed27f95e4f231f..24d12cb63428e96a687d199d230b90e00c768355 100644 (file)
@@ -42,7 +42,7 @@ class FieldTestMixin(object):
         An invalid lookup_class dotted path should raise an ImportError.
         """
         with self.assertRaises(ImportError):
-            self.field_cls('this.is.an.invalid.path')
+            self.field_cls('that.is.an.invalid.path')
 
     def test_dotted_path_wrong_type(self):
         """
index 7bce96eec91113f058c24e88820c97f2d2dc2fc2..ed6bac90e60663df38bb6f958c7899713748d6f8 100644 (file)
@@ -1,7 +1,5 @@
-from django.conf import settings
-
 from ..forms import BaseLookupForm
-from .base import BaseSelectableTestCase, PatchSettingsMixin
+from .base import BaseSelectableTestCase
 
 
 __all__ = (
@@ -9,7 +7,7 @@ __all__ = (
 )
 
 
-class BaseLookupFormTestCase(PatchSettingsMixin, BaseSelectableTestCase):
+class BaseLookupFormTestCase(BaseSelectableTestCase):
 
     def get_valid_data(self):
         data = {
@@ -39,12 +37,13 @@ class BaseLookupFormTestCase(PatchSettingsMixin, BaseSelectableTestCase):
         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)
+        with self.settings(SELECTABLE_MAX_LIMIT=25):
+            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'], 25)
 
     def test_no_max_set(self):
         """
@@ -52,12 +51,12 @@ class BaseLookupFormTestCase(PatchSettingsMixin, BaseSelectableTestCase):
         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'])
+        with self.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):
         """
@@ -65,13 +64,13 @@ class BaseLookupFormTestCase(PatchSettingsMixin, BaseSelectableTestCase):
         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'))
+        with self.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):
         """
@@ -79,8 +78,9 @@ class BaseLookupFormTestCase(PatchSettingsMixin, BaseSelectableTestCase):
         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)
+        with self.settings(SELECTABLE_MAX_LIMIT=25):
+            data = self.get_valid_data()
+            data['limit'] = 125
+            form = BaseLookupForm(data)
+            self.assertTrue(form.is_valid(), "%s" % form.errors)
+            self.assertEqual(form.cleaned_data['limit'], 25)
index 01bf9bfcb8525b2d58d9b066887345d8a19ad4c0..ef3933480b4f8d845616e0fa4051d8b3b9ca5b82 100644 (file)
@@ -8,7 +8,7 @@ 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
+from .base import BaseSelectableTestCase
 
 
 __all__ = (
@@ -26,6 +26,7 @@ class OtherThingForm(forms.ModelForm):
 
     class Meta(object):
         model = OtherThing
+        fields = ('name', 'thing', )
 
 
 class FuncAutoCompleteSelectTestCase(BaseSelectableTestCase):
@@ -77,32 +78,57 @@ class FuncAutoCompleteSelectTestCase(BaseSelectableTestCase):
         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)
+        self.assertInHTML(
+            '''
+            <input data-selectable-allow-new="false" data-selectable-type="text"
+                data-selectable-url="/selectable-tests/selectable-thinglookup/"
+                id="id_thing_0" name="thing_0" type="text" value="{}" {} />
+            '''.format(self.test_thing.name,
+                       'required' if hasattr(form, 'use_required_attribute') else ''),
+            rendered_form
+        )
         # Selected pk should be populated
-        thing_1 = inputs['thing_1'][0]
-        self.assertEqual(int(thing_1.attributes['value'].value), self.test_thing.pk)
+        self.assertInHTML(
+            '''
+            <input data-selectable-type="hidden" name="thing_1" id="id_thing_1"
+                type="hidden" value="{}" {} />
+            '''.format(self.test_thing.pk,
+                       'required' if hasattr(form, 'use_required_attribute') else ''),
+            rendered_form,
+        )
 
     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)
+        self.assertInHTML(
+            '''
+            <input data-selectable-allow-new="false" data-selectable-type="text"
+                data-selectable-url="/selectable-tests/selectable-thinglookup/"
+                id="id_thing_0" name="thing_0" type="text" value="{}" {} />
+            '''.format(self.test_thing.name,
+                       'required' if hasattr(form, 'use_required_attribute') else ''),
+            rendered_form
+        )
         # Selected pk should be populated
-        thing_1 = inputs['thing_1'][0]
-        self.assertEqual(int(thing_1.attributes['value'].value), self.test_thing.pk)
+        self.assertInHTML(
+            '''
+            <input data-selectable-type="hidden" name="thing_1" id="id_thing_1"
+                type="hidden" value="{}" {} />
+            '''.format(self.test_thing.pk,
+                       'required' if hasattr(form, 'use_required_attribute') else ''),
+            rendered_form
+        )
 
 
 class SelectWidgetForm(forms.ModelForm):
 
     class Meta(object):
         model = OtherThing
+        fields = ('name', 'thing', )
         widgets = {
             'thing': AutoCompleteSelectWidget(lookup_class=ThingLookup)
         }
@@ -166,6 +192,7 @@ class ComboboxSelectWidgetForm(forms.ModelForm):
 
     class Meta(object):
         model = OtherThing
+        fields = ('name', 'thing', )
         widgets = {
             'thing': AutoComboboxSelectWidget(lookup_class=ThingLookup)
         }
@@ -231,6 +258,7 @@ class ManyThingForm(forms.ModelForm):
 
     class Meta(object):
         model = ManyThing
+        fields = ('name', 'things', )
 
 
 class FuncManytoManyMultipleSelectTestCase(BaseSelectableTestCase):
@@ -316,6 +344,15 @@ class FuncManytoManyMultipleSelectTestCase(BaseSelectableTestCase):
         form = ManyThingForm(data=data)
         self.assertTrue(form.is_valid(), str(form.errors))
 
+    def test_render_form(self):
+        thing_1 = self.create_thing()
+        manything = ManyThing.objects.create(name='Foo')
+        manything.things.add(thing_1)
+        form = ManyThingForm(instance=manything)
+        rendered = form.as_p()
+        self.assertIn('title="{0}"'.format(thing_1.name),
+                      rendered)
+
 
 class SimpleForm(forms.Form):
     "Non-model form usage."
index c0042fb0282ca60aa3777f2849b1a40d2701ebed..74af539e9b7d5d58e2b20a07feb27cb7d1027dc2 100644 (file)
@@ -23,8 +23,8 @@ class JqueryTagTestCase(BaseSelectableTestCase):
         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')
+        self.assertJQueryVersion(result, '1.12.4')
+        self.assertUIVersion(result, '1.11.4')
 
     def test_render_jquery_version(self):
         "Render template tag with specified jQuery version."
@@ -82,7 +82,7 @@ class ThemeTagTestCase(BaseSelectableTestCase):
         template = Template("{% load selectable_tags %}{% include_ui_theme %}")
         context = Context({})
         result = template.render(context)
-        self.assertUICSS(result, 'base', '1.8.23')
+        self.assertUICSS(result, 'smoothness', '1.11.4')
 
     def test_render_version(self):
         "Render template tag with alternate version."
@@ -90,7 +90,7 @@ class ThemeTagTestCase(BaseSelectableTestCase):
         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'
@@ -104,12 +104,12 @@ class ThemeTagTestCase(BaseSelectableTestCase):
         template = Template("{% load selectable_tags %}{% include_ui_theme 'ui-lightness' %}")
         context = Context({})
         result = template.render(context)
-        self.assertUICSS(result, 'ui-lightness', '1.8.23')
-        
+        self.assertUICSS(result, 'ui-lightness', '1.11.4')
+
     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')
+        self.assertUICSS(result, theme, '1.11.4')
index 4dc20c41819eb897ca27aa4d216c042fb1465996..0bb65597c097405441f2e41298d4d36396f68489 100644 (file)
@@ -4,9 +4,10 @@ import json
 
 from django.conf import settings
 from django.core.urlresolvers import reverse
+from django.test import override_settings
 
 from . import ThingLookup
-from .base import BaseSelectableTestCase, PatchSettingsMixin
+from .base import BaseSelectableTestCase
 
 
 __all__ = (
@@ -14,7 +15,8 @@ __all__ = (
 )
 
 
-class SelectableViewTest(PatchSettingsMixin, BaseSelectableTestCase):
+@override_settings(SELECTABLE_MAX_LIMIT=25)
+class SelectableViewTest(BaseSelectableTestCase):
 
     def setUp(self):
         super(SelectableViewTest, self).setUp()
index 58a7d3ad36722ee6648baa684d5af02e5c09dedf..08e802468434b8b7f7ad9038758d03400841b91a 100644 (file)
@@ -3,11 +3,10 @@ import json
 from django import forms
 from django.utils.http import urlencode
 
+from . import Thing, ThingLookup
 from ..compat import urlparse
 from ..forms import widgets
-from . import Thing, ThingLookup
-from .base import BaseSelectableTestCase, parsed_inputs
-
+from .base import BaseSelectableTestCase, parsed_inputs, parsed_widget_attributes
 
 __all__ = (
     'AutoCompleteWidgetTestCase',
@@ -43,7 +42,7 @@ class WidgetTestMixin(object):
         An invalid lookup_class dotted path should raise an ImportError.
         """
         with self.assertRaises(ImportError):
-            self.__class__.widget_cls('this.is.an.invalid.path')
+            self.__class__.widget_cls('that.is.an.invalid.path')
 
     def test_dotted_path_wrong_type(self):
         """
@@ -58,9 +57,9 @@ class AutoCompleteWidgetTestCase(BaseSelectableTestCase, WidgetTestMixin):
     widget_cls = widgets.AutoCompleteWidget
     lookup_cls = ThingLookup
 
-    def test_build_attrs(self):
+    def test_rendered_attrs(self):
         widget = self.get_widget_instance()
-        attrs = widget.build_attrs()
+        attrs = parsed_widget_attributes(widget)
         self.assertTrue('data-selectable-url' in attrs)
         self.assertTrue('data-selectable-type' in attrs)
         self.assertTrue('data-selectable-allow-new' in attrs)
@@ -69,15 +68,15 @@ class AutoCompleteWidgetTestCase(BaseSelectableTestCase, WidgetTestMixin):
         params = {'active': 1}
         widget = self.get_widget_instance()
         widget.update_query_parameters(params)
-        attrs = widget.build_attrs()
+        attrs = parsed_widget_attributes(widget)
         url = attrs['data-selectable-url']
         parse = urlparse(url)
         query = parse.query
         self.assertEqual(query, urlencode(params))
 
-    def test_limit_paramter(self):
+    def test_limit_parameter(self):
         widget = self.get_widget_instance(limit=10)
-        attrs = widget.build_attrs()
+        attrs = parsed_widget_attributes(widget)
         url = attrs['data-selectable-url']
         parse = urlparse(url)
         query = parse.query
@@ -86,7 +85,7 @@ class AutoCompleteWidgetTestCase(BaseSelectableTestCase, WidgetTestMixin):
     def test_initial_query_parameters(self):
         params = {'active': 1}
         widget = self.get_widget_instance(query_params=params)
-        attrs = widget.build_attrs()
+        attrs = parsed_widget_attributes(widget)
         url = attrs['data-selectable-url']
         parse = urlparse(url)
         query = parse.query
@@ -96,7 +95,7 @@ class AutoCompleteWidgetTestCase(BaseSelectableTestCase, WidgetTestMixin):
         "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()
+        attrs = parsed_widget_attributes(widget)
         self.assertTrue('data-selectable-options' in attrs)
         self.assertEqual(attrs['data-selectable-options'], json.dumps(options))
 
@@ -115,8 +114,7 @@ class AutoCompleteSelectWidgetTestCase(BaseSelectableTestCase, WidgetTestMixin):
 
     def test_hidden_type(self):
         widget = self.get_widget_instance()
-        sub_widget = widget.widgets[1]
-        attrs = sub_widget.build_attrs()
+        attrs = parsed_widget_attributes(widget.widgets[1])
         self.assertTrue('data-selectable-type' in attrs)
         self.assertEqual(attrs['data-selectable-type'], 'hidden')
 
@@ -124,17 +122,15 @@ class AutoCompleteSelectWidgetTestCase(BaseSelectableTestCase, WidgetTestMixin):
         params = {'active': 1}
         widget = self.get_widget_instance()
         widget.update_query_parameters(params)
-        sub_widget = widget.widgets[0]
-        attrs = sub_widget.build_attrs()
+        attrs = parsed_widget_attributes(widget.widgets[0])
         url = attrs['data-selectable-url']
         parse = urlparse(url)
         query = parse.query
         self.assertEqual(query, urlencode(params))
 
-    def test_limit_paramter(self):
+    def test_limit_parameter(self):
         widget = self.get_widget_instance(limit=10)
-        sub_widget = widget.widgets[0]
-        attrs = sub_widget.build_attrs()
+        attrs = parsed_widget_attributes(widget.widgets[0])
         url = attrs['data-selectable-url']
         parse = urlparse(url)
         query = parse.query
@@ -143,8 +139,7 @@ class AutoCompleteSelectWidgetTestCase(BaseSelectableTestCase, WidgetTestMixin):
     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()
+        attrs = parsed_widget_attributes(widget.widgets[0])
         url = attrs['data-selectable-url']
         parse = urlparse(url)
         query = parse.query
@@ -154,8 +149,7 @@ class AutoCompleteSelectWidgetTestCase(BaseSelectableTestCase, WidgetTestMixin):
         "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()
+        attrs = parsed_widget_attributes(widget.widgets[0])
         self.assertTrue('data-selectable-options' in attrs)
         self.assertEqual(attrs['data-selectable-options'], json.dumps(options))
 
@@ -164,16 +158,16 @@ class AutoCompleteSelectWidgetTestCase(BaseSelectableTestCase, WidgetTestMixin):
         postdata = {'fruit': '1'}
         widget = self.get_widget_instance()
         widget_val = widget.value_from_datadict(postdata, [], 'fruit')
-        self.assertEquals(widget_val, '1')
+        self.assertEqual(widget_val, '1')
 
 
 class AutoComboboxWidgetTestCase(BaseSelectableTestCase, WidgetTestMixin):
     widget_cls = widgets.AutoComboboxWidget
     lookup_cls = ThingLookup
 
-    def test_build_attrs(self):
+    def test_rendered_attrs(self):
         widget = self.get_widget_instance()
-        attrs = widget.build_attrs()
+        attrs = parsed_widget_attributes(widget)
         self.assertTrue('data-selectable-url' in attrs)
         self.assertTrue('data-selectable-type' in attrs)
         self.assertTrue('data-selectable-allow-new' in attrs)
@@ -182,15 +176,15 @@ class AutoComboboxWidgetTestCase(BaseSelectableTestCase, WidgetTestMixin):
         params = {'active': 1}
         widget = self.get_widget_instance()
         widget.update_query_parameters(params)
-        attrs = widget.build_attrs()
+        attrs = parsed_widget_attributes(widget)
         url = attrs['data-selectable-url']
         parse = urlparse(url)
         query = parse.query
         self.assertEqual(query, urlencode(params))
 
-    def test_limit_paramter(self):
+    def test_limit_parameter(self):
         widget = self.get_widget_instance(limit=10)
-        attrs = widget.build_attrs()
+        attrs = parsed_widget_attributes(widget)
         url = attrs['data-selectable-url']
         parse = urlparse(url)
         query = parse.query
@@ -199,7 +193,7 @@ class AutoComboboxWidgetTestCase(BaseSelectableTestCase, WidgetTestMixin):
     def test_initial_query_parameters(self):
         params = {'active': 1}
         widget = self.get_widget_instance(query_params=params)
-        attrs = widget.build_attrs()
+        attrs = parsed_widget_attributes(widget)
         url = attrs['data-selectable-url']
         parse = urlparse(url)
         query = parse.query
@@ -209,7 +203,7 @@ class AutoComboboxWidgetTestCase(BaseSelectableTestCase, WidgetTestMixin):
         "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()
+        attrs = parsed_widget_attributes(widget)
         self.assertTrue('data-selectable-options' in attrs)
         self.assertEqual(attrs['data-selectable-options'], json.dumps(options))
 
@@ -228,8 +222,7 @@ class AutoComboboxSelectWidgetTestCase(BaseSelectableTestCase, WidgetTestMixin):
 
     def test_hidden_type(self):
         widget = self.get_widget_instance()
-        sub_widget = widget.widgets[1]
-        attrs = sub_widget.build_attrs()
+        attrs = parsed_widget_attributes(widget.widgets[1])
         self.assertTrue('data-selectable-type' in attrs)
         self.assertEqual(attrs['data-selectable-type'], 'hidden')
 
@@ -237,17 +230,15 @@ class AutoComboboxSelectWidgetTestCase(BaseSelectableTestCase, WidgetTestMixin):
         params = {'active': 1}
         widget = self.get_widget_instance()
         widget.update_query_parameters(params)
-        sub_widget = widget.widgets[0]
-        attrs = sub_widget.build_attrs()
+        attrs = parsed_widget_attributes(widget.widgets[0])
         url = attrs['data-selectable-url']
         parse = urlparse(url)
         query = parse.query
         self.assertEqual(query, urlencode(params))
 
-    def test_limit_paramter(self):
+    def test_limit_parameter(self):
         widget = self.get_widget_instance(limit=10)
-        sub_widget = widget.widgets[0]
-        attrs = sub_widget.build_attrs()
+        attrs = parsed_widget_attributes(widget.widgets[0])
         url = attrs['data-selectable-url']
         parse = urlparse(url)
         query = parse.query
@@ -256,8 +247,7 @@ class AutoComboboxSelectWidgetTestCase(BaseSelectableTestCase, WidgetTestMixin):
     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()
+        attrs = parsed_widget_attributes(widget.widgets[0])
         url = attrs['data-selectable-url']
         parse = urlparse(url)
         query = parse.query
@@ -267,8 +257,7 @@ class AutoComboboxSelectWidgetTestCase(BaseSelectableTestCase, WidgetTestMixin):
         "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()
+        attrs = parsed_widget_attributes(widget.widgets[0])
         self.assertTrue('data-selectable-options' in attrs)
         self.assertEqual(attrs['data-selectable-options'], json.dumps(options))
 
@@ -283,8 +272,7 @@ class AutoCompleteSelectMultipleWidgetTestCase(BaseSelectableTestCase, WidgetTes
 
     def test_multiple_attr(self):
         widget = self.get_widget_instance()
-        sub_widget = widget.widgets[0]
-        attrs = sub_widget.build_attrs()
+        attrs = parsed_widget_attributes(widget.widgets[0])
         self.assertTrue('data-selectable-multiple' in attrs)
         self.assertEqual(attrs['data-selectable-multiple'], 'true')
 
@@ -294,8 +282,7 @@ class AutoCompleteSelectMultipleWidgetTestCase(BaseSelectableTestCase, WidgetTes
 
     def test_hidden_type(self):
         widget = self.get_widget_instance()
-        sub_widget = widget.widgets[1]
-        attrs = sub_widget.build_attrs()
+        attrs = parsed_widget_attributes(widget.widgets[1])
         self.assertTrue('data-selectable-type' in attrs)
         self.assertEqual(attrs['data-selectable-type'], 'hidden-multiple')
 
@@ -325,31 +312,32 @@ class AutoCompleteSelectMultipleWidgetTestCase(BaseSelectableTestCase, WidgetTes
         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)
+        qs_val = Thing.objects.filter(pk__in=[t1.pk, t2.pk])
         rendered_value = widget.render('field_name', qs_val)
         inputs = parsed_inputs(rendered_value)
         found_values = []
+        found_titles = []
         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])
+            found_titles.append(field.attributes['title'].value)
+            found_values.append(field.attributes['value'].value)
+        self.assertListEqual(found_values, [str(t1.pk), str(t2.pk)])
+        self.assertListEqual(found_titles, [t1.name, t2.name])
 
     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()
+        attrs = parsed_widget_attributes(widget.widgets[0])
         url = attrs['data-selectable-url']
         parse = urlparse(url)
         query = parse.query
         self.assertEqual(query, urlencode(params))
 
-    def test_limit_paramter(self):
+    def test_limit_parameter(self):
         widget = self.get_widget_instance(limit=10)
-        sub_widget = widget.widgets[0]
-        attrs = sub_widget.build_attrs()
+        attrs = parsed_widget_attributes(widget.widgets[0])
         url = attrs['data-selectable-url']
         parse = urlparse(url)
         query = parse.query
@@ -358,8 +346,7 @@ class AutoCompleteSelectMultipleWidgetTestCase(BaseSelectableTestCase, WidgetTes
     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()
+        attrs = parsed_widget_attributes(widget.widgets[0])
         url = attrs['data-selectable-url']
         parse = urlparse(url)
         query = parse.query
@@ -369,8 +356,7 @@ class AutoCompleteSelectMultipleWidgetTestCase(BaseSelectableTestCase, WidgetTes
         "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()
+        attrs = parsed_widget_attributes(widget.widgets[0])
         self.assertTrue('data-selectable-options' in attrs)
         self.assertEqual(attrs['data-selectable-options'], json.dumps(options))
 
@@ -385,8 +371,7 @@ class AutoComboboxSelectMultipleWidgetTestCase(BaseSelectableTestCase, WidgetTes
 
     def test_multiple_attr(self):
         widget = self.get_widget_instance()
-        sub_widget = widget.widgets[0]
-        attrs = sub_widget.build_attrs()
+        attrs = parsed_widget_attributes(widget.widgets[0])
         self.assertTrue('data-selectable-multiple' in attrs)
         self.assertEqual(attrs['data-selectable-multiple'], 'true')
 
@@ -396,8 +381,7 @@ class AutoComboboxSelectMultipleWidgetTestCase(BaseSelectableTestCase, WidgetTes
 
     def test_hidden_type(self):
         widget = self.get_widget_instance()
-        sub_widget = widget.widgets[1]
-        attrs = sub_widget.build_attrs()
+        attrs = parsed_widget_attributes(widget.widgets[1])
         self.assertTrue('data-selectable-type' in attrs)
         self.assertEqual(attrs['data-selectable-type'], 'hidden-multiple')
 
@@ -427,7 +411,7 @@ class AutoComboboxSelectMultipleWidgetTestCase(BaseSelectableTestCase, WidgetTes
         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)
+        qs_val = Thing.objects.filter(pk__in=[t1.pk, t2.pk])
         rendered_value = widget.render('field_name', qs_val)
         inputs = parsed_inputs(rendered_value)
         found_values = []
@@ -441,17 +425,15 @@ class AutoComboboxSelectMultipleWidgetTestCase(BaseSelectableTestCase, WidgetTes
         params = {'active': 1}
         widget = self.get_widget_instance()
         widget.update_query_parameters(params)
-        sub_widget = widget.widgets[0]
-        attrs = sub_widget.build_attrs()
+        attrs = parsed_widget_attributes(widget.widgets[0])
         url = attrs['data-selectable-url']
         parse = urlparse(url)
         query = parse.query
         self.assertEqual(query, urlencode(params))
 
-    def test_limit_paramter(self):
+    def test_limit_parameter(self):
         widget = self.get_widget_instance(limit=10)
-        sub_widget = widget.widgets[0]
-        attrs = sub_widget.build_attrs()
+        attrs = parsed_widget_attributes(widget.widgets[0])
         url = attrs['data-selectable-url']
         parse = urlparse(url)
         query = parse.query
@@ -460,8 +442,7 @@ class AutoComboboxSelectMultipleWidgetTestCase(BaseSelectableTestCase, WidgetTes
     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()
+        attrs = parsed_widget_attributes(widget.widgets[0])
         url = attrs['data-selectable-url']
         parse = urlparse(url)
         query = parse.query
@@ -471,7 +452,6 @@ class AutoComboboxSelectMultipleWidgetTestCase(BaseSelectableTestCase, WidgetTes
         "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()
+        attrs = parsed_widget_attributes(widget.widgets[0])
         self.assertTrue('data-selectable-options' in attrs)
         self.assertEqual(attrs['data-selectable-options'], json.dumps(options))
index f747967f1e57469dda9646f7724de30cd7b34d4d..aa18f3e3a294edce057e010786239fde2ab90e7f 100644 (file)
@@ -1,5 +1,6 @@
 from django.http import HttpResponseNotFound, HttpResponseServerError
 
+
 def test_404(request):
     return HttpResponseNotFound()
 
index 02444b997c0feddd33a8ead993e87dfd6bd00cb5..eee1b68ac1ab780dc1f6381c0d97912b885a2ec1 100644 (file)
@@ -1,13 +1,6 @@
 from django.conf.urls import url
 
 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()
 
 
 urlpatterns = [
index ec4dc6d39140b7126db477b28ff0e418c9dd7e09..5d8ef623f7c5fd40e4a1ac65daa20991b2459d10 100644 (file)
@@ -1,6 +1,6 @@
 from __future__ import unicode_literals
 
-from django.http import HttpResponse, Http404
+from django.http import Http404
 
 from selectable.registry import registry
 
index 6f08d0e3e7d4475804244ad049d476c45fa48356..d4d5fd84976c9775a0e1d83478a7420e78479703 100644 (file)
@@ -1,8 +1,11 @@
-[bdist_wheel]
-universal = 1
+[coverage:run]
+branch = true
+omit = */tests/*, example/*, .tox/*, setup.py, runtests.py
+source = .
+
+[coverage:report]
+show_missing = true
 
-[egg_info]
-tag_build = 
-tag_date = 0
-tag_svn_revision = 0
 
+[bdist_wheel]
+universal = 1
index 77ed45758d6d36c4844073f25c3d93f8fc81bffe..fc2aa0489b5176b4904a35ecc2e3a2c74262d47c 100644 (file)
@@ -1,3 +1,4 @@
+#!/usr/bin/env python
 import os
 from setuptools import setup, find_packages
 
@@ -23,22 +24,21 @@ setup(
     license='BSD',
     description=' '.join(__import__('selectable').__doc__.splitlines()).strip(),
     classifiers=[
-        'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
+        'Development Status :: 5 - Production/Stable',
         'Intended Audience :: Developers',
+        'Framework :: Django',
         'License :: OSI Approved :: BSD License',
+        'Operating System :: OS Independent',
         'Programming Language :: Python',
-        'Programming Language :: Python :: 2.6',
         'Programming Language :: Python :: 2.7',
         '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',
+        'Programming Language :: Python :: 3.5',
+        'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
     ],
     long_description=read_file('README.rst'),
     test_suite="runtests.runtests",
-    tests_require=['mock'],
-    zip_safe=False, # because we're including media that Django needs
+    tests_require=['mock'],
+    zip_safe=False,  # because we're including media that Django needs
 )
diff --git a/dep/django-selectable/tox.ini b/dep/django-selectable/tox.ini
new file mode 100644 (file)
index 0000000..7e21929
--- /dev/null
@@ -0,0 +1,27 @@
+[tox]
+envlist = py{27,33}-django{17,18},py{27,34,35}-django{19,110,111},py35-django_master,docs
+
+[testenv]
+basepython =
+    py27: python2.7
+    py33: python3.3
+    py34: python3.4
+    py35: python3.5
+deps =
+    coverage>=4.0,<4.1
+    django17: Django>=1.7,<1.8
+    django18: Django>=1.8,<1.9
+    django19: Django>=1.9,<1.10
+    django110: Django>=1.10,<1.11
+    django111: Django>=1.11,<2.0
+    django_master: https://github.com/django/django/archive/master.tar.gz
+    py27: mock
+commands = coverage run runtests.py
+
+[testenv:docs]
+basepython = python3.5
+deps =
+    Sphinx
+    Django
+commands =
+    {envbindir}/sphinx-build -a -n -b html -d docs/_build/doctrees docs docs/_build/html