Compare commits

...
104 Commits
Author SHA1 Message Date
Johannes Hoppe b46f69f166 Add i18n support 2017-07-16 17:30:42 +02:00
Johannes Hoppe b88e26bfa2 Switch to Chrome headless for selenium tests 2017-07-16 12:35:11 +02:00
pyup-bot 78c54f2c47 Update pytest from 3.1.2 to 3.1.3 2017-07-08 14:54:21 +02:00
Yuval Adam 018701680b Update ArrayField tagging widget documentation
This change reflects the new widget rendering APIs in Django 1.11
and enables the tagging widget to correctly show existing tags
2017-07-03 15:21:52 +02:00
pyup-bot d4668e3125 Update selenium from 3.4.2 to 3.4.3 2017-07-03 11:04:48 +02:00
pyup-bot bc0eab96e0 Update sphinx from 1.6.2 to 1.6.3 2017-07-03 11:04:33 +02:00
pyup-bot 6d2bd1cbd3 Update pytest-rerunfailures from 2.1.0 to 2.2 2017-07-03 11:04:21 +02:00
pyup-bot 9d215f6efe Update pytest from 3.1.1 to 3.1.2 2017-06-11 10:38:32 +02:00
pyup-bot 5741279d0e Update py from 1.4.33 to 1.4.34 2017-06-09 11:46:26 +02:00
pyup-bot 12b0d472b8 Update isort from 4.2.13 to 4.2.15 2017-06-09 11:25:51 +02:00
pyup-bot 74d2266817 Update isort from 4.2.9 to 4.2.13 2017-06-04 12:31:29 +02:00
pyup-bot c46fd5a5ec Update isort from 4.2.5 to 4.2.9 2017-06-01 17:38:59 +02:00
pyup-bot ad938c6008 Update pytest from 3.1.0 to 3.1.1 2017-06-01 17:37:52 +02:00
pyup-bot 2c1ae2df92 Update sphinx from 1.6.1 to 1.6.2 2017-05-29 14:57:15 +02:00
Johannes Hoppe 6260226ae5 Remove PhantomJS test runs 2017-05-25 12:09:07 +02:00
pyup-bot a5bb222ea8 Update pytest from 3.0.7 to 3.1.0 2017-05-23 09:14:38 +02:00
pyup-bot f12e624cb8 Update selenium from 3.4.1 to 3.4.2 2017-05-19 10:04:30 +02:00
pyup-bot c0d6fcb663 Update sphinx from 1.5.6 to 1.6.1 2017-05-17 11:54:37 +02:00
pyup-bot 6dc2f3eb19 Update coverage from 4.4 to 4.4.1 2017-05-17 11:54:31 +02:00
Johannes Hoppe 26f2a36dc6 Update tests suite for wait for browser to be loaded 2017-05-17 11:54:16 +02:00
pyup-bot d6180e05fc Update selenium from 3.3.3 to 3.4.1 2017-05-17 11:54:16 +02:00
Johannes Hoppe 67b7e755fe Add reruns for flaky selenium tests 2017-05-17 11:53:25 +02:00
Johannes Hoppe 4147cac2f8 Add 100ms implicit wait time to selenium driver 2017-05-17 11:53:25 +02:00
Johannes Hoppe 55e481470e Update selenium drivers 2017-05-17 11:53:25 +02:00
pyup-bot 7297ed7fde Update sphinx from 1.5.5 to 1.5.6 2017-05-15 10:48:29 +02:00
pyup-bot 90be63441b Update coverage from 4.3.4 to 4.4 2017-05-08 19:32:09 +02:00
pyup-bot d6e056c009 Update django-redis from 4.7.0 to 4.8.0 2017-04-25 17:23:30 +02:00
pyup-bot a910581f7a Update pydocstyle from 1.1.1 to 2.0.0 2017-04-19 11:30:11 +02:00
Alexey Ruzin 1931a98240 Chained select2 2017-04-18 10:57:33 +02:00
Johannes Hoppe 5840622794 Add support for Django 1.11
Drop support for Django 1.9
2017-04-14 14:09:23 +02:00
pyup-bot dc6e87e7ba Update selenium from 3.3.1 to 3.3.3 2017-04-07 15:02:11 +02:00
pyup-bot 71a836c892 Update sphinx from 1.5.4 to 1.5.5 2017-04-04 11:18:10 +02:00
pyup-bot 735d5931d6 Update jinja2 from 2.9.5 to 2.9.6 2017-04-03 15:48:09 +02:00
pyup-bot 9cc0a9d62d Update sphinx from 1.5.3 to 1.5.4 2017-04-03 14:30:26 +02:00
pyup-bot dfdc626ad3 Update pytz from 2016.10 to 2017.2 2017-03-30 15:18:43 +02:00
pyup-bot 07e8029026 Update babel from 2.3.4 to 2.4.0 2017-03-27 09:41:41 +02:00
pyup-bot 1f9d564445 Update py from 1.4.32 to 1.4.33 2017-03-17 18:30:29 +01:00
pyup-bot 5099b8d44d Update pytest from 3.0.6 to 3.0.7 2017-03-15 10:52:18 +01:00
pyup-bot 8958940c43 Update selenium from 3.3.0 to 3.3.1 2017-03-13 20:55:49 +01:00
pyup-bot d6202c4108 Update selenium from 3.0.2 to 3.3.0 2017-03-08 14:00:49 +01:00
pyup-bot 0d560c08fc Update markupsafe from 0.23 to 1.0 2017-03-07 17:20:29 +01:00
pyup-bot 17dff07690 Update sphinx-rtd-theme from 0.2.2 to 0.2.4 2017-03-07 10:47:01 +01:00
pyup-bot c6d7054e16 Update sphinx-rtd-theme from 0.2.0 to 0.2.2 2017-03-04 12:54:11 +01:00
pyup-bot a89fedbe75 Update alabaster from 0.7.9 to 0.7.10 2017-03-02 14:30:11 +01:00
pyup-bot 0918cae3e7 Update sphinx-rtd-theme from 0.1.9 to 0.2.0 2017-03-02 14:29:52 +01:00
pyup-bot 8f6e33845e Update sphinx from 1.5.2 to 1.5.3 2017-02-27 16:03:07 +01:00
pyup.io bot 46a50f377b Update pygments from 2.1.3 to 2.2.0 (#316) 2017-02-23 10:05:12 +01:00
Johannes Hoppe 895a7e519a Reformat JS to match StandardJS style 2017-02-23 10:04:36 +01:00
Johannes Hoppe c235c2e3f0 Add standardjs linter 2017-02-23 10:04:36 +01:00
pyup-bot 9a658b3b0f Update sphinx from 1.5.1 to 1.5.2 2017-02-22 13:24:31 +01:00
pyup-bot efb120b47d Update pytest from 3.0.5 to 3.0.6 2017-02-22 13:24:04 +01:00
pyup-bot b34642dcef Update pyflakes from 1.4.0 to 1.5.0 2017-02-22 13:22:27 +01:00
pyup-bot 8b64b5d4fa Update mccabe from 0.5.3 to 0.6.1 2017-02-22 13:22:10 +01:00
pyup-bot 4e8b2c24a3 Update jinja2 from 2.8.1 to 2.9.5 2017-02-22 13:21:41 +01:00
Johannes Hoppe 23e91c8bb0 Fix flaky test 2017-02-22 13:21:30 +01:00
pyup-bot e88c0e059c Update flake8 from 3.2.1 to 3.3.0 2017-02-21 15:53:52 +01:00
pyup-bot 047ee28722 Update coverage from 4.3.1 to 4.3.4 2017-02-21 15:37:05 +01:00
Johannes Hoppe 6018b60fea Wait for form to be submitted 2017-02-21 15:07:19 +01:00
Johannes Hoppe f1e0752f2d Update gecko driver to 0.14.0 2017-02-21 15:07:19 +01:00
Johannes Hoppe 172ef79ffd Fix coverage source configuration 2017-02-21 15:07:19 +01:00
Johannes Hoppe 36a9ab7bab Fix pytest config 2017-02-21 15:07:19 +01:00
Johannes Hoppe 09110a5460 Fix syntax 2017-02-21 15:07:19 +01:00
pyup-bot ec1c81ad87 Update mccabe from 0.4.0 to 0.5.3 2017-02-21 15:07:19 +01:00
pyup-bot 3673e55195 Update pyflakes from 1.0.0 to 1.4.0 2017-02-21 15:07:19 +01:00
pyup-bot 937874127f Update pep8-naming from 0.3.3 to 0.4.1 2017-02-21 15:07:19 +01:00
pyup-bot 681ced9f15 Update coverage from 4.3 to 4.3.1 2017-02-21 15:07:19 +01:00
pyup-bot e375a58b4d Update flake8 from 2.5.2 to 3.2.1 2017-02-21 15:07:19 +01:00
pyup-bot b2609256f5 Update django-redis from 4.6.0 to 4.7.0 2017-02-21 15:07:19 +01:00
pyup-bot b676e563e2 Update docutils from 0.12 to 0.13.1 2017-02-21 15:07:19 +01:00
pyup-bot 65cfc12be3 Update pyenchant from 1.6.6 to 1.6.8 2017-02-21 15:07:19 +01:00
pyup-bot e6ace9a8bc Update babel from 2.2.0 to 2.3.4 2017-02-21 15:07:19 +01:00
pyup-bot c3a48de1a3 Update django-appconf from 1.0.1 to 1.0.2 2017-02-21 15:07:19 +01:00
pyup-bot 8a463752a0 Update pygments from 2.1 to 2.1.3 2017-02-21 15:07:19 +01:00
pyup-bot c364394450 Update isort from 4.2.2 to 4.2.5 2017-02-21 15:07:19 +01:00
pyup-bot 977f2c3e25 Update py from 1.4.31 to 1.4.32 2017-02-21 15:07:19 +01:00
pyup-bot aa15810d92 Update pytz from 2015.7 to 2016.10 2017-02-21 15:07:19 +01:00
pyup-bot 4a5c13a35f Update pytest-django from 2.9.1 to 3.1.2 2017-02-21 15:07:19 +01:00
pyup-bot 584e2e9f89 Update alabaster from 0.7.7 to 0.7.9 2017-02-21 15:07:19 +01:00
pyup-bot 33a7e03acf Update jinja2 from 2.8 to 2.8.1 2017-02-21 15:07:19 +01:00
pyup-bot 93dab02fd1 Update sphinx from 1.3.5 to 1.5.1 2017-02-21 15:07:19 +01:00
pyup-bot d364dc9630 Update sphinxcontrib-spelling from 2.1.2 to 2.3.0 2017-02-21 15:07:19 +01:00
pyup-bot 701d57ef31 Update selenium from 2.50.1 to 3.0.2 2017-02-21 15:07:19 +01:00
pyup-bot 71c6e1d9c3 Update pytest from 2.8.7 to 3.0.5 2017-02-21 15:07:19 +01:00
Tim Tisdall 7c2a898cfa update CHANGELOG 2017-01-23 20:01:03 +01:00
Johannes Hoppe 6180846345 Update test suite
- Add tox
- Add python 3.6
- Drop python 3.4
- Add chrome and gecko driver
- Switch tests from memcache to redis
2016-12-28 21:02:32 +01:00
Tim Tisdall 9430a950c2 refer to the correct class 2016-12-05 14:54:11 +01:00
Johannes Hoppe 1f9b094b67 v5.8.10 2016-12-05 14:52:54 +01:00
Tim Tisdall 15dbbf41a7 retain order of choices
If the choices are put into a `set` then the order is lost.  Regardless, I don't think a `set` is very useful here as this won't even deal with duplicate choice values/ids (ex. `(('1', 'one'), ('1', 'also one'))`)
2016-12-05 14:47:49 +01:00
Raony Guimarães fb3bc19595 Fix Test suite for Django 1.10+
* Fix template dir

This is necessary to make your example work!

* Fix Jquery2 url
2016-11-04 15:52:29 +01:00
Johannes Hoppe f03d7d44a8 Add tests for Django 1.10 release 2016-08-25 10:05:22 +02:00
Johannes Hoppe e3c6919bbd Fix tests 2016-08-15 09:07:57 +02:00
Johannes Hoppe 9f5644b479 Upgrade select2 to 4.0.3 2016-08-15 08:58:34 +02:00
Johannes Hoppe c797e59584 Bump version number to 5.8.7 2016-08-14 10:41:31 +02:00
Josef Kolář 72616f8276 ModelSelect2Mixin - avoid queryset evaluation (#287) 2016-08-14 10:38:57 +02:00
Johannes Hoppe 99aed19308 Write changelog bump version to 5.8.6 2016-06-19 12:29:45 +02:00
Jonas Haag 74bf15ce48 Drop six requirement, use django.utils.six instead (#282)
* Drop six requirement, use django.utils.six instead

* Drop six requirement, use django.utils.six instead

* Make isort happy
2016-06-19 12:27:49 +02:00
Johannes Hoppe 57dfd53d55 Bump version number 2016-05-23 16:18:39 +02:00
Johannes Hoppe f943fe07f5 Fixes #256 -- Don't render empty option for multiselect (#275) 2016-05-23 16:17:31 +02:00
AppleGrew a6a12d7392 Doc link's domain name updated 2016-04-28 13:12:16 +05:30
Johannes Hoppe f579f4af35 Adds travis-ci auto deploy 2016-04-22 15:25:04 +02:00
Julian 0b3abb8f1f Fixes minor documentation typo 2016-04-03 13:20:53 +02:00
Jônatas Castro 01c8979100 Fixes #238 -- Changed Token Separators
When Django was parsing the separator attribute, it was comming with 'u' char from unicode.
So the trick was to put the value in a String.
Solved https://github.com/applegrew/django-select2/issues/238

Closes #268
2016-03-10 15:34:30 +01:00
Damir Arbula f6ee1f57db Add exception handling for cPickle.PicklingError to widget caching
Closes #265
2016-03-08 16:30:47 +01:00
Johannes Hoppe 894a251be3 Switch coverage badge to SVG 2016-03-01 10:16:11 +01:00
28 changed files with 789 additions and 192 deletions
+4 -3
View File
@@ -1,6 +1,3 @@
testapp/tt.py
*.pyc
Django_Select2.egg-info
@@ -19,3 +16,7 @@ docs/_build
env/
venv/
.cache/
.tox/
geckodriver.log
ghostdriver.log
.coverage
+57 -29
View File
@@ -1,44 +1,72 @@
language: python
sudo: false
dist: trusty
cache:
- apt
- pip
- apt
- pip
services:
- memcached
python:
- "2.7"
- "3.4"
- "3.5"
- redis
addons:
chrome: stable
apt:
packages:
- python3-enchant
- python2-enchant
- graphviz
- python3-enchant
- python-enchant
- graphviz
python:
- "2.7"
- "3.5"
- "3.6"
env:
global:
- DISPLAY=:99.0
matrix:
- DJANGO="Django<1.9,>=1.8"
- DJANGO="Django<1.10,>=1.9"
- DJANGO="-e git+https://github.com/django/django.git@master#egg=Django"
- DJANGO=18
- DJANGO=110
- DJANGO=111
- DJANGO=master
- TOXENV=qa
- TOXENV=docs
matrix:
fast_finish: true
allow_failures:
- env: DJANGO="-e git+https://github.com/django/django.git@master#egg=Django"
- env: DJANGO=master
exclude:
- env: DJANGO=master
python: "2.7"
- env: TOXENV=qa
python: "2.7"
- env: TOXENV=qa
python: "3.5"
- env: TOXENV=docs
python: "2.7"
- env: TOXENV=docs
python: "3.5"
install:
- pip install --upgrade pip
- pip install -r requirements_dev.txt
- if [[ $TRAVIS_PYTHON_VERSION == 2* ]] || [[ $TRAVIS_PYTHON_VERSION == pypy ]]; then pip install python-memcached; fi
- if [[ $TRAVIS_PYTHON_VERSION == 3* ]] || [[ $TRAVIS_PYTHON_VERSION == pypy3 ]]; then pip install python3-memcached; fi
- pip install $DJANGO
- pip install coveralls
- sh -e /etc/init.d/xvfb start
- pip install --upgrade pip tox
- pip install -U coveralls
before_script:
- mkdir bin
- export CHROME_DRIVER_VERSION=`curl https://chromedriver.storage.googleapis.com/LATEST_RELEASE`
- curl -O https://chromedriver.storage.googleapis.com/${CHROME_DRIVER_VERSION}/chromedriver_linux64.zip
- unzip chromedriver_linux64.zip -d bin
- PATH=$PATH:$PWD/bin
- nvm install --lts
- npm install -g standard
- |
if [[ -z $TOXENV ]]; then
export TOXENV=py$(echo $TRAVIS_PYTHON_VERSION | sed -e 's/\.//g')-dj$DJANGO
fi
- echo $TOXENV
script:
- isort --check-only --recursive --diff .
- flake8 --jobs=2 .
- pep257 --explain --source --count django_select2
- (cd docs; make spelling)
- coverage run --source=django_select2 -m py.test
- tox -e $TOXENV
- standard
after_success:
- coveralls
- coveralls
deploy:
provider: pypi
user: codingjoe
password:
secure: fEP9K7y0ZF9fRvQEUN4kgPXQEZvi3Cx3ikUebG2UM/2uhcaUQm0+KpgZ2S+lvOTYcBnNgzPzFsVIZMcVcTxwIKuQDEMq9y2eop2hnisb9KXsIm9qPYSzOnRm74VuiOt4hNOZMe0qVBK2cO3vC9NDXuzdI8A0JynJhthfl4t+kFM=
on:
tags: true
distributions: sdist bdist_wheel
repo: applegrew/django-select2
+23
View File
@@ -1,6 +1,29 @@
Changelog Summary
=================
### v5.10.0
* Add support for dependent select fields [321](github.com/applegrew/django-select2/pull/321/).
### v5.9.0
* Add support for Django 1.11 LTS
* Drop support for Django 1.9
### v5.8.10
* Fixes tests for Django 1.10+
* retain order of choices [299](https://github.com/applegrew/django-select2/pull/299)
### v5.8.9
* Fixes tests. No functional change from 5.8.8.
### v5.8.8
* Upgrade Select2 to version 4.0.3
### v5.8.7
* Fix #285 -- Do not evaluate queryset in `render_options`.
### v5.8.3
* Remove six dependency -- User django.utils.six instead.
### v5.8.2
* Fixes #260 -- Fixes bug in render choices
+3
View File
@@ -3,3 +3,6 @@ recursive-include django_select2/static *.*
recursive-exclude django_select2/static .DS_Store
exclude requirements_dev.txt
prune tests
[bdist_wheel]
universal=1
+2 -2
View File
@@ -3,7 +3,7 @@ Django-Select2
[![PyPi Version](https://img.shields.io/pypi/v/Django-Select2.svg)](https://pypi.python.org/pypi/Django-Select2/)
[![Build Status](https://travis-ci.org/applegrew/django-select2.svg?branch=master)](https://travis-ci.org/applegrew/django-select2)
[![Test Coverage](https://coveralls.io/repos/applegrew/django-select2/badge.png?branch=master)](https://coveralls.io/r/applegrew/django-select2)
[![Test Coverage](https://coveralls.io/repos/applegrew/django-select2/badge.svg?branch=master)](https://coveralls.io/r/applegrew/django-select2)
[![GitHub license](https://img.shields.io/badge/license-APL2-blue.svg)](https://raw.githubusercontent.com/applegrew/django-select2/master/LICENSE.txt)
[![Join the chat at https://gitter.im/applegrew/django-select2](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/applegrew/django-select2?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
@@ -54,7 +54,7 @@ Major changes:
## Documentation
Documentation available at http://django-select2.readthedocs.org/.
Documentation available at http://django-select2.readthedocs.io/.
## External Dependencies
+2 -2
View File
@@ -2,11 +2,11 @@
"""
This is a Django_ integration of Select2_.
The app includes Select2 driven Django Widgets and Form Fields.
The application includes Select2 driven Django Widgets and Form Fields.
.. _Django: https://www.djangoproject.com/
.. _Select2: http://ivaynberg.github.com/select2/
"""
__version__ = "5.8.2"
__version__ = "5.11.0"
+87 -3
View File
@@ -43,12 +43,12 @@ class Select2Conf(AppConf):
"""
CACHE_PREFIX = 'select2_'
"""
If you caching backend doesn't support multiple databases
If you caching backend does not support multiple databases
you can isolate select2 using the cache prefix setting.
It has set `select2_` as a default value, which you can change if needed.
"""
JS = '//cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/js/select2.min.js'
JS = '//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/select2.min.js'
"""
The URI for the Select2 JS file. By default this points to the Cloudflare CDN.
@@ -61,7 +61,7 @@ class Select2Conf(AppConf):
develop without an Internet connection.
"""
CSS = '//cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/css/select2.min.css'
CSS = '//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/css/select2.min.css'
"""
The URI for the Select2 CSS file. By default this points to the Cloudflare CDN.
@@ -74,5 +74,89 @@ class Select2Conf(AppConf):
develop without an Internet connection.
"""
I18N_PATH = '//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/i18n'
"""
The base URI for the Select2 i18n files. By default this points to the Cloudflare CDN.
If you want to select the version of the JS library used, or want to serve it from
the local 'static' resources, add a line to your settings.py like so::
SELECT2_JS = 'assets/js/i18n'
.. tip:: Change this setting to a local asset in your development environment to
develop without an Internet connection.
"""
I18N_AVAILABLE_LANGUAGES = [
'ar',
'az',
'bg',
'ca',
'cs',
'da',
'de',
'el',
'en',
'es',
'et',
'eu',
'fa',
'fi',
'fr',
'gl',
'he',
'hi',
'hr',
'hu',
'id',
'is',
'it',
'ja',
'km',
'ko',
'lt',
'lv',
'mk',
'ms',
'nb',
'nl',
'pl',
'pt-BR',
'pt',
'ro',
'ru',
'sk',
'sr-Cyrl',
'sr',
'sv',
'th',
'tr',
'uk',
'vi',
'zh-CN',
'zh-TW',
]
"""
List of available translations.
List of ISO 639-1 language codes that are supported by Select2.
If currently set language code (e.g. using the HTTP ``Accept-Language`` header)
is in this list, Django-Select2 will use the language code to create load
the proper translation.
The full path for the language file consists of::
from django.utils import translations
full_path = "{i18n_path}/{language_code}.js".format(
i18n_path=settings.DJANGO_SELECT2_I18N,
language_code=translations.get_language(),
)
``settings.DJANGO_SELECT2_I18N`` refers to :attr:`.I18N_PATH`.
"""
class Meta:
"""Prefix for all Django-Select2 settings."""
prefix = 'SELECT2'
+102 -26
View File
@@ -54,14 +54,20 @@ from pickle import PicklingError
from django import forms
from django.core import signing
from django.core.urlresolvers import reverse
from django.db.models import Q
from django.forms.models import ModelChoiceIterator
from django.utils.encoding import force_text
from django.utils.six.moves.cPickle import PicklingError as cPicklingError
from django.utils.translation import get_language
from .cache import cache
from .conf import settings
try:
from django.urls import reverse
except ImportError:
from django.core.urlresolvers import reverse
class Select2Mixin(object):
"""
@@ -72,9 +78,9 @@ class Select2Mixin(object):
form media.
"""
def build_attrs(self, extra_attrs=None, **kwargs):
def build_attrs(self, *args, **kwargs):
"""Add select2 data attributes."""
attrs = super(Select2Mixin, self).build_attrs(extra_attrs=extra_attrs, **kwargs)
attrs = super(Select2Mixin, self).build_attrs(*args, **kwargs)
if self.is_required:
attrs.setdefault('data-allow-clear', 'false')
else:
@@ -88,9 +94,15 @@ class Select2Mixin(object):
attrs['class'] = 'django-select2'
return attrs
def optgroups(self, name, value, attrs=None):
"""Add empty option for clearable selects."""
if not self.is_required and not self.allow_multiple_selected:
self.choices = list(chain([('', '')], self.choices))
return super(Select2Mixin, self).optgroups(name, value, attrs=attrs)
def render_options(self, *args, **kwargs):
"""Render options including an empty one, if the field is not required."""
output = '<option></option>' if not self.is_required else ''
output = '<option value=""></option>' if not self.is_required and not self.allow_multiple_selected else ''
output += super(Select2Mixin, self).render_options(*args, **kwargs)
return output
@@ -101,8 +113,10 @@ class Select2Mixin(object):
.. Note:: For more information visit
https://docs.djangoproject.com/en/1.8/topics/forms/media/#media-as-a-dynamic-property
"""
i18n_file = '%s/%s.js' % (settings.SELECT2_I18N_PATH, get_language())
i18n_file = (i18n_file,) if get_language() in settings.SELECT2_I18N_AVAILABLE_LANGUAGES else ()
return forms.Media(
js=(settings.SELECT2_JS, 'django_select2/django_select2.js'),
js=(settings.SELECT2_JS,) + i18n_file + ('django_select2/django_select2.js',),
css={'screen': (settings.SELECT2_CSS,)}
)
@@ -112,12 +126,12 @@ class Select2Mixin(object):
class Select2TagMixin(object):
"""Mixin to add select2 tag functionality."""
def build_attrs(self, extra_attrs=None, **kwargs):
def build_attrs(self, *args, **kwargs):
"""Add select2's tag attributes."""
self.attrs.setdefault('data-minimum-input-length', 1)
self.attrs.setdefault('data-tags', 'true')
self.attrs.setdefault('data-token-separators', [",", " "])
return super(Select2TagMixin, self).build_attrs(extra_attrs, **kwargs)
self.attrs.setdefault('data-token-separators', '[",", " "]')
return super(Select2TagMixin, self).build_attrs(*args, **kwargs)
class Select2Widget(Select2Mixin, forms.Select):
@@ -166,6 +180,12 @@ class Select2TagWidget(Select2TagMixin, Select2Mixin, forms.SelectMultiple):
values = super(MyWidget, self).value_from_datadict(data, files, name):
return ",".join(values)
def optgroups(self, name, value, attrs=None):
values = value[0].split(',') if value[0] else []
selected = set(values)
subgroup = [self.create_option(name, v, v, selected, i) for i, v in enumerate(values)]
return [(None, subgroup, 0)]
"""
pass
@@ -174,21 +194,37 @@ class Select2TagWidget(Select2TagMixin, Select2Mixin, forms.SelectMultiple):
class HeavySelect2Mixin(object):
"""Mixin that adds select2's AJAX options and registers itself on Django's cache."""
def __init__(self, **kwargs):
dependent_fields = {}
def __init__(self, attrs=None, choices=(), **kwargs):
"""
Return HeavySelect2Mixin.
Args:
data_view (str): URL pattern name
data_url (str): URL
dependent_fields (dict): Dictionary of dependent parent fields.
The value of the dependent field will be passed as to :func:`.filter_queryset`.
It can be used to further restrict the search results. For example, a city
widget could be dependent on a country.
Key is a name of a field in a form.
Value is a name of a field in a model (used in `queryset`).
"""
self.choices = choices
if attrs is not None:
self.attrs = attrs.copy()
else:
self.attrs = {}
self.data_view = kwargs.pop('data_view', None)
self.data_url = kwargs.pop('data_url', None)
dependent_fields = kwargs.pop('dependent_fields', None)
if dependent_fields is not None:
self.dependent_fields = dict(dependent_fields)
if not (self.data_view or self.data_url):
raise ValueError('You must ether specify "data_view" or "data_url".')
self.userGetValTextFuncName = kwargs.pop('userGetValTextFuncName', 'null')
super(HeavySelect2Mixin, self).__init__(**kwargs)
def get_url(self):
"""Return URL from instance or by reversing :attr:`.data_view`."""
@@ -196,9 +232,9 @@ class HeavySelect2Mixin(object):
return self.data_url
return reverse(self.data_view)
def build_attrs(self, extra_attrs=None, **kwargs):
def build_attrs(self, *args, **kwargs):
"""Set select2's AJAX attributes."""
attrs = super(HeavySelect2Mixin, self).build_attrs(extra_attrs=extra_attrs, **kwargs)
attrs = super(HeavySelect2Mixin, self).build_attrs(*args, **kwargs)
# encrypt instance Id
self.widget_id = signing.dumps(id(self))
@@ -208,6 +244,8 @@ class HeavySelect2Mixin(object):
attrs.setdefault('data-ajax--cache', "true")
attrs.setdefault('data-ajax--type', "GET")
attrs.setdefault('data-minimum-input-length', 2)
if self.dependent_fields:
attrs.setdefault('data-select2-dependent-fields', " ".join(self.dependent_fields))
attrs['class'] += ' django-select2-heavy'
return attrs
@@ -233,7 +271,7 @@ class HeavySelect2Mixin(object):
'widget': self,
'url': self.get_url(),
})
except (PicklingError, AttributeError):
except (PicklingError, cPicklingError, AttributeError):
msg = "You need to overwrite \"set_to_cache\" or ensure that %s is serialisable."
raise NotImplementedError(msg % self.__class__.__name__)
@@ -246,9 +284,9 @@ class HeavySelect2Mixin(object):
choices = chain(self.choices, choices)
else:
choices = self.choices
output = ['<option></option>' if not self.is_required else '']
output = ['<option value=""></option>' if not self.is_required and not self.allow_multiple_selected else '']
selected_choices = {force_text(v) for v in selected_choices}
choices = {(k, v) for k, v in choices if force_text(k) in selected_choices}
choices = [(k, v) for k, v in choices if force_text(k) in selected_choices]
for option_value, option_label in choices:
output.append(self.render_option(selected_choices, option_value, option_label))
return '\n'.join(output)
@@ -260,14 +298,14 @@ class HeavySelect2Widget(HeavySelect2Mixin, Select2Widget):
Usage example::
class MyWidget(HeavySelectWidget):
class MyWidget(HeavySelect2Widget):
data_view = 'my_view_name'
or::
class MyForm(forms.Form):
my_field = forms.ChoicesField(
widget=HeavySelectWidget(
widget=HeavySelect2Widget(
data_url='/url/to/json/response'
)
)
@@ -318,7 +356,7 @@ class ModelSelect2Mixin(object):
Args:
model (django.db.models.Model): Model to select choices from.
queryset (django.db.models.QuerySet): QuerySet to select choices from.
queryset (django.db.models.query.QuerySet): QuerySet to select choices from.
search_fields (list): List of model lookup strings.
max_results (int): Max. JsonResponse view page size.
@@ -348,14 +386,19 @@ class ModelSelect2Mixin(object):
'search_fields': self.search_fields,
'max_results': self.max_results,
'url': self.get_url(),
'dependent_fields': self.dependent_fields,
})
def filter_queryset(self, term, queryset=None):
def filter_queryset(self, term, queryset=None, **dependent_fields):
"""
Return QuerySet filtered by search_fields matching the passed term.
Args:
term (str): Search term
queryset (django.db.models.query.QuerySet): QuerySet to select choices from.
**dependent_fields: Dependent fields and their values. If you want to inherit
from ModelSelect2Mixin and later call to this method, be sure to pop
from kwargs everything if it is not a dependent field.
Returns:
QuerySet: Filtered QuerySet
@@ -370,6 +413,9 @@ class ModelSelect2Mixin(object):
for t in [t for t in term.split(' ') if not t == '']:
select &= reduce(lambda x, y: x | Q(**{y: t}), search_fields,
Q(**{search_fields[0]: t}))
if dependent_fields:
select &= Q(**dependent_fields)
return queryset.filter(select).distinct()
def get_queryset(self):
@@ -400,8 +446,38 @@ class ModelSelect2Mixin(object):
return self.search_fields
raise NotImplementedError('%s, must implement "search_fields".' % self.__class__.__name__)
def optgroups(self, name, value, attrs=None):
"""Return only selected options and set QuerySet from `ModelChoicesIterator`."""
default = (None, [], 0)
groups = [default]
has_selected = False
selected_choices = {force_text(v) for v in value}
if not self.is_required and not self.allow_multiple_selected:
default[1].append(self.create_option(name, '', '', False, 0))
if not isinstance(self.choices, ModelChoiceIterator):
return super(ModelSelect2Mixin, self).optgroups(name, value, attrs=attrs)
selected_choices = {
c for c in selected_choices
if c not in self.choices.field.empty_values
}
choices = (
(obj.pk, self.label_from_instance(obj))
for obj in self.choices.queryset.filter(pk__in=selected_choices)
)
for option_value, option_label in choices:
selected = (
force_text(option_value) in value and
(has_selected is False or self.allow_multiple_selected)
)
if selected is True and has_selected is False:
has_selected = True
index = len(default[1])
subgroup = default[1]
subgroup.append(self.create_option(name, option_value, option_label, selected_choices, index))
return groups
def render_options(self, *args):
"""Render only selected options and set QuerySet from :class:`ModelChoicesIterator`."""
"""Render only selected options and set QuerySet from :class:`ModelChoiceIterator`."""
try:
selected_choices, = args
except ValueError:
@@ -410,16 +486,16 @@ class ModelSelect2Mixin(object):
else:
choices = self.choices
selected_choices = {force_text(v) for v in selected_choices}
output = ['<option></option>' if not self.is_required else '']
output = ['<option value=""></option>' if not self.is_required and not self.allow_multiple_selected else '']
if isinstance(self.choices, ModelChoiceIterator):
if not self.queryset:
if self.queryset is None:
self.queryset = self.choices.queryset
selected_choices = {c for c in selected_choices
if c not in self.choices.field.empty_values}
choices = {(obj.pk, self.label_from_instance(obj))
for obj in self.choices.queryset.filter(pk__in=selected_choices)}
choices = [(obj.pk, self.label_from_instance(obj))
for obj in self.choices.queryset.filter(pk__in=selected_choices)]
else:
choices = {(k, v) for k, v in choices if force_text(k) in selected_choices}
choices = [(k, v) for k, v in choices if force_text(k) in selected_choices]
for option_value, option_label in choices:
output.append(self.render_option(selected_choices, option_value, option_label))
return '\n'.join(output)
@@ -1,48 +1,56 @@
(function ($) {
var init = function ($element, options) {
$element.select2(options)
}
var init = function ($element, options) {
$element.select2(options);
};
var initHeavy = function ($element, options) {
var settings = $.extend({
ajax: {
data: function (params) {
var result = {
term: params.term,
page: params.page,
field_id: $element.data('field_id')
}
var initHeavy = function ($element, options) {
var settings = $.extend({
ajax: {
data: function (params) {
return {
term: params.term,
page: params.page,
field_id: $element.data('field_id')
};
},
processResults: function (data, page) {
return {
results: data.results,
pagination: {
more: data.more
}
};
}
var dependentFields = $element.data('select2-dependent-fields')
if (dependentFields) {
dependentFields = dependentFields.trim().split(/\s+/)
$.each(dependentFields, function (i, dependentField) {
result[dependentField] = $('[name=' + dependentField + ']', $element.closest('form')).val()
})
}
return result
},
processResults: function (data, page) {
return {
results: data.results,
pagination: {
more: data.more
}
}, options);
}
}
}
}, options)
$element.select2(settings);
};
$element.select2(settings)
}
$.fn.djangoSelect2 = function (options) {
var settings = $.extend({}, options);
$.each(this, function (i, element) {
var $element = $(element);
if ($element.hasClass('django-select2-heavy')) {
initHeavy($element, settings);
} else {
init($element, settings);
}
});
return this;
};
$.fn.djangoSelect2 = function (options) {
var settings = $.extend({}, options)
$.each(this, function (i, element) {
var $element = $(element)
if ($element.hasClass('django-select2-heavy')) {
initHeavy($element, settings)
} else {
init($element, settings)
}
})
return this
}
$(function () {
$('.django-select2').djangoSelect2();
});
}(this.jQuery));
$(function () {
$('.django-select2').djangoSelect2()
})
}(this.jQuery))
+6 -1
View File
@@ -52,7 +52,12 @@ class AutoResponseView(BaseListView):
def get_queryset(self):
"""Get QuerySet from cached widget."""
return self.widget.filter_queryset(self.term, self.queryset)
kwargs = {
model_field_name: self.request.GET.get(form_field_name)
for form_field_name, model_field_name in self.widget.dependent_fields.items()
if form_field_name in self.request.GET and self.request.GET.get(form_field_name, '') != ''
}
return self.widget.filter_queryset(self.term, self.queryset, **kwargs)
def get_paginate_by(self, queryset):
"""Paginate response by size of widget's `max_results` parameter."""
+119
View File
@@ -0,0 +1,119 @@
Extra
=====
Chained select2
---------------
Suppose you have an address form where a user should choose a Country and a City.
When the user selects a country we want to show only cities belonging to that country.
So the one selector depends on another one.
Models
``````
Here are our two models:
.. code-block:: python
class Country(models.Model):
name = models.CharField(max_length=255)
class City(models.Model):
name = models.CharField(max_length=255)
country = models.ForeignKey('Country', related_name="cities")
Customizing a Form
``````````````````
Lets link two widgets via *dependent_fields*.
.. code-block:: python
:emphasize-lines: 15
class AddressForm(forms.Form):
country = forms.ModelChoiceField(
queryset=Country.objects.all(),
label=u"Country",
widget=ModelSelect2Widget(
search_fields=['name__icontains'],
)
)
city = forms.ModelChoiceField(
queryset=City.objects.all(),
label=u"City",
widget=ModelSelect2Widget(
search_fields=['name__icontains'],
dependent_fields={'country': 'country'},
max_results=500,
)
)
Interdependent select2
----------------------
Also you may want not to restrict the user to which field should be selected first.
Instead you want to suggest to the user options for any select2 depending of his selection in another one.
Customize the form in a manner:
.. code-block:: python
:emphasize-lines: 7
class AddressForm(forms.Form):
country = forms.ModelChoiceField(
queryset=Country.objects.all(),
label=u"Country",
widget=ModelSelect2Widget(
search_fields=['name__icontains'],
dependent_fields={'city': 'cities'},
)
)
city = forms.ModelChoiceField(
queryset=City.objects.all(),
label=u"City",
widget=ModelSelect2Widget(
search_fields=['name__icontains'],
dependent_fields={'country': 'country'},
max_results=500,
)
)
Take attention to country's dependent_fields. The value of 'city' is 'cities' because of
related name used in a filter condition `cities` which differs from widget field name `city`.
.. caution::
Be aware of using interdependent select2 in parent-child relation.
When a child is selected, you are restricted to change parent (only one value is available).
Probably you should let the user reset the child first to release parent select2.
Multi-dependent select2
-----------------------
Furthermore you may want to filter options on two or more select2 selections (some code is dropped for clarity):
.. code-block:: python
:emphasize-lines: 14
class SomeForm(forms.Form):
field1 = forms.ModelChoiceField(
widget=ModelSelect2Widget(
)
)
field2 = forms.ModelChoiceField(
widget=ModelSelect2Widget(
)
)
field3 = forms.ModelChoiceField(
widget=ModelSelect2Widget(
dependent_fields={'field1': 'field1', 'field2': 'field2'},
)
)
+1
View File
@@ -14,6 +14,7 @@ Contents:
get_started
django_select2
extra
Indices and tables
==================
+1
View File
@@ -15,3 +15,4 @@ functionalities
plugin
multi
Indices
clearable
+4 -2
View File
@@ -1,12 +1,14 @@
coverage
django-appconf
flake8
pep8-naming
mccabe
isort
pep257
pydocstyle
pytest
pytest-django
selenium
sphinx
sphinxcontrib-spelling
pyenchant
pyenchant
django-redis
+34
View File
@@ -0,0 +1,34 @@
#
# This file is autogenerated by pip-compile
# To update, run:
#
# pip-compile --output-file requirements-dev.txt requirements-dev.in
#
alabaster==0.7.10 # via sphinx
babel==2.4.0 # via sphinx
coverage==4.4.1
django-appconf==1.0.2
django-redis==4.8.0
docutils==0.13.1 # via sphinx
flake8==3.3.0
isort==4.2.15
jinja2==2.9.6 # via sphinx
markupsafe==1.0 # via jinja2
mccabe==0.6.1
pydocstyle==2.0.0
pep8-naming==0.4.1
pep8==1.7.0 # via flake8
py==1.4.34 # via pytest
pyenchant==1.6.8
pyflakes==1.5.0 # via flake8
pygments==2.2.0 # via sphinx
pytest-django==3.1.2
pytest==3.1.3
pytz==2017.2 # via babel
redis==2.10.5 # via django-redis
selenium==3.4.3
six==1.10.0 # via django-appconf, sphinx, sphinxcontrib-spelling
snowballstemmer==1.2.1 # via sphinx
sphinx-rtd-theme==0.2.4 # via sphinx
sphinx==1.6.3
sphinxcontrib-spelling==2.3.0
-31
View File
@@ -1,31 +0,0 @@
#
# This file is autogenerated by pip-compile
# Make changes in requirements_dev.in, then run this to update:
#
# pip-compile requirements_dev.in
#
alabaster==0.7.7 # via sphinx
babel==2.2.0 # via sphinx
django-appconf==1.0.1
docutils==0.12 # via sphinx
flake8==2.5.2
isort==4.2.2
Jinja2==2.8
MarkupSafe==0.23
mccabe==0.4.0
pep257==0.7.0
pep8-naming==0.3.3
pep8==1.7.0 # via flake8
py==1.4.31 # via pytest
pyenchant==1.6.6
pyflakes==1.0.0 # via flake8
Pygments==2.1
pytest-django==2.9.1
pytest==2.8.7
pytz==2015.7 # via babel
selenium==2.50.1
six==1.10.0 # via django-appconf, sphinx, sphinxcontrib-spelling
snowballstemmer==1.2.1 # via sphinx
sphinx-rtd-theme==0.1.9 # via sphinx
sphinx==1.3.5
sphinxcontrib-spelling==2.1.2
+6 -4
View File
@@ -1,5 +1,5 @@
[pytest]
norecursedirs = env
[tool:pytest]
norecursedirs = env docs
addopts = --tb=short -rxs
DJANGO_SETTINGS_MODULE=tests.testapp.settings
@@ -8,15 +8,17 @@ max-line-length = 120
max-complexity = 10
statistics = true
show-source = true
exclude = docs,runtests.py,setup.py,env
exclude = docs,runtests.py,setup.py,.tox,env
[pep257]
match = (?!setup).*.py
match-dir = (?!tests|env|docs|\.).*
[isort]
atomic = true
multi_line_output = 5
line_length = 79
skip = manage.py,docs
skip = manage.py,docs,.tox,env
known_first_party = django_select2, tests
known_third_party = django
combine_as_imports = true
+2 -1
View File
@@ -44,7 +44,8 @@ setup(
"Programming Language :: Python :: 3",
"Framework :: Django",
"Framework :: Django :: 1.8",
"Framework :: Django :: 1.9",
"Framework :: Django :: 1.10",
"Framework :: Django :: 1.11",
],
install_requires=[
'django-appconf>=0.6.0',
+30 -17
View File
@@ -1,20 +1,14 @@
# -*- coding:utf-8 -*-
from __future__ import absolute_import, print_function, unicode_literals
import os
import random
import string
import pytest
from django.utils.encoding import force_text
from selenium import webdriver
from selenium.common.exceptions import WebDriverException
browsers = {
# 'firefox': webdriver.Firefox,
# 'chrome': webdriver.Chrome,
'phantomjs': webdriver.PhantomJS,
}
def random_string(n):
return ''.join(
@@ -23,20 +17,23 @@ def random_string(n):
)
@pytest.fixture(scope='session',
params=browsers.keys())
def driver(request):
if 'DISPLAY' not in os.environ:
pytest.skip('Test requires display server (export DISPLAY)')
def random_name(n):
words = ''.join(random.choice(string.ascii_lowercase + ' ') for _ in range(n)).strip().split(' ')
return '-'.join([x.capitalize() for x in words])
@pytest.yield_fixture(scope='session')
def driver():
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument('headless')
chrome_options.add_argument('window-size=1200x800')
try:
b = browsers[request.param]()
b = webdriver.Chrome(chrome_options=chrome_options)
except WebDriverException as e:
pytest.skip(e)
pytest.skip(force_text(e))
else:
b.set_window_size(1200, 800)
request.addfinalizer(lambda *args: b.quit())
return b
yield b
b.quit()
@pytest.fixture
@@ -54,3 +51,19 @@ def artists(db):
return Artist.objects.bulk_create(
[Artist(pk=pk, title=random_string(50)) for pk in range(100)]
)
@pytest.fixture
def countries(db):
from .testapp.models import Country
return Country.objects.bulk_create(
[Country(pk=pk, name=random_name(random.randint(10, 20))) for pk in range(10)]
)
@pytest.fixture
def cities(db, countries):
from .testapp.models import City
return City.objects.bulk_create(
[City(pk=pk, name=random_name(random.randint(5, 15)), country=random.choice(countries)) for pk in range(100)]
)
+143 -18
View File
@@ -3,14 +3,18 @@ from __future__ import absolute_import, unicode_literals
import collections
import json
import os
import pytest
from django.core import signing
from django.core.urlresolvers import reverse
from django.db.models import QuerySet
from django.utils import translation
from django.utils.encoding import force_text
from django.utils.six import text_type
from selenium.common.exceptions import NoSuchElementException
from six import text_type
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.support.wait import WebDriverWait
from django_select2.cache import cache
from django_select2.forms import (
@@ -21,12 +25,18 @@ from tests.testapp import forms
from tests.testapp.forms import (
NUMBER_CHOICES, HeavySelect2MultipleWidgetForm, TitleModelSelect2Widget
)
from tests.testapp.models import Genre
from tests.testapp.models import City, Country, Genre
try:
from django.urls import reverse
except ImportError:
from django.core.urlresolvers import reverse
class TestSelect2Mixin(object):
url = reverse('select2_widget')
form = forms.AlbumSelect2WidgetForm()
multiple_form = forms.AlbumSelect2MultipleWidgetForm()
widget_cls = Select2Widget
def test_initial_data(self, genres):
@@ -44,7 +54,7 @@ class TestSelect2Mixin(object):
assert required_field.required is True
assert 'data-allow-clear="true"' not in required_field.widget.render('artist', None)
assert 'data-allow-clear="false"' in required_field.widget.render('artist', None)
assert '<option></option>' not in required_field.widget.render('artist', None)
assert '<option value=""></option>' not in required_field.widget.render('artist', None)
not_required_field = self.form.fields['primary_genre']
assert not_required_field.required is False
@@ -52,7 +62,7 @@ class TestSelect2Mixin(object):
assert 'data-allow-clear="false"' not in not_required_field.widget.render('primary_genre',
None)
assert 'data-placeholder' in not_required_field.widget.render('primary_genre', None)
assert '<option></option>' in not_required_field.widget.render('primary_genre', None)
assert '<option value=""></option>' in not_required_field.widget.render('primary_genre', None)
def test_no_js_error(self, db, live_server, driver):
driver.get(live_server + self.url)
@@ -82,13 +92,46 @@ class TestSelect2Mixin(object):
widget = HeavySelect2Widget(data_url='/foo/bar')
assert widget.get_url() == '/foo/bar'
def test_empty_option(self, db):
# Empty options is only required for single selects
# https://select2.github.io/options.html#allowClear
single_select = self.form.fields['primary_genre']
assert single_select.required is False
assert '<option value=""></option>' in single_select.widget.render('primary_genre', None)
multiple_select = self.multiple_form.fields['featured_artists']
assert multiple_select.required is False
assert multiple_select.widget.allow_multiple_selected
assert '<option value=""></option>' not in multiple_select.widget.render('featured_artists', None)
def test_i18n(self):
translation.activate('de')
assert Select2Widget().media._js == [
'//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/select2.min.js',
'//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/i18n/de.js',
'django_select2/django_select2.js'
]
translation.activate('en')
assert Select2Widget().media._js == [
'//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/select2.min.js',
'//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/i18n/en.js',
'django_select2/django_select2.js'
]
translation.activate('00')
assert Select2Widget().media._js == [
'//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/select2.min.js',
'django_select2/django_select2.js'
]
class TestSelect2MixinSettings(object):
def test_default_media(self):
sut = Select2Widget()
result = sut.media.render()
assert '//cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/js/select2.min.js' in result
assert '//cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/css/select2.min.css' in result
assert '//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/select2.min.js' in result
assert '//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/css/select2.min.css' in result
assert 'django_select2/django_select2.js' in result
def test_js_setting(self, settings):
@@ -123,7 +166,9 @@ class TestHeavySelect2Mixin(TestSelect2Mixin):
not_required_field = self.form.fields['primary_genre']
assert not_required_field.required is False
assert '<option value="1" selected="selected">One</option>' in \
not_required_field.widget.render('primary_genre', 1), \
not_required_field.widget.render('primary_genre', 1) or \
'<option value="1" selected>One</option>' in \
not_required_field.widget.render('primary_genre', 1), \
not_required_field.widget.render('primary_genre', 1)
def test_many_selected_option(self, db, genres):
@@ -131,10 +176,12 @@ class TestHeavySelect2Mixin(TestSelect2Mixin):
field.widget.choices = NUMBER_CHOICES
widget_output = field.widget.render('genres', [1, 2])
selected_option = '<option value="{pk}" selected="selected">{value}</option>'.format(pk=1, value='One')
selected_option_a = '<option value="{pk}" selected>{value}</option>'.format(pk=1, value='One')
selected_option2 = '<option value="{pk}" selected="selected">{value}</option>'.format(pk=2, value='Two')
selected_option2a = '<option value="{pk}" selected>{value}</option>'.format(pk=2, value='Two')
assert selected_option in widget_output, widget_output
assert selected_option2 in widget_output
assert selected_option in widget_output or selected_option_a in widget_output, widget_output
assert selected_option2 in widget_output or selected_option2a in widget_output
def test_multiple_widgets(self, db, live_server, driver):
driver.get(live_server + self.url)
@@ -143,9 +190,14 @@ class TestHeavySelect2Mixin(TestSelect2Mixin):
elem1, elem2 = driver.find_elements_by_css_selector('.select2-selection')
elem1.click()
result1 = driver.find_element_by_css_selector('.select2-results li:first-child').text
result1 = WebDriverWait(driver, 60).until(
expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '.select2-results li:first-child'))
).text
elem2.click()
result2 = driver.find_element_by_css_selector('.select2-results li:first-child').text
result2 = WebDriverWait(driver, 60).until(
expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '.select2-results li:first-child'))
).text
assert result1 != result2
@@ -170,6 +222,7 @@ class TestHeavySelect2Mixin(TestSelect2Mixin):
class TestModelSelect2Mixin(TestHeavySelect2Mixin):
form = forms.AlbumModelSelect2WidgetForm(initial={'primary_genre': 1})
multiple_form = forms.ArtistModelSelect2MultipleWidgetForm()
def test_initial_data(self, genres):
genre = genres[0]
@@ -182,7 +235,7 @@ class TestModelSelect2Mixin(TestHeavySelect2Mixin):
genre.save()
form = self.form.__class__(initial={'primary_genre': genre.pk})
assert genre.title not in form.as_p()
assert genre.title not in form.as_p(), form.as_p()
assert genre.title.upper() in form.as_p()
@pytest.fixture(autouse=True)
@@ -198,10 +251,12 @@ class TestModelSelect2Mixin(TestHeavySelect2Mixin):
'primary_genre', genre.pk)
selected_option = '<option value="{pk}" selected="selected">{value}</option>'.format(
pk=genre.pk, value=force_text(genre))
selected_option_a = '<option value="{pk}" selected>{value}</option>'.format(
pk=genre.pk, value=force_text(genre))
unselected_option = '<option value="{pk}">{value}</option>'.format(
pk=genre2.pk, value=force_text(genre2))
assert selected_option in widget_output, widget_output
assert selected_option in widget_output or selected_option_a in widget_output, widget_output
assert unselected_option not in widget_output
def test_selected_option_label_from_instance(self, db, genres):
@@ -212,14 +267,15 @@ class TestModelSelect2Mixin(TestHeavySelect2Mixin):
field = self.form.fields['primary_genre']
widget_output = field.widget.render('primary_genre', genre.pk)
def get_selected_option(genre):
def get_selected_options(genre):
return '<option value="{pk}" selected="selected">{value}</option>'.format(
pk=genre.pk, value=force_text(genre)), '<option value="{pk}" selected>{value}</option>'.format(
pk=genre.pk, value=force_text(genre))
assert get_selected_option(genre) not in widget_output
assert all(o not in widget_output for o in get_selected_options(genre))
genre.title = genre.title.upper()
assert get_selected_option(genre) in widget_output
assert any(o in widget_output for o in get_selected_options(genre))
def test_get_queryset(self):
widget = ModelSelect2Widget()
@@ -308,8 +364,13 @@ class TestHeavySelect2MultipleWidget(object):
form = forms.HeavySelect2MultipleWidgetForm()
widget_cls = HeavySelect2MultipleWidget
@pytest.mark.xfail(bool(os.environ.get('CI', False)),
reason='https://bugs.chromium.org/p/chromedriver/issues/detail?id=1772')
def test_widgets_selected_after_validation_error(self, db, live_server, driver):
driver.get(live_server + self.url)
WebDriverWait(driver, 3).until(
expected_conditions.presence_of_element_located((By.ID, 'id_title'))
)
title = driver.find_element_by_id('id_title')
title.send_keys('fo')
genres, fartists = driver.find_elements_by_css_selector('.select2-selection--multiple')
@@ -319,8 +380,72 @@ class TestHeavySelect2MultipleWidget(object):
driver.find_element_by_css_selector('.select2-results li:nth-child(2)').click()
genres.submit()
# there is a ValidationError raised, check for it
errstring = driver.find_element_by_css_selector('ul.errorlist li').text
errstring = WebDriverWait(driver, 3).until(
expected_conditions.presence_of_element_located((By.CSS_SELECTOR, 'ul.errorlist li'))
).text
assert errstring == "Title must have more than 3 characters."
# genres should still have One as selected option
result_title = driver.find_element_by_css_selector('.select2-selection--multiple li').get_attribute('title')
assert result_title == 'One'
class TestAddressChainedSelect2Widget(object):
url = reverse('model_chained_select2_widget')
form = forms.AddressChainedSelect2WidgetForm()
def test_widgets_selected_after_validation_error(self, db, live_server, driver, countries, cities):
driver.get(live_server + self.url)
WebDriverWait(driver, 60).until(
expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '.select2-selection--single'))
)
country_container, city_container = driver.find_elements_by_css_selector('.select2-selection--single')
# clicking city select2 lists all available cities
city_container.click()
WebDriverWait(driver, 60).until(
expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '.select2-results li'))
)
city_options = driver.find_elements_by_css_selector('.select2-results li')
city_names_from_browser = {option.text for option in city_options}
city_names_from_db = set(City.objects.values_list('name', flat=True))
assert len(city_names_from_browser) == City.objects.count()
assert city_names_from_browser == city_names_from_db
# selecting a country really does it
country_container.click()
WebDriverWait(driver, 60).until(
expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '.select2-results li:nth-child(2)'))
)
country_option = driver.find_element_by_css_selector('.select2-results li:nth-child(2)')
country_name = country_option.text
country_option.click()
assert country_name == country_container.text
# clicking city select2 lists reduced list of cities belonging to the country
city_container.click()
WebDriverWait(driver, 60).until(
expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '.select2-results li'))
)
city_options = driver.find_elements_by_css_selector('.select2-results li')
city_names_from_browser = {option.text for option in city_options}
city_names_from_db = set(Country.objects.get(name=country_name).cities.values_list('name', flat=True))
assert len(city_names_from_browser) != City.objects.count()
assert city_names_from_browser == city_names_from_db
# selecting a city reaaly does it
city_option = driver.find_element_by_css_selector('.select2-results li:nth-child(2)')
city_name = city_option.text
city_option.click()
assert city_name == city_container.text
# clicking country select2 lists reduced list to the only country available to the city
country_container.click()
WebDriverWait(driver, 60).until(
expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '.select2-results li'))
)
country_options = driver.find_elements_by_css_selector('.select2-results li')
country_names_from_browser = {option.text for option in country_options}
country_names_from_db = {City.objects.get(name=city_name).country.name}
assert len(country_names_from_browser) != Country.objects.count()
assert country_names_from_browser == country_names_from_db
+5 -1
View File
@@ -4,7 +4,6 @@ from __future__ import absolute_import, unicode_literals
import json
from django.core import signing
from django.core.urlresolvers import reverse
from django.utils.encoding import smart_text
from django_select2.cache import cache
@@ -14,6 +13,11 @@ from tests.testapp.forms import (
)
from tests.testapp.models import Genre
try:
from django.urls import reverse
except ImportError:
from django.core.urlresolvers import reverse
class TestAutoResponseView(object):
def test_get(self, client, artists):
+1
View File
@@ -0,0 +1 @@
!coverage.py: This is a private format, don't read it directly!{"lines":{}}
+32 -2
View File
@@ -10,7 +10,7 @@ from django_select2.forms import (
Select2Widget
)
from tests.testapp import models
from tests.testapp.models import Album
from tests.testapp.models import Album, City, Country
class TitleSearchFieldMixin(object):
@@ -117,7 +117,13 @@ class ArtistModelSelect2MultipleWidgetForm(forms.Form):
genres = forms.ModelMultipleChoiceField(widget=ModelSelect2MultipleWidget(
queryset=models.Genre.objects.all(),
search_fields=['title__icontains'],
), queryset=models.Genre.objects.all(), required=False)
), queryset=models.Genre.objects.all(), required=True)
featured_artists = forms.ModelMultipleChoiceField(widget=ModelSelect2MultipleWidget(
queryset=models.Artist.objects.all(),
search_fields=['title__icontains'],
), queryset=models.Artist.objects.all(), required=False)
NUMBER_CHOICES = [
(1, 'One'),
@@ -168,3 +174,27 @@ class ModelSelect2TagWidgetForm(forms.ModelForm):
widgets = {
'genres': GenreSelect2TagWidget
}
class AddressChainedSelect2WidgetForm(forms.Form):
country = forms.ModelChoiceField(
queryset=Country.objects.all(),
label='Country',
widget=ModelSelect2Widget(
model=Country,
search_fields=['name__icontains'],
max_results=500,
dependent_fields={'city': 'cities'},
)
)
city = forms.ModelChoiceField(
queryset=City.objects.all(),
label='City',
widget=ModelSelect2Widget(
model=City,
search_fields=['name__icontains'],
dependent_fields={'country': 'country'},
max_results=500,
)
)
+20 -2
View File
@@ -25,10 +25,28 @@ class Artist(models.Model):
@python_2_unicode_compatible
class Album(models.Model):
title = models.CharField(max_length=255)
artist = models.ForeignKey(Artist)
artist = models.ForeignKey(Artist, on_delete=models.CASCADE)
featured_artists = models.ManyToManyField(Artist, blank=True, related_name='featured_album_set')
primary_genre = models.ForeignKey(Genre, blank=True, null=True, related_name='primary_album_set')
primary_genre = models.ForeignKey(Genre, on_delete=models.CASCADE, blank=True, null=True,
related_name='primary_album_set')
genres = models.ManyToManyField(Genre)
def __str__(self):
return self.title
@python_2_unicode_compatible
class Country(models.Model):
name = models.CharField(max_length=255)
def __str__(self):
return self.name
@python_2_unicode_compatible
class City(models.Model):
name = models.CharField(max_length=255)
country = models.ForeignKey('Country', related_name="cities", on_delete=models.CASCADE)
def __str__(self):
return self.name
+13 -3
View File
@@ -33,21 +33,31 @@ MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
SITE_ID = 1
ROOT_URLCONF = 'tests.testapp.urls'
LANGUAGES = [
('de', 'German'),
('en', 'English'),
]
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'APP_DIRS': True,
'DIRS': ['templates'],
},
]
SECRET_KEY = '123456'
USE_L10N = True
USE_I18N = True
if os.environ.get('TRAVIS'):
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
'LOCATION': 'localhost:11211',
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/1",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
}
}
}
+2 -2
View File
@@ -15,11 +15,11 @@
{{ form }}
<input type="submit" value="Submit Form"/>
</form>
<script src="{% static '//code.jquery.com/jquery-2.1.4.min.js' %}"></script>
<script src="//code.jquery.com/jquery-2.1.4.min.js"></script>
<script type="text/javascript">
window.onerror = function (msg) {
$("body").attr("JSError", msg);
}
</script>
{{ form.media.js }}
</body>
</body>
+7 -2
View File
@@ -4,8 +4,9 @@ from __future__ import absolute_import, unicode_literals
from django.conf.urls import include, url
from .forms import (
AlbumModelSelect2WidgetForm, HeavySelect2MultipleWidgetForm,
HeavySelect2WidgetForm, ModelSelect2TagWidgetForm, Select2WidgetForm
AddressChainedSelect2WidgetForm, AlbumModelSelect2WidgetForm,
HeavySelect2MultipleWidgetForm, HeavySelect2WidgetForm,
ModelSelect2TagWidgetForm, Select2WidgetForm
)
from .views import TemplateFormView, heavy_data_1, heavy_data_2
@@ -26,6 +27,10 @@ urlpatterns = [
TemplateFormView.as_view(form_class=ModelSelect2TagWidgetForm),
name='model_select2_tag_widget'),
url(r'^model_chained_select2_widget/$',
TemplateFormView.as_view(form_class=AddressChainedSelect2WidgetForm),
name='model_chained_select2_widget'),
url(r'^heavy_data_1/$', heavy_data_1, name='heavy_data_1'),
url(r'^heavy_data_2/$', heavy_data_2, name='heavy_data_2'),
+34
View File
@@ -0,0 +1,34 @@
[tox]
envlist = py{27,35,36}-dj{18,110,111,master},qa,docs
[testenv]
setenv=
PYTHONPATH = {toxinidir}
passenv=CI
deps=
-rrequirements-dev.txt
dj18: https://github.com/django/django/archive/stable/1.8.x.tar.gz#egg=django
dj110: https://github.com/django/django/archive/stable/1.10.x.tar.gz#egg=django
dj111: https://github.com/django/django/archive/stable/1.11.x.tar.gz#egg=django
djmaster: https://github.com/django/django/archive/master.tar.gz#egg=django
commands=
coverage run --source=django_select2 -m 'pytest' \
--basetemp={envtmpdir} \
--ignore=.tox \
{posargs}
[testenv:qa]
changedir={toxinidir}
deps=
-rrequirements-dev.txt
commands=
isort --check-only --recursive --diff {posargs}
flake8 --jobs=2 {posargs}
pydocstyle --verbose --explain --source --count {posargs}
[testenv:docs]
whitelist_externals=make
changedir={toxinidir}/docs
deps=
-rrequirements-dev.txt
https://github.com/django/django/archive/stable/1.10.x.tar.gz#egg=django
commands=make spelling