Thomas Güttler
Yuri Khrustalev
@SaeX
+Tam Huynh
+Raphael Merx
+Josh Addington
+Tobias Zanke
+Petr Dlouhy
Thanks for all of your work!
-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,
--- /dev/null
+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
.. 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
-----------------------------------
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::
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
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/>`_.
-
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.
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.
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``
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
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.
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.
==========================
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.
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.
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.
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:
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.
- 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>`_.
</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.
: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">
// 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>
# 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
# 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.
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.
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.
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
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
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:
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')
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)
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:
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
--------------------------------------
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
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
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:
--------------------------------------
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
{% 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.
{% 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.
.. 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::
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.
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
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)
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
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)
Default: ``25``
-.. versionadded:: 0.6
-
.. _SELECTABLE_ESCAPED_KEYS:
SELECTABLE_ESCAPED_KEYS
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
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.
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
: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
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
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
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
'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', ]
if __name__ == '__main__':
runtests()
-
"Auto-complete selection widgets using Django and jQuery UI."
-__version__ = '0.9.0'
+__version__ = '1.1.0'
default_app_config = 'selectable.apps.SelectableConfig'
-try:
- from django.apps import AppConfig
-except ImportError:
- AppConfig = object
+from django.apps import AppConfig
class SelectableConfig(AppConfig):
"Base classes for lookup creation."
from __future__ import unicode_literals
-import json
import operator
import re
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
)
-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."
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):
'''
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."
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
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):
"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
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__ = (
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 _
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
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
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__ = (
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)
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
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()
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
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):
"""
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):
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)
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)
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(" ")
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("");
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}
def __str__(self):
return self.name
+ class Meta:
+ ordering = ['id']
+
@python_2_unicode_compatible
class OtherThing(models.Model):
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 *
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):
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))
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
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!
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");
$('#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 () {
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");
});
$('#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 () {
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");
});
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");
});
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");
});
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):
"""
-from django.conf import settings
-
from ..forms import BaseLookupForm
-from .base import BaseSelectableTestCase, PatchSettingsMixin
+from .base import BaseSelectableTestCase
__all__ = (
)
-class BaseLookupFormTestCase(PatchSettingsMixin, BaseSelectableTestCase):
+class BaseLookupFormTestCase(BaseSelectableTestCase):
def get_valid_data(self):
data = {
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):
"""
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):
"""
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):
"""
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)
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__ = (
class Meta(object):
model = OtherThing
+ fields = ('name', 'thing', )
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)
}
class Meta(object):
model = OtherThing
+ fields = ('name', 'thing', )
widgets = {
'thing': AutoComboboxSelectWidget(lookup_class=ThingLookup)
}
class Meta(object):
model = ManyThing
+ fields = ('name', 'things', )
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."
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."
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."
context = Context({})
result = template.render(context)
self.assertUICSS(result, 'base', '1.8.13')
-
+
def test_variable_version(self):
"Render using version from content variable."
version = '1.8.13'
template = Template("{% load selectable_tags %}{% include_ui_theme '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')
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__ = (
)
-class SelectableViewTest(PatchSettingsMixin, BaseSelectableTestCase):
+@override_settings(SELECTABLE_MAX_LIMIT=25)
+class SelectableViewTest(BaseSelectableTestCase):
def setUp(self):
super(SelectableViewTest, self).setUp()
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',
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):
"""
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)
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
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
"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))
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')
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
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
"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))
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)
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
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
"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))
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')
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
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
"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))
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')
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')
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
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
"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))
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')
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')
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 = []
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
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
"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))
from django.http import HttpResponseNotFound, HttpResponseServerError
+
def test_404(request):
return HttpResponseNotFound()
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 = [
from __future__ import unicode_literals
-from django.http import HttpResponse, Http404
+from django.http import Http404
from selectable.registry import registry
-[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
+#!/usr/bin/env python
import os
from setuptools import setup, find_packages
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
)
--- /dev/null
+[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