mirror of
https://github.com/postgres/pgweb.git
synced 2025-08-06 09:57:57 +00:00
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:
@ -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!
|
||||
|
@ -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,
|
||||
|
@ -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/>`_.
|
||||
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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')
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -16,8 +16,6 @@ the query string to retrive more values.
|
||||
Default: ``25``
|
||||
|
||||
|
||||
.. versionadded:: 0.6
|
||||
|
||||
.. _SELECTABLE_ESCAPED_KEYS:
|
||||
|
||||
SELECTABLE_ESCAPED_KEYS
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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'
|
||||
|
@ -1,7 +1,4 @@
|
||||
try:
|
||||
from django.apps import AppConfig
|
||||
except ImportError:
|
||||
AppConfig = object
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class SelectableConfig(AppConfig):
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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__ = (
|
||||
|
@ -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
|
||||
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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(" ")
|
||||
@ -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("");
|
||||
|
@ -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}
|
||||
|
@ -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 *
|
||||
|
@ -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
|
||||
|
@ -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!
|
||||
|
@ -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");
|
||||
});
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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)
|
||||
|
@ -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."
|
||||
|
@ -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')
|
||||
|
@ -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()
|
||||
|
@ -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))
|
||||
|
@ -1,5 +1,6 @@
|
||||
from django.http import HttpResponseNotFound, HttpResponseServerError
|
||||
|
||||
|
||||
def test_404(request):
|
||||
return HttpResponseNotFound()
|
||||
|
||||
|
@ -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 = [
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
)
|
||||
|
Reference in New Issue
Block a user