Import newer version of django-selectable

Sync up to the same version we have on the commitfest app, which will
also be required for eventual django 1.11 support.
This commit is contained in:
Magnus Hagander
2018-03-09 16:43:49 -05:00
parent 09f904f787
commit 6ffc1d4811
40 changed files with 809 additions and 471 deletions

View 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!

View 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,

View 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/>`_.

View 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.

View 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 %}
.. code-block:: html
Then in our lookup we will grab the state value and filter our results on it:
.. 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">

View 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.

View 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

View 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)
.. versionadded:: 0.7
`lookup_class`` may also be a dotted path.
.. code-block:: python
selectable.AutoCompleteSelectField(lookup_class='core.lookups.FruitLookup')
from django import forms
from selectable.forms import AutoCompleteSelectField
from .lookups import 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')

View 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

View 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

View 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)

View File

@ -16,8 +16,6 @@ the query string to retrive more values.
Default: ``25``
.. versionadded:: 0.6
.. _SELECTABLE_ESCAPED_KEYS:
SELECTABLE_ESCAPED_KEYS

View 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

View 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

View 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()

View 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'

View File

@ -1,7 +1,4 @@
try:
from django.apps import AppConfig
except ImportError:
AppConfig = object
from django.apps import AppConfig
class SelectableConfig(AppConfig):

View 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):

View 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

View 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__ = (

View 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

View 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):

View 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)

View File

@ -71,7 +71,8 @@
icons: {
primary: this.options.removeIcon
},
text: false
text: false,
disabled: this.disabled
},
button = $('<a>')
.attr('href', '#')
@ -131,7 +132,8 @@
icons: {
primary: this.options.comboboxIcon
},
text: false
text: false,
disabled: this.disabled
},
button = $("<a>")
.html("&nbsp;")
@ -157,6 +159,7 @@
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("");

View File

@ -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}

View 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 *

View 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))
@ -61,3 +48,50 @@ class BaseSelectableTestCase(TestCase):
class SimpleModelLookup(ModelLookup):
model = Thing
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

View 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!

View 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");
});

View 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):
"""

View 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)

View 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."

View 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."
@ -104,7 +104,7 @@ 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."
@ -112,4 +112,4 @@ class ThemeTagTestCase(BaseSelectableTestCase):
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')

View 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()

View 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))

View File

@ -1,5 +1,6 @@
from django.http import HttpResponseNotFound, HttpResponseServerError
def test_404(request):
return HttpResponseNotFound()

View 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 = [

View 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

View File

@ -1,8 +1,11 @@
[coverage:run]
branch = true
omit = */tests/*, example/*, .tox/*, setup.py, runtests.py
source = .
[coverage:report]
show_missing = true
[bdist_wheel]
universal = 1
[egg_info]
tag_build =
tag_date = 0
tag_svn_revision = 0

View 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
)