Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ffe91627d8 | ||
|
|
66d2051c20 | ||
|
|
48ceeaa312 | ||
|
|
9b54cc30c7 | ||
|
|
82859121a6 | ||
|
|
d34cd2e5de | ||
|
|
e4efaab6f1 | ||
|
|
1e8971c12b | ||
|
|
de7e7a1903 | ||
|
|
0f18958099 | ||
|
|
ebbdd2b9be | ||
|
|
e9d96cf856 | ||
|
|
f6eaa26c16 | ||
|
|
73d4be8992 | ||
|
|
e1d42bfcad | ||
|
|
1954bf57fa | ||
|
|
08ca1c1c36 | ||
|
|
aef27f48df | ||
|
|
f31beec0c4 | ||
|
|
496cc7c502 | ||
|
|
17af750d88 | ||
|
|
67bf060991 | ||
|
|
d9c899e8f7 | ||
|
|
e4729feb9a | ||
|
|
2ea6ef8edb | ||
|
|
a9aa31146f | ||
|
|
c2331b272a | ||
|
|
5d98048c9f | ||
|
|
7782e9bc3d | ||
|
|
426cace3a3 | ||
|
|
90fbf7422f | ||
|
|
f6d5afe51d | ||
|
|
19e2e50921 | ||
|
|
937dcac08b | ||
|
|
322b389ad3 | ||
|
|
ac7050d2db | ||
|
|
d4dcc5f3c0 | ||
|
|
cca6583d48 | ||
|
|
1b1f345b3d | ||
|
|
e09951ce62 | ||
|
|
f967eb643b | ||
|
|
176be2fdcf | ||
|
|
e1f5b7d999 | ||
|
|
5f263f9385 | ||
|
|
3ebad0dc12 | ||
|
|
00c082bd6d | ||
|
|
3cc666e46d | ||
|
|
47c191fe13 | ||
|
|
4ebbb05d11 | ||
|
|
4ba7ecb8a0 | ||
|
|
2b2b11fdf4 | ||
|
|
88319ecc47 | ||
|
|
4c0139f253 | ||
|
|
fdfdbbb512 | ||
|
|
15201d1c7e | ||
|
|
da6867c083 | ||
|
|
6153f4778a | ||
|
|
58d8a9faa8 | ||
|
|
1a27c3562d | ||
|
|
b81b7f4d0c | ||
|
|
c07194e382 | ||
|
|
c4b3fdfbef | ||
|
|
2000395c3b | ||
|
|
8911ad91b2 | ||
|
|
5dec946658 | ||
|
|
85ffeda276 | ||
|
|
44d0039d49 | ||
|
|
a3f4a28ff1 | ||
|
|
403a32ff46 | ||
|
|
cca167bd24 | ||
|
|
90028d9e36 | ||
|
|
ec3462e87a | ||
|
|
db5ec06f83 | ||
|
|
b46f69f166 | ||
|
|
b88e26bfa2 | ||
|
|
78c54f2c47 | ||
|
|
018701680b | ||
|
|
d4668e3125 | ||
|
|
bc0eab96e0 | ||
|
|
6d2bd1cbd3 | ||
|
|
9d215f6efe | ||
|
|
5741279d0e | ||
|
|
12b0d472b8 | ||
|
|
74d2266817 | ||
|
|
c46fd5a5ec | ||
|
|
ad938c6008 | ||
|
|
2c1ae2df92 | ||
|
|
6260226ae5 | ||
|
|
a5bb222ea8 | ||
|
|
f12e624cb8 | ||
|
|
c0d6fcb663 | ||
|
|
6dc2f3eb19 | ||
|
|
26f2a36dc6 | ||
|
|
d6180e05fc | ||
|
|
67b7e755fe | ||
|
|
4147cac2f8 | ||
|
|
55e481470e | ||
|
|
7297ed7fde | ||
|
|
90be63441b | ||
|
|
d6e056c009 | ||
|
|
a910581f7a | ||
|
|
1931a98240 | ||
|
|
5840622794 | ||
|
|
dc6e87e7ba | ||
|
|
71a836c892 | ||
|
|
735d5931d6 | ||
|
|
9cc0a9d62d | ||
|
|
dfdc626ad3 | ||
|
|
07e8029026 | ||
|
|
1f9d564445 | ||
|
|
5099b8d44d | ||
|
|
8958940c43 | ||
|
|
d6202c4108 | ||
|
|
0d560c08fc | ||
|
|
17dff07690 | ||
|
|
c6d7054e16 | ||
|
|
a89fedbe75 | ||
|
|
0918cae3e7 | ||
|
|
8f6e33845e | ||
|
|
46a50f377b | ||
|
|
895a7e519a | ||
|
|
c235c2e3f0 | ||
|
|
9a658b3b0f | ||
|
|
efb120b47d | ||
|
|
b34642dcef | ||
|
|
8b64b5d4fa | ||
|
|
4e8b2c24a3 | ||
|
|
23e91c8bb0 | ||
|
|
e88c0e059c | ||
|
|
047ee28722 | ||
|
|
6018b60fea | ||
|
|
f1e0752f2d | ||
|
|
172ef79ffd | ||
|
|
36a9ab7bab | ||
|
|
09110a5460 | ||
|
|
ec1c81ad87 | ||
|
|
3673e55195 | ||
|
|
937874127f | ||
|
|
681ced9f15 | ||
|
|
e375a58b4d | ||
|
|
b2609256f5 | ||
|
|
b676e563e2 | ||
|
|
65cfc12be3 | ||
|
|
e6ace9a8bc | ||
|
|
c3a48de1a3 | ||
|
|
8a463752a0 | ||
|
|
c364394450 | ||
|
|
977f2c3e25 | ||
|
|
aa15810d92 | ||
|
|
4a5c13a35f | ||
|
|
584e2e9f89 | ||
|
|
33a7e03acf | ||
|
|
93dab02fd1 | ||
|
|
d364dc9630 | ||
|
|
701d57ef31 | ||
|
|
71c6e1d9c3 | ||
|
|
7c2a898cfa | ||
|
|
6180846345 | ||
|
|
9430a950c2 | ||
|
|
1f9b094b67 | ||
|
|
15dbbf41a7 | ||
|
|
fb3bc19595 | ||
|
|
f03d7d44a8 | ||
|
|
e3c6919bbd | ||
|
|
9f5644b479 | ||
|
|
c797e59584 | ||
|
|
72616f8276 | ||
|
|
99aed19308 | ||
|
|
74bf15ce48 | ||
|
|
57dfd53d55 | ||
|
|
f943fe07f5 | ||
|
|
a6a12d7392 | ||
|
|
f579f4af35 | ||
|
|
0b3abb8f1f | ||
|
|
01c8979100 | ||
|
|
f6ee1f57db | ||
|
|
894a251be3 | ||
|
|
9ed8f58fa6 | ||
|
|
a8a4baf04d | ||
|
|
5b0400dfaa | ||
|
|
7923eeb0ee | ||
|
|
f09f9e5249 | ||
|
|
031ec6682c | ||
|
|
4dacbdf283 | ||
|
|
bc9601b3d3 | ||
|
|
d57b726d34 | ||
|
|
d25e733c85 | ||
|
|
27091f5b49 | ||
|
|
8d48887f5c | ||
|
|
fa6a841746 | ||
|
|
0ee50d9866 | ||
|
|
7d8908daab | ||
|
|
4a1b83ad1e | ||
|
|
33a00c1e39 | ||
|
|
89674ec375 | ||
|
|
e655ed7a59 | ||
|
|
3c06873148 | ||
|
|
84e1abaf4e | ||
|
|
6ebeb4c6d7 | ||
|
|
7d8a7e7a9f | ||
|
|
9ba75bb820 | ||
|
|
cfc7c2c541 | ||
|
|
6c5e2036ca | ||
|
|
4defb8112b | ||
|
|
bb90be77b7 | ||
|
|
18647161a1 | ||
|
|
3e6c1fc3ba | ||
|
|
19a8d63bb1 | ||
|
|
0457617738 | ||
|
|
974ba552b0 | ||
|
|
2beb5d23c7 | ||
|
|
b8d53aa714 | ||
|
|
b0d2325c43 | ||
|
|
9d1bcfee6a | ||
|
|
2c1eb8bc4a |
@@ -0,0 +1,3 @@
|
||||
- flake8
|
||||
- pydocstyle
|
||||
- bandit
|
||||
+4
-3
@@ -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
|
||||
|
||||
+72
-28
@@ -1,38 +1,82 @@
|
||||
sudo: true
|
||||
dist: xenial
|
||||
|
||||
services: redis-server
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- chromium-chromedriver
|
||||
|
||||
language: python
|
||||
sudo: false
|
||||
cache:
|
||||
- pip
|
||||
services:
|
||||
- memcached
|
||||
python:
|
||||
- "2.7"
|
||||
- "3.3"
|
||||
- "3.4"
|
||||
- "pypy"
|
||||
- "pypy3"
|
||||
- 3.6
|
||||
- 3.7
|
||||
cache: pip
|
||||
env:
|
||||
global:
|
||||
- DISPLAY=:99.0
|
||||
matrix:
|
||||
- DJANGO="Django<1.8,>=1.7"
|
||||
- DJANGO="Django<1.9,>=1.8"
|
||||
- DJANGO="-e git+https://github.com/django/django.git@master#egg=Django"
|
||||
- DJANGO=20
|
||||
- DJANGO=21
|
||||
- DJANGO=22
|
||||
- DJANGO=master
|
||||
matrix:
|
||||
fast_finish: true
|
||||
allow_failures:
|
||||
- env: DJANGO="-e git+https://github.com/django/django.git@master#egg=Django"
|
||||
- env: DJANGO=master
|
||||
|
||||
jobs:
|
||||
include:
|
||||
- stage: docs
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- python3-enchant
|
||||
- python-enchant
|
||||
- graphviz
|
||||
python: "3.5"
|
||||
script: tox -e docs
|
||||
- stage: qa
|
||||
addons:
|
||||
language: node_js
|
||||
install: npm install
|
||||
script: npm test
|
||||
node_js: lts/*
|
||||
cache: npm
|
||||
states:
|
||||
- qa
|
||||
- docs
|
||||
- test
|
||||
- deploy
|
||||
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 codecov
|
||||
before_script:
|
||||
- ln -s /usr/lib/chromium-browser/chromedriver ~/bin/chromedriver
|
||||
- |
|
||||
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 django_select2
|
||||
- coverage run --source=django_select2 runtests.py
|
||||
- tox -e $TOXENV
|
||||
after_success:
|
||||
- coveralls
|
||||
- codecov
|
||||
before_deploy:
|
||||
- git stash --all
|
||||
- ./set_version.py
|
||||
_deploy_provider: &_deploy_provider
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
repo: applegrew/django-select2
|
||||
deploy:
|
||||
- <<: *_deploy_provider
|
||||
provider: pypi
|
||||
distributions: sdist bdist_wheel
|
||||
user: codingjoe
|
||||
password:
|
||||
secure: fEP9K7y0ZF9fRvQEUN4kgPXQEZvi3Cx3ikUebG2UM/2uhcaUQm0+KpgZ2S+lvOTYcBnNgzPzFsVIZMcVcTxwIKuQDEMq9y2eop2hnisb9KXsIm9qPYSzOnRm74VuiOt4hNOZMe0qVBK2cO3vC9NDXuzdI8A0JynJhthfl4t+kFM=
|
||||
- <<: *_deploy_provider
|
||||
provider: npm
|
||||
email: info@johanneshoppe.com
|
||||
api_key:
|
||||
secure: PV38cQx9qhEFkpSdytbM72UzIMCfhpjmRJ8dzT+bCAaOIs5rEcyKN+h1r5ranunCxWyuFsMW4A2iW/SCxnKCR/oPAguuwUbT5ogBXlsskqPFWUxuoTHYMrd+zB+SC6+bMgq+o5ul+kJCYtEkWP6cMlIEzKyTLab7m5PsnDXNVnI=
|
||||
|
||||
@@ -1,6 +1,90 @@
|
||||
Changelog Summary
|
||||
=================
|
||||
|
||||
Changes for django-select2 v6.0.1 and later are logged on the [github releases page](https://github.com/applegrew/django-select2/releases).
|
||||
|
||||
### v6.0.0
|
||||
|
||||
* Add Support for Django 2.0
|
||||
* Drop Python 2 support
|
||||
* Drop Python 3.5 support
|
||||
* Drop Django 1.8 support
|
||||
* Drop Django 1.10 support
|
||||
|
||||
### 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
|
||||
|
||||
### v5.8.0
|
||||
* Changed signature of `render` and `render_choices` to satisfy Django 1.10 changes.
|
||||
* Changed widgets' inheritance tree to be more consistent.
|
||||
|
||||
### v5.7.1
|
||||
* Fixes pickle bug of lazy object
|
||||
|
||||
### v5.7.0
|
||||
* Security fix that allows a `field_id` to only be used for the intended JSON endpoint.
|
||||
|
||||
Prior to that change you could use any `field_id` on any select2 JSON endpoint.
|
||||
Even if the id was intended to be used on a private endpoint if could be used on
|
||||
the default one and therefore leak sensitive data.
|
||||
|
||||
* Breaking change on how `Heavy` widgets are being cached.
|
||||
|
||||
Heavy widgets used to add themselves to the cache. Now they add a dictionary to
|
||||
the cache containing themselves and the target url.
|
||||
|
||||
```python
|
||||
{
|
||||
'widget': self,
|
||||
'url': self.get_url(),
|
||||
}
|
||||
```
|
||||
|
||||
### v5.6.0
|
||||
* Added `label_from_instance` method for model widgets to define custom option labels.
|
||||
|
||||
### v5.5.0
|
||||
* Added settings to delivery static assets from different source.
|
||||
|
||||
### v5.4.2
|
||||
* Fixed initial data not being shown for heavy widgets.
|
||||
|
||||
### v5.4.1
|
||||
* Fixed memory leak in `ModelSelect2Mixin` and subclasses
|
||||
|
||||
### v5.4.0
|
||||
* Added `Select2TagWidget` a light widget with tagging support
|
||||
|
||||
### v5.3.0
|
||||
* Added djangoSelect2 jQuery plugin to support
|
||||
dynamic field initialisation
|
||||
|
||||
### v5.2.0
|
||||
* Added pagination
|
||||
|
||||
### v5.1.0
|
||||
* Added search term splitting
|
||||
* Model widgets get smarter pickling to reduce size and avoid pickling issues
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
Copyright 2017 Johannes Hoppe
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-176
@@ -1,176 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
@@ -1,5 +0,0 @@
|
||||
include *.txt *.md
|
||||
recursive-include django_select2/static *.*
|
||||
recursive-exclude django_select2/static .DS_Store
|
||||
exclude requirements_dev.txt
|
||||
prune tests
|
||||
@@ -1,100 +0,0 @@
|
||||
Django-Select2
|
||||
==============
|
||||
|
||||
[](https://pypi.python.org/pypi/Django-Select2/)
|
||||
[](https://travis-ci.org/applegrew/django-select2)
|
||||
[](https://coveralls.io/r/applegrew/django-select2)
|
||||
[](https://raw.githubusercontent.com/applegrew/django-select2/master/LICENSE.txt)
|
||||
[](https://gitter.im/applegrew/django-select2?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
This is a [Django](https://www.djangoproject.com/) integration of [Select2](http://ivaynberg.github.com/select2/).
|
||||
|
||||
The app includes Select2 driven Django Widgets.
|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
1. Install `django_select2`
|
||||
|
||||
pip install django_select2
|
||||
|
||||
2. Add `django_select2` to your `INSTALLED_APPS` in your project settings.
|
||||
|
||||
3. Add `django_select` to your urlconf if you use any 'Auto' fields.
|
||||
|
||||
url(r'^select2/', include('django_select2.urls')),
|
||||
|
||||
|
||||
### Upgrade from Version 4
|
||||
|
||||
Version 5 is a complete rewrite of the package to drastically reduce
|
||||
the code base and to ensure a future maintainability.
|
||||
|
||||
While we feature set remained unchanged, the API changed completely.
|
||||
Major changes:
|
||||
- Fields have been removed in favor of widgets.
|
||||
- All version 4 settings have been removed.
|
||||
- Template tags have been removed.
|
||||
- 3rd party javascript is served by a CDN.
|
||||
- No more inline javascript code.
|
||||
|
||||
#### Upgrade can be done in 5 simple steps:
|
||||
|
||||
1. Remove all existing and to setup the new cache backend.
|
||||
2. Remove the old template tags from your templates:
|
||||
1. `import_django_select2_js`
|
||||
2. `import_django_select2_css`
|
||||
3. `import_django_select2_js_css`
|
||||
3. Add `form.media.css` to the top and `form.media.js`
|
||||
to the bottom of your base template.
|
||||
4. Upgrade to jQuery version 2, if you are still running version 1.
|
||||
5. Replace old fields with new widgets.
|
||||
|
||||
|
||||
## Documentation
|
||||
|
||||
|
||||
Documentation available at http://django-select2.readthedocs.org/.
|
||||
|
||||
## External Dependencies
|
||||
|
||||
|
||||
* jQuery version 2
|
||||
This is not included in the package since it is expected
|
||||
that in most scenarios this would already be available.
|
||||
|
||||
|
||||
## Example Application
|
||||
|
||||
Please see `tests/testapp` application.
|
||||
This application is used to manually test the functionalities of this package.
|
||||
This also serves as a good example.
|
||||
|
||||
## Special Thanks
|
||||
|
||||
|
||||
* Samuel Goldszmidt (@ouhouhsami) for reporting many fundamental issues with the code, because of which versions 2.0 and 2.0.1 were released.
|
||||
|
||||
## Official Contributors
|
||||
|
||||
* Johannes Hoppe (@codingjoe)
|
||||
|
||||
## Changelog
|
||||
|
||||
See [CHANGELOG.md](CHANGELOG.md)
|
||||
|
||||
## License
|
||||
|
||||
Copyright 2012 Nirupam Biswas
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this project except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
Django-Select2
|
||||
==============
|
||||
|
||||
|version| |ci| |coverage| |license|
|
||||
|
||||
This is a `Django`_ integration of `Select2`_.
|
||||
|
||||
The app includes Select2 driven Django Widgets.
|
||||
|
||||
.. note::
|
||||
Django's admin comes with builtin support for Select2
|
||||
since version 2.0 via the `autocomplete_fields`_ feature.
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
1. Install ``django_select2``
|
||||
|
||||
.. code:: python
|
||||
|
||||
pip install django_select2
|
||||
|
||||
2. Add ``django_select2`` to your ``INSTALLED_APPS`` in your project
|
||||
settings.
|
||||
|
||||
3. Add ``django_select`` to your urlconf if you use any "Auto" fields.
|
||||
|
||||
.. code:: python
|
||||
|
||||
url(r'^select2/', include('django_select2.urls')),
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
Documentation available at http://django-select2.readthedocs.io/.
|
||||
|
||||
External Dependencies
|
||||
---------------------
|
||||
|
||||
- jQuery version 2 This is not included in the package since it is
|
||||
expected that in most scenarios this would already be available.
|
||||
|
||||
Example Application
|
||||
-------------------
|
||||
|
||||
Please see ``tests/testapp`` application. This application is used to
|
||||
manually test the functionalities of this package. This also serves as a
|
||||
good example.
|
||||
|
||||
Changelog
|
||||
---------
|
||||
|
||||
See `Github releases`_
|
||||
|
||||
|
||||
.. _Django: https://www.djangoproject.com/
|
||||
.. _Select2: http://ivaynberg.github.com/select2/
|
||||
.. _autocomplete_fields: https://docs.djangoproject.com/en/stable/ref/contrib/admin/#django.contrib.admin.ModelAdmin.autocomplete_fields
|
||||
.. _CHANGELOG.md: CHANGELOG.md
|
||||
.. _Github releases: https://github.com/applegrew/django-select2/releases
|
||||
|
||||
.. |version| image:: https://img.shields.io/pypi/v/Django-Select2.svg
|
||||
:target: https://pypi.python.org/pypi/Django-Select2/
|
||||
.. |ci| image:: https://travis-ci.org/applegrew/django-select2.svg?branch=master
|
||||
:target: https://travis-ci.org/applegrew/django-select2
|
||||
.. |coverage| image:: https://codecov.io/gh/applegrew/django-select2/branch/master/graph/badge.svg
|
||||
:target: https://codecov.io/gh/applegrew/django-select2
|
||||
.. |license| image:: https://img.shields.io/badge/license-APL2-blue.svg
|
||||
:target: https://raw.githubusercontent.com/applegrew/django-select2/master/LICENSE.txt
|
||||
@@ -1,12 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
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.1.0"
|
||||
default_app_config = 'django_select2.apps.Select2AppConfig'
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
"""Django application configuration."""
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class Select2AppConfig(AppConfig):
|
||||
"""Django application configuration."""
|
||||
|
||||
name = 'django_select2'
|
||||
verbose_name = 'Select2'
|
||||
@@ -1,19 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Shared memory across multiple machines to the heavy ajax lookups.
|
||||
Shared memory across multiple machines to the heavy AJAX lookups.
|
||||
|
||||
Select2 uses django.core.cache_ to share fields across
|
||||
multiple threads and even machines.
|
||||
|
||||
Select2 uses the cabhe backend defind in the setting
|
||||
Select2 uses the cache backend defined in the setting
|
||||
``SELECT2_CACHE_BACKEND`` [default=``default``].
|
||||
|
||||
It is advised to always setup a separate cache server for Select2.
|
||||
|
||||
.. _django.core.cache: https://docs.djangoproject.com/en/dev/topics/cache/
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.core.cache import caches
|
||||
|
||||
from .conf import settings
|
||||
|
||||
+134
-7
@@ -1,7 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Settings for Django-Select2."""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from appconf import AppConf
|
||||
from django.conf import settings # NOQA
|
||||
|
||||
@@ -9,9 +6,11 @@ __all__ = ('settings', 'Select2Conf')
|
||||
|
||||
|
||||
class Select2Conf(AppConf):
|
||||
|
||||
"""Settings for Django-Select2."""
|
||||
|
||||
LIB_VERSION = '4.0.5'
|
||||
"""Version of the Select2 library."""
|
||||
|
||||
CACHE_BACKEND = 'default'
|
||||
"""
|
||||
Django-Select2 uses Django's cache to sure a consistent state across multiple machines.
|
||||
@@ -27,8 +26,11 @@ class Select2Conf(AppConf):
|
||||
}
|
||||
},
|
||||
'select2': {
|
||||
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
|
||||
'LOCATION': '127.0.0.1:11211',
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": "redis://127.0.0.1:6379/2",
|
||||
"OPTIONS": {
|
||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,16 +40,141 @@ class Select2Conf(AppConf):
|
||||
.. tip:: To ensure a consistent state across all you machines you need to user
|
||||
a consistent external cache backend like Memcached, Redis or a database.
|
||||
|
||||
.. note::
|
||||
Should you have copied the example configuration please make sure you
|
||||
have Redis setup. It's recommended to run a separate Redis server in a
|
||||
production environment.
|
||||
|
||||
.. note:: The timeout of select2's caching backend determines
|
||||
how long a browser session can last.
|
||||
Once widget is dropped from the cache the json response view will return a 404.
|
||||
"""
|
||||
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/{version}/js/select2.min.js'.format(version=LIB_VERSION)
|
||||
"""
|
||||
The URI for the Select2 JS file. 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/select2.min.js'
|
||||
|
||||
If you provide your own JS and would not like Django-Select2 to load any, change
|
||||
this setting to a blank string like so::
|
||||
|
||||
SELECT2_JS = ''
|
||||
|
||||
.. tip:: Change this setting to a local asset in your development environment to
|
||||
develop without an Internet connection.
|
||||
"""
|
||||
|
||||
CSS = '//cdnjs.cloudflare.com/ajax/libs/select2/{version}/css/select2.min.css'.format(version=LIB_VERSION)
|
||||
"""
|
||||
The URI for the Select2 CSS file. By default this points to the Cloudflare CDN.
|
||||
|
||||
If you want to select the version of the library used, or want to serve it from
|
||||
the local 'static' resources, add a line to your settings.py like so::
|
||||
|
||||
SELECT2_CSS = 'assets/css/select2.css'
|
||||
|
||||
If you provide your own CSS and would not like Django-Select2 to load any, change
|
||||
this setting to a blank string like so::
|
||||
|
||||
SELECT2_CSS = ''
|
||||
|
||||
.. tip:: Change this setting to a local asset in your development environment to
|
||||
develop without an Internet connection.
|
||||
"""
|
||||
|
||||
I18N_PATH = '//cdnjs.cloudflare.com/ajax/libs/select2/{version}/js/i18n'.format(version=LIB_VERSION)
|
||||
"""
|
||||
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 I18N library used, or want to serve it from
|
||||
the local 'static' resources, add a line to your settings.py like so::
|
||||
|
||||
SELECT2_I18N_PATH = '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'
|
||||
|
||||
+210
-117
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Django-Select2 Widgets.
|
||||
|
||||
@@ -46,23 +45,23 @@ Light widgets are normally named, i.e. there is no
|
||||
:parts: 1
|
||||
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from functools import reduce
|
||||
from itertools import chain
|
||||
from pickle import PicklingError # nosec
|
||||
|
||||
from django import forms
|
||||
from django.contrib.admin.widgets import SELECT2_TRANSLATIONS
|
||||
from django.core import signing
|
||||
from django.core.urlresolvers import reverse_lazy
|
||||
from django.db.models import Q
|
||||
from django.forms.models import ModelChoiceIterator
|
||||
from django.utils.encoding import force_text
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import get_language
|
||||
|
||||
from .cache import cache
|
||||
from .conf import settings
|
||||
|
||||
|
||||
class Select2Mixin(object):
|
||||
|
||||
class Select2Mixin:
|
||||
"""
|
||||
The base mixin of all Select2 widgets.
|
||||
|
||||
@@ -71,9 +70,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:
|
||||
@@ -87,30 +86,49 @@ class Select2Mixin(object):
|
||||
attrs['class'] = 'django-select2'
|
||||
return attrs
|
||||
|
||||
def render_options(self, choices, selected_choices):
|
||||
"""Render options including an empty one, if the field is not required."""
|
||||
output = '<option></option>' if not self.is_required else ''
|
||||
output += super(Select2Mixin, self).render_options(choices, selected_choices)
|
||||
return output
|
||||
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 _get_media(self):
|
||||
"""
|
||||
Construct Media as a dynamic property.
|
||||
|
||||
.. Note:: For more information visit
|
||||
https://docs.djangoproject.com/en/1.8/topics/forms/media/#media-as-a-dynamic-property
|
||||
https://docs.djangoproject.com/en/stable/topics/forms/media/#media-as-a-dynamic-property
|
||||
"""
|
||||
lang = get_language()
|
||||
select2_js = (settings.SELECT2_JS,) if settings.SELECT2_JS else ()
|
||||
select2_css = (settings.SELECT2_CSS,) if settings.SELECT2_CSS else ()
|
||||
|
||||
i18n_name = SELECT2_TRANSLATIONS.get(lang)
|
||||
if i18n_name not in settings.SELECT2_I18N_AVAILABLE_LANGUAGES:
|
||||
i18n_name = None
|
||||
|
||||
i18n_file = ('%s/%s.js' % (settings.SELECT2_I18N_PATH, i18n_name),) if i18n_name else ()
|
||||
|
||||
return forms.Media(
|
||||
js=('//cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/js/select2.min.js',
|
||||
'django_select2/django_select2.js'),
|
||||
css={'screen': ('//cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/css/select2.min.css',)}
|
||||
js=select2_js + i18n_file + ('django_select2/django_select2.js',),
|
||||
css={'screen': select2_css}
|
||||
)
|
||||
|
||||
media = property(_get_media)
|
||||
|
||||
|
||||
class Select2Widget(Select2Mixin, forms.Select):
|
||||
class Select2TagMixin:
|
||||
"""Mixin to add select2 tag functionality."""
|
||||
|
||||
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(*args, **kwargs)
|
||||
|
||||
|
||||
class Select2Widget(Select2Mixin, forms.Select):
|
||||
"""
|
||||
Select2 drop in widget.
|
||||
|
||||
@@ -135,7 +153,6 @@ class Select2Widget(Select2Mixin, forms.Select):
|
||||
|
||||
|
||||
class Select2MultipleWidget(Select2Mixin, forms.SelectMultiple):
|
||||
|
||||
"""
|
||||
Select2 drop in widget for multiple select.
|
||||
|
||||
@@ -145,36 +162,73 @@ class Select2MultipleWidget(Select2Mixin, forms.SelectMultiple):
|
||||
pass
|
||||
|
||||
|
||||
class HeavySelect2Mixin(Select2Mixin):
|
||||
class Select2TagWidget(Select2TagMixin, Select2Mixin, forms.SelectMultiple):
|
||||
"""
|
||||
Select2 drop in widget for for tagging.
|
||||
|
||||
"""Mixin that adds select2's ajax options and registers itself on django's cache."""
|
||||
Example for :class:`.django.contrib.postgres.fields.ArrayField`::
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
class MyWidget(Select2TagWidget):
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
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
|
||||
|
||||
|
||||
class HeavySelect2Mixin:
|
||||
"""Mixin that adds select2's AJAX options and registers itself on Django's cache."""
|
||||
|
||||
dependent_fields = {}
|
||||
|
||||
def __init__(self, attrs=None, choices=(), **kwargs):
|
||||
"""
|
||||
Return HeavySelect2Mixin.
|
||||
|
||||
:param data_view: url pattern name
|
||||
:type data_view: str
|
||||
:param data_url: url
|
||||
:type data_url: str
|
||||
:return:
|
||||
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`."""
|
||||
"""Return URL from instance or by reversing :attr:`.data_view`."""
|
||||
if self.data_url:
|
||||
return self.data_url
|
||||
return reverse_lazy(self.data_view)
|
||||
return reverse(self.data_view)
|
||||
|
||||
def build_attrs(self, extra_attrs=None, **kwargs):
|
||||
"""Set select2's ajax attributes."""
|
||||
attrs = super(HeavySelect2Mixin, self).build_attrs(extra_attrs=extra_attrs, **kwargs)
|
||||
def build_attrs(self, *args, **kwargs):
|
||||
"""Set select2's AJAX attributes."""
|
||||
attrs = super(HeavySelect2Mixin, self).build_attrs(*args, **kwargs)
|
||||
|
||||
# encrypt instance Id
|
||||
self.widget_id = signing.dumps(id(self))
|
||||
@@ -184,13 +238,15 @@ class HeavySelect2Mixin(Select2Mixin):
|
||||
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
|
||||
|
||||
def render(self, name, value, attrs=None, choices=()):
|
||||
def render(self, *args, **kwargs):
|
||||
"""Render widget and register it in Django's cache."""
|
||||
output = super(HeavySelect2Mixin, self).render(name, value, attrs=attrs, choices=choices)
|
||||
output = super(HeavySelect2Mixin, self).render(*args, **kwargs)
|
||||
self.set_to_cache()
|
||||
return output
|
||||
|
||||
@@ -198,34 +254,36 @@ class HeavySelect2Mixin(Select2Mixin):
|
||||
return "%s%s" % (settings.SELECT2_CACHE_PREFIX, id(self))
|
||||
|
||||
def set_to_cache(self):
|
||||
"""Add widget object to Djnago's cache."""
|
||||
cache.set(self._get_cache_key(), self)
|
||||
"""
|
||||
Add widget object to Django's cache.
|
||||
|
||||
def render_options(self, choices, selected_choices):
|
||||
"""Render only selected options."""
|
||||
output = ['<option></option>' if not self.is_required else '']
|
||||
choices = {(k, v) for k, v in choices if k in selected_choices}
|
||||
selected_choices = {force_text(v) for v 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)
|
||||
You may need to overwrite this method, to pickle all information
|
||||
that is required to serve your JSON response view.
|
||||
"""
|
||||
try:
|
||||
cache.set(self._get_cache_key(), {
|
||||
'widget': self,
|
||||
'url': self.get_url(),
|
||||
})
|
||||
except (PicklingError, AttributeError):
|
||||
msg = "You need to overwrite \"set_to_cache\" or ensure that %s is serialisable."
|
||||
raise NotImplementedError(msg % self.__class__.__name__)
|
||||
|
||||
|
||||
class HeavySelect2Widget(HeavySelect2Mixin, forms.Select):
|
||||
|
||||
class HeavySelect2Widget(HeavySelect2Mixin, Select2Widget):
|
||||
"""
|
||||
Select2 widget with AJAX support that registers itself to Django's Cache.
|
||||
|
||||
Usage example::
|
||||
|
||||
class MyWidget(HeavySelectWidget):
|
||||
class MyWidget(HeavySelect2Widget):
|
||||
data_view = 'my_view_name'
|
||||
|
||||
or::
|
||||
|
||||
class MyForm(forms.Form):
|
||||
my_field = forms.ChoicesField(
|
||||
widget=HeavySelectWidget(
|
||||
my_field = forms.ChoiceField(
|
||||
widget=HeavySelect2Widget(
|
||||
data_url='/url/to/json/response'
|
||||
)
|
||||
)
|
||||
@@ -235,38 +293,29 @@ class HeavySelect2Widget(HeavySelect2Mixin, forms.Select):
|
||||
pass
|
||||
|
||||
|
||||
class HeavySelect2MultipleWidget(HeavySelect2Mixin, forms.SelectMultiple):
|
||||
|
||||
class HeavySelect2MultipleWidget(HeavySelect2Mixin, Select2MultipleWidget):
|
||||
"""Select2 multi select widget similar to :class:`.HeavySelect2Widget`."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class HeavySelect2TagWidget(HeavySelect2MultipleWidget):
|
||||
class HeavySelect2TagWidget(HeavySelect2Mixin, Select2TagWidget):
|
||||
"""Select2 tag widget."""
|
||||
|
||||
"""Mixin to add select2 tag functionality."""
|
||||
|
||||
def build_attrs(self, extra_attrs=None, **kwargs):
|
||||
"""Add select2's tag attributes."""
|
||||
attrs = super(HeavySelect2TagWidget, self).build_attrs(extra_attrs, **kwargs)
|
||||
attrs['data-minimum-input-length'] = 1
|
||||
attrs['data-tags'] = 'true'
|
||||
attrs['data-token-separators'] = [",", " "]
|
||||
return attrs
|
||||
pass
|
||||
|
||||
|
||||
# Auto Heavy widgets
|
||||
|
||||
|
||||
class ModelSelect2Mixin(object):
|
||||
|
||||
class ModelSelect2Mixin:
|
||||
"""Widget mixin that provides attributes and methods for :class:`.AutoResponseView`."""
|
||||
|
||||
model = None
|
||||
queryset = None
|
||||
search_fields = []
|
||||
"""
|
||||
Model lookups that are used to filter the queryset.
|
||||
Model lookups that are used to filter the QuerySet.
|
||||
|
||||
Example::
|
||||
|
||||
@@ -283,28 +332,26 @@ class ModelSelect2Mixin(object):
|
||||
"""
|
||||
Overwrite class parameters if passed as keyword arguments.
|
||||
|
||||
:param model: model to select choices from
|
||||
:type model: django.db.models.Model
|
||||
:param queryset: queryset to select choices from
|
||||
:type queryset: django.db.models.query.QuerySet
|
||||
:param search_fields: list of model lookup strings
|
||||
:type search_fields: list
|
||||
:param max_results: max. JsonResponse view page size
|
||||
:type max_results: int
|
||||
Args:
|
||||
model (django.db.models.Model): Model 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.
|
||||
|
||||
"""
|
||||
self.model = kwargs.pop('model', self.model)
|
||||
self.queryset = kwargs.pop('queryset', self.queryset)
|
||||
self.search_fields = kwargs.pop('search_fields', self.search_fields)
|
||||
self.max_results = kwargs.pop('max_results', self.max_results)
|
||||
defaults = {'data_view': 'django_select2-json'}
|
||||
defaults = {'data_view': 'django_select2:auto-json'}
|
||||
defaults.update(kwargs)
|
||||
super(ModelSelect2Mixin, self).__init__(*args, **defaults)
|
||||
|
||||
def set_to_cache(self):
|
||||
"""
|
||||
Add widget's attributes to Djnago's cache.
|
||||
Add widget's attributes to Django's cache.
|
||||
|
||||
Split the queryset, to not pickle the result set.
|
||||
Split the QuerySet, to not pickle the result set.
|
||||
"""
|
||||
queryset = self.get_queryset()
|
||||
cache.set(self._get_cache_key(), {
|
||||
@@ -314,20 +361,30 @@ class ModelSelect2Mixin(object):
|
||||
queryset.query,
|
||||
],
|
||||
'cls': self.__class__,
|
||||
'search_fields': self.search_fields,
|
||||
'max_results': self.max_results,
|
||||
'search_fields': tuple(self.search_fields),
|
||||
'max_results': int(self.max_results),
|
||||
'url': str(self.get_url()),
|
||||
'dependent_fields': dict(self.dependent_fields),
|
||||
})
|
||||
|
||||
def filter_queryset(self, term, queryset=None):
|
||||
def filter_queryset(self, request, term, queryset=None, **dependent_fields):
|
||||
"""
|
||||
Return queryset filtered by search_fields matching the passed term.
|
||||
Return QuerySet filtered by search_fields matching the passed term.
|
||||
|
||||
Args:
|
||||
request (django.http.request.HttpRequest): The request is being passed from
|
||||
the JSON view and can be used to dynamically alter the response queryset.
|
||||
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
|
||||
everything from keyword arguments that is not a dependent field.
|
||||
|
||||
Returns:
|
||||
QuerySet: Filtered QuerySet
|
||||
|
||||
:param term: Search term
|
||||
:type term: str
|
||||
:return: Filtered queryset
|
||||
:rtype: :class:`.django.db.models.QuerySet`
|
||||
"""
|
||||
if not queryset:
|
||||
if queryset is None:
|
||||
queryset = self.get_queryset()
|
||||
search_fields = self.get_search_fields()
|
||||
select = Q()
|
||||
@@ -335,15 +392,19 @@ class ModelSelect2Mixin(object):
|
||||
term = term.replace('\n', ' ')
|
||||
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.pop(): t}))
|
||||
Q(**{search_fields[0]: t}))
|
||||
if dependent_fields:
|
||||
select &= Q(**dependent_fields)
|
||||
|
||||
return queryset.filter(select).distinct()
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
Return queryset based on :attr:`.queryset` or :attr:`.model`.
|
||||
Return QuerySet based on :attr:`.queryset` or :attr:`.model`.
|
||||
|
||||
Returns:
|
||||
QuerySet: QuerySet of available choices.
|
||||
|
||||
:return: queryset of available choices
|
||||
:rtype: :class:`.django.db.models.QuerySet`
|
||||
"""
|
||||
if self.queryset is not None:
|
||||
queryset = self.queryset
|
||||
@@ -365,26 +426,60 @@ class ModelSelect2Mixin(object):
|
||||
return self.search_fields
|
||||
raise NotImplementedError('%s, must implement "search_fields".' % self.__class__.__name__)
|
||||
|
||||
def render_options(self, choices, selected_choices):
|
||||
"""Render only selected options and set queryset from :class:`ModelChoicesIterator`."""
|
||||
output = ['<option></option>' if not self.is_required else '']
|
||||
if isinstance(self.choices, ModelChoiceIterator):
|
||||
if not self.queryset:
|
||||
self.queryset = self.choices.queryset
|
||||
selected_choices = {c for c in selected_choices
|
||||
if c not in self.choices.field.empty_values}
|
||||
choices = {self.choices.choice(obj)
|
||||
for obj in self.choices.queryset.filter(pk__in=selected_choices)}
|
||||
else:
|
||||
choices = {(k, v) for k, v in choices if k in selected_choices}
|
||||
selected_choices = {force_text(v) for v 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)
|
||||
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 = {str(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
|
||||
}
|
||||
field_name = self.choices.field.to_field_name or 'pk'
|
||||
query = Q(**{'%s__in' % field_name: selected_choices})
|
||||
for obj in self.choices.queryset.filter(query):
|
||||
option_value = self.choices.choice(obj)[0]
|
||||
option_label = self.label_from_instance(obj)
|
||||
|
||||
selected = (
|
||||
str(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 label_from_instance(self, obj):
|
||||
"""
|
||||
Return option label representation from instance.
|
||||
|
||||
Can be overridden to change the representation of each choice.
|
||||
|
||||
Example usage::
|
||||
|
||||
class MyWidget(ModelSelect2Widget):
|
||||
def label_from_instance(obj):
|
||||
return str(obj.title).upper()
|
||||
|
||||
Args:
|
||||
obj (django.db.models.Model): Instance of Django Model.
|
||||
|
||||
Returns:
|
||||
str: Option label.
|
||||
|
||||
"""
|
||||
return str(obj)
|
||||
|
||||
|
||||
class ModelSelect2Widget(ModelSelect2Mixin, HeavySelect2Widget):
|
||||
|
||||
"""
|
||||
Select2 drop in model select widget.
|
||||
|
||||
@@ -392,7 +487,7 @@ class ModelSelect2Widget(ModelSelect2Mixin, HeavySelect2Widget):
|
||||
|
||||
class MyWidget(ModelSelect2Widget):
|
||||
search_fields = [
|
||||
'title__icontians',
|
||||
'title__icontains',
|
||||
]
|
||||
|
||||
class MyModelForm(forms.ModelForm):
|
||||
@@ -414,8 +509,8 @@ class ModelSelect2Widget(ModelSelect2Mixin, HeavySelect2Widget):
|
||||
)
|
||||
|
||||
.. tip:: The ModelSelect2(Multiple)Widget will try
|
||||
to get the queryset from the fields choices.
|
||||
Therefore you don't need to define a queryset,
|
||||
to get the QuerySet from the fields choices.
|
||||
Therefore you don't need to define a QuerySet,
|
||||
if you just drop in the widget for a ForeignKey field.
|
||||
"""
|
||||
|
||||
@@ -423,7 +518,6 @@ class ModelSelect2Widget(ModelSelect2Mixin, HeavySelect2Widget):
|
||||
|
||||
|
||||
class ModelSelect2MultipleWidget(ModelSelect2Mixin, HeavySelect2MultipleWidget):
|
||||
|
||||
"""
|
||||
Select2 drop in model multiple select widget.
|
||||
|
||||
@@ -434,13 +528,12 @@ class ModelSelect2MultipleWidget(ModelSelect2Mixin, HeavySelect2MultipleWidget):
|
||||
|
||||
|
||||
class ModelSelect2TagWidget(ModelSelect2Mixin, HeavySelect2TagWidget):
|
||||
|
||||
"""
|
||||
Select2 model field with tag support.
|
||||
Select2 model widget with tag support.
|
||||
|
||||
This it not a simple drop in widget.
|
||||
It requires to implement you own :func:`.value_from_datadict`
|
||||
that adds missing tags to you queryset.
|
||||
that adds missing tags to you QuerySet.
|
||||
|
||||
Example::
|
||||
|
||||
@@ -448,12 +541,12 @@ class ModelSelect2TagWidget(ModelSelect2Mixin, HeavySelect2TagWidget):
|
||||
queryset = MyModel.objects.all()
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
values = super().value_from_datadict(self, data, files, name):
|
||||
values = super().value_from_datadict(self, data, files, name)
|
||||
qs = self.queryset.filter(**{'pk__in': list(values)})
|
||||
pks = set(force_text(getattr(o, pk)) for o in qs)
|
||||
pks = set(str(getattr(o, pk)) for o in qs)
|
||||
cleaned_values = []
|
||||
for val in value:
|
||||
if force_text(val) not in pks:
|
||||
if str(val) not in pks:
|
||||
val = queryset.create(title=val).pk
|
||||
cleaned_values.append(val)
|
||||
return cleaned_values
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
"""Django legacy file."""
|
||||
@@ -1,23 +1,69 @@
|
||||
$(function () {
|
||||
$('.django-select2').not('django-select2-heavy').select2();
|
||||
$('.django-select2.django-select2-heavy').each(function () {
|
||||
var field_id = $(this).data('field_id');
|
||||
$(this).select2({
|
||||
ajax: {
|
||||
data: function (params) {
|
||||
return {
|
||||
term: params.term,
|
||||
page: params.page,
|
||||
field_id: field_id
|
||||
};
|
||||
},
|
||||
processResults: function (data, page) {
|
||||
return {
|
||||
results: data.results
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
/* global define, jQuery */
|
||||
(function (factory) {
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
define(['jquery'], factory)
|
||||
} else if (typeof module === 'object' && module.exports) {
|
||||
module.exports = factory(require('jquery'))
|
||||
} else {
|
||||
// Browser globals
|
||||
factory(jQuery)
|
||||
}
|
||||
}(function ($) {
|
||||
'use strict'
|
||||
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 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)
|
||||
|
||||
$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
|
||||
}
|
||||
|
||||
$(function () {
|
||||
$('.django-select2').djangoSelect2()
|
||||
})
|
||||
|
||||
return $.fn.djangoSelect2
|
||||
}))
|
||||
|
||||
+12
-12
@@ -1,20 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Django-Select2 url config.
|
||||
Django-Select2 URL configuration.
|
||||
|
||||
Add `django_select` to your urlconf **if** you use any 'Model' fields::
|
||||
Add `django_select` to your ``urlconf`` **if** you use any 'Model' fields::
|
||||
|
||||
url(r'^select2/', include('django_select2.urls')),
|
||||
from django.urls import path
|
||||
|
||||
|
||||
path('select2/', include('django_select2.urls')),
|
||||
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.conf.urls import patterns, url
|
||||
from django.urls import path
|
||||
|
||||
from .views import AutoResponseView
|
||||
|
||||
urlpatterns = patterns(
|
||||
"",
|
||||
url(r"^fields/auto.json$",
|
||||
AutoResponseView.as_view(), name="django_select2-json"),
|
||||
)
|
||||
app_name = 'django_select2'
|
||||
|
||||
urlpatterns = [
|
||||
path("fields/auto.json", AutoResponseView.as_view(), name="auto-json"),
|
||||
]
|
||||
|
||||
+18
-12
@@ -1,11 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""JSONResponse views for model widgets."""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.core import signing
|
||||
from django.core.signing import BadSignature
|
||||
from django.http import Http404, JsonResponse
|
||||
from django.utils.encoding import smart_text
|
||||
from django.views.generic.list import BaseListView
|
||||
|
||||
from .cache import cache
|
||||
@@ -13,7 +9,6 @@ from .conf import settings
|
||||
|
||||
|
||||
class AutoResponseView(BaseListView):
|
||||
|
||||
"""
|
||||
View that handles requests from heavy model widgets.
|
||||
|
||||
@@ -32,7 +27,8 @@ class AutoResponseView(BaseListView):
|
||||
'text': "foo",
|
||||
'id': 123
|
||||
}
|
||||
]
|
||||
],
|
||||
'more': true
|
||||
}
|
||||
|
||||
"""
|
||||
@@ -43,16 +39,22 @@ class AutoResponseView(BaseListView):
|
||||
return JsonResponse({
|
||||
'results': [
|
||||
{
|
||||
'text': smart_text(obj),
|
||||
'text': self.widget.label_from_instance(obj),
|
||||
'id': obj.pk,
|
||||
}
|
||||
for obj in context['object_list']
|
||||
],
|
||||
'more': context['page_obj'].has_next()
|
||||
})
|
||||
|
||||
def get_queryset(self):
|
||||
"""Get queryset from cached widget."""
|
||||
return self.widget.filter_queryset(self.term, self.queryset)
|
||||
"""Get QuerySet from cached widget."""
|
||||
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.request, self.term, self.queryset, **kwargs)
|
||||
|
||||
def get_paginate_by(self, queryset):
|
||||
"""Paginate response by size of widget's `max_results` parameter."""
|
||||
@@ -62,10 +64,12 @@ class AutoResponseView(BaseListView):
|
||||
"""
|
||||
Get and return widget from cache.
|
||||
|
||||
Raises a 404 if the widget can not be found or no id is provided.
|
||||
Raises:
|
||||
Http404: If if the widget can not be found or no id is provided.
|
||||
|
||||
Returns:
|
||||
ModelSelect2Mixin: Widget from cache.
|
||||
|
||||
:raises: Http404
|
||||
:return: ModelSelect2Mixin
|
||||
"""
|
||||
field_id = self.kwargs.get('field_id', self.request.GET.get('field_id', None))
|
||||
if not field_id:
|
||||
@@ -79,6 +83,8 @@ class AutoResponseView(BaseListView):
|
||||
widget_dict = cache.get(cache_key)
|
||||
if widget_dict is None:
|
||||
raise Http404('field_id not found')
|
||||
if widget_dict.pop('url') != self.request.path:
|
||||
raise Http404('field_id was issued for the view.')
|
||||
qs, qs.query = widget_dict.pop('queryset')
|
||||
self.queryset = qs.all()
|
||||
widget_dict['queryset'] = self.queryset
|
||||
|
||||
-153
@@ -1,153 +0,0 @@
|
||||
# Makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
PAPER =
|
||||
BUILDDIR = _build
|
||||
|
||||
# Internal variables.
|
||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||
PAPEROPT_letter = -D latex_paper_size=letter
|
||||
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
# the i18n builder cannot share the environment and doctrees with the others
|
||||
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
|
||||
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
|
||||
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@echo " html to make standalone HTML files"
|
||||
@echo " dirhtml to make HTML files named index.html in directories"
|
||||
@echo " singlehtml to make a single large HTML file"
|
||||
@echo " pickle to make pickle files"
|
||||
@echo " json to make JSON files"
|
||||
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||
@echo " qthelp to make HTML files and a qthelp project"
|
||||
@echo " devhelp to make HTML files and a Devhelp project"
|
||||
@echo " epub to make an epub"
|
||||
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
||||
@echo " text to make text files"
|
||||
@echo " man to make manual pages"
|
||||
@echo " texinfo to make Texinfo files"
|
||||
@echo " info to make Texinfo files and run them through makeinfo"
|
||||
@echo " gettext to make PO message catalogs"
|
||||
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||
@echo " linkcheck to check all external links for integrity"
|
||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||
|
||||
clean:
|
||||
-rm -rf $(BUILDDIR)/*
|
||||
|
||||
html:
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||
|
||||
dirhtml:
|
||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||
|
||||
singlehtml:
|
||||
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
||||
|
||||
pickle:
|
||||
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||
@echo
|
||||
@echo "Build finished; now you can process the pickle files."
|
||||
|
||||
json:
|
||||
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||
@echo
|
||||
@echo "Build finished; now you can process the JSON files."
|
||||
|
||||
htmlhelp:
|
||||
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||
|
||||
qthelp:
|
||||
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Django-Select2.qhcp"
|
||||
@echo "To view the help file:"
|
||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Django-Select2.qhc"
|
||||
|
||||
devhelp:
|
||||
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||
@echo
|
||||
@echo "Build finished."
|
||||
@echo "To view the help file:"
|
||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/Django-Select2"
|
||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Django-Select2"
|
||||
@echo "# devhelp"
|
||||
|
||||
epub:
|
||||
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
||||
@echo
|
||||
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
||||
|
||||
latex:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo
|
||||
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
||||
"(use \`make latexpdf' here to do that automatically)."
|
||||
|
||||
latexpdf:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through pdflatex..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
text:
|
||||
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
||||
@echo
|
||||
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
||||
|
||||
man:
|
||||
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
||||
@echo
|
||||
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
||||
|
||||
texinfo:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo
|
||||
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
||||
@echo "Run \`make' in that directory to run these through makeinfo" \
|
||||
"(use \`make info' here to do that automatically)."
|
||||
|
||||
info:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo "Running Texinfo files through makeinfo..."
|
||||
make -C $(BUILDDIR)/texinfo info
|
||||
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
||||
|
||||
gettext:
|
||||
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
||||
@echo
|
||||
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
||||
|
||||
changes:
|
||||
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||
@echo
|
||||
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||
|
||||
linkcheck:
|
||||
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||
@echo
|
||||
@echo "Link check complete; look for any errors in the above output " \
|
||||
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||
|
||||
doctest:
|
||||
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||
@echo "Testing of doctests in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/doctest/output.txt."
|
||||
+18
-255
@@ -1,25 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Django-Select2 documentation build configuration file, created by
|
||||
# sphinx-quickstart on Sat Aug 25 10:23:46 2012.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
|
||||
|
||||
year = datetime.datetime.now().strftime("%Y")
|
||||
|
||||
# This is needed since django_select2 requires django model modules
|
||||
# and those modules assume that django settings is configured and
|
||||
# have proper DB settings.
|
||||
@@ -32,95 +14,31 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.testapp.settings")
|
||||
sys.path.insert(0, os.path.abspath('../tests.testapp'))
|
||||
sys.path.insert(0, os.path.abspath('..'))
|
||||
|
||||
|
||||
|
||||
# -- General configuration -----------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.inheritance_diagram', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode']
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.napoleon',
|
||||
'sphinx.ext.inheritance_diagram',
|
||||
'sphinx.ext.intersphinx',
|
||||
'sphinx.ext.viewcode',
|
||||
'sphinxcontrib.spelling',
|
||||
]
|
||||
|
||||
intersphinx_mapping = {
|
||||
'python': ('http://docs.python.org/3.5', None),
|
||||
'django': ('https://docs.djangoproject.com/en/dev/',
|
||||
'https://docs.djangoproject.com/en/dev/_objects/'),
|
||||
'python': ('http://docs.python.org/3', None),
|
||||
'django': ('https://docs.djangoproject.com/en/stable/',
|
||||
'https://docs.djangoproject.com/en/stable/_objects/'),
|
||||
}
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
# spell check
|
||||
spelling_word_list_filename = 'spelling_wordlist.txt'
|
||||
spelling_show_suggestions = True
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The encoding of source files.
|
||||
#source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'Django-Select2'
|
||||
copyright = u'%s, Nirupam Biswas' % year
|
||||
|
||||
|
||||
def get_version():
|
||||
import django_select2
|
||||
|
||||
return django_select2.__version__
|
||||
|
||||
# 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 = get_version()
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = version
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
#today = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
#today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = ['_build']
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all documents.
|
||||
#default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
#add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
#add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
#show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
#modindex_common_prefix = []
|
||||
|
||||
def skip(app, what, name, obj, skip, options):
|
||||
if name == "__init__" and obj.__doc__:
|
||||
return False
|
||||
return skip
|
||||
|
||||
def setup(app):
|
||||
app.connect("autodoc-skip-member", skip)
|
||||
project = 'django-select2'
|
||||
year = datetime.datetime.now().strftime("%Y")
|
||||
copyright = '%s, Johannes Hoppe' % year
|
||||
|
||||
autodoc_default_flags = ['members', 'show-inheritance']
|
||||
autodoc_member_order = 'bysource'
|
||||
@@ -132,159 +50,4 @@ inheritance_node_attrs = dict(shape='rect', fontsize=14, fillcolor='gray90',
|
||||
|
||||
inheritance_edge_attrs = dict(penwidth=0.75)
|
||||
|
||||
# -- Options for HTML output ---------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
if on_rtd:
|
||||
html_theme = 'default'
|
||||
else:
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
#html_theme_path = []
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
#html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
#html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
#html_logo = None
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
#html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
#html_static_path = ['_static']
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
#html_last_updated_fmt = '%b %d, %Y'
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
#html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
#html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
#html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
#html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
#html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
#html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
#html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
#html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
#html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
#html_file_suffix = None
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'Django-Select2doc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output --------------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||
latex_documents = [
|
||||
('index', 'Django-Select2.tex', u'Django-Select2 Documentation',
|
||||
u'Nirupam Biswas', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
#latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
#latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
#latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#latex_show_urls = False
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output --------------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'django-select2', u'Django-Select2 Documentation',
|
||||
[u'Nirupam Biswas'], 1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#man_show_urls = False
|
||||
|
||||
|
||||
# -- Options for Texinfo output ------------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
('index', 'Django-Select2', u'Django-Select2 Documentation',
|
||||
u'Nirupam Biswas', 'Django-Select2', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#texinfo_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#texinfo_domain_indices = True
|
||||
|
||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||
#texinfo_show_urls = 'footnote'
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
|
||||
@@ -40,3 +40,47 @@ Cache
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
JavaScript
|
||||
----------
|
||||
|
||||
DjangoSelect2 handles the initialization of select2 fields automatically. Just include
|
||||
``{{ form.media.js }}`` in your template before the closing ``body`` tag. That's it!
|
||||
|
||||
If you insert forms after page load or if you want to handle the initialization
|
||||
yourself, DjangoSelect2 provides a jQuery plugin. It will handle both normal and
|
||||
heavy fields. Simply call ``djangoSelect2(options)`` on your select fields.::
|
||||
|
||||
$('.django-select2').djangoSelect2();
|
||||
|
||||
|
||||
You can pass see `Select2 options <https://select2.github.io/options.html>`_ if needed::
|
||||
|
||||
$('.django-select2').djangoSelect2({placeholder: 'Select an option'});
|
||||
|
||||
Security & Authentication
|
||||
-------------------------
|
||||
|
||||
Security is important. Therefore make sure to read and understand what
|
||||
the security measures in place and their limitations.
|
||||
|
||||
Set up a separate cache. If you have a public form that uses a model widget
|
||||
make sure to setup a separate cache database for Select2. An attacker
|
||||
could constantly reload your site and fill up the select2 cache.
|
||||
Having a separate cache allows you to limit the effect to select2 only.
|
||||
|
||||
You might want to add a secure select2 JSON endpoint for data you don't
|
||||
want to be accessible to the general public. Doing so is easy::
|
||||
|
||||
class UserSelect2View(LoginRequiredMixin, AutoResponseView):
|
||||
pass
|
||||
|
||||
class UserSelect2WidgetMixin(object):
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['data_view'] = 'user-select2-view'
|
||||
super(UserSelect2WidgetMixin, self).__init__(*args, **kwargs)
|
||||
|
||||
class MySecretWidget(UserSelect2WidgetMixin, Select2ModelWidget):
|
||||
model = MySecretModel
|
||||
search_fields = ['title__icontains']
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Django >= 1.4.1
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
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(
|
||||
model=Country,
|
||||
search_fields=['name__icontains'],
|
||||
)
|
||||
)
|
||||
|
||||
city = forms.ModelChoiceField(
|
||||
queryset=City.objects.all(),
|
||||
label=u"City",
|
||||
widget=ModelSelect2Widget(
|
||||
model=City,
|
||||
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'},
|
||||
)
|
||||
)
|
||||
|
||||
+32
-1
@@ -18,10 +18,41 @@ Installation
|
||||
2. Add ``django_select2`` to your ``INSTALLED_APPS`` in your project settings.
|
||||
|
||||
|
||||
3. Add ``django_select`` to your urlconf **if** you use any 'Auto' fields::
|
||||
3. Add ``django_select`` to your ``urlconf`` **if** you use any
|
||||
:class:`ModelWidgets <.django_select2.forms.ModelSelect2Mixin>`::
|
||||
|
||||
url(r'^select2/', include('django_select2.urls')),
|
||||
|
||||
Quick Start
|
||||
-----------
|
||||
|
||||
Here is a quick example to get you started:
|
||||
|
||||
0. Follow the installation instructions above.
|
||||
|
||||
1. Add a select2 widget to the form. For example if you wanted Select2 with multi-select you would use
|
||||
``Select2MultipleWidget``
|
||||
Replacing::
|
||||
|
||||
class MyForm(forms.Form):
|
||||
things = ModelMultipleChoiceField(queryset=Thing.objects.all())
|
||||
|
||||
with::
|
||||
|
||||
from django_select2.forms import Select2MultipleWidget
|
||||
|
||||
class MyForm(forms.Form):
|
||||
things = ModelMultipleChoiceField(queryset=Thing.objects.all(), widget=Select2MultipleWidget)
|
||||
|
||||
2. Add the CSS to the ``head`` of your Django template::
|
||||
|
||||
{{ form.media.css }}
|
||||
|
||||
3. Add the JavaScript to the end of the ``body`` of your Django template::
|
||||
|
||||
{{ form.media.js }}
|
||||
|
||||
4. Done - enjoy the wonders of Select2!
|
||||
|
||||
External Dependencies
|
||||
---------------------
|
||||
|
||||
@@ -14,6 +14,7 @@ Contents:
|
||||
|
||||
get_started
|
||||
django_select2
|
||||
extra
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
-190
@@ -1,190 +0,0 @@
|
||||
@ECHO OFF
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set BUILDDIR=_build
|
||||
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
|
||||
set I18NSPHINXOPTS=%SPHINXOPTS% .
|
||||
if NOT "%PAPER%" == "" (
|
||||
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
|
||||
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
|
||||
)
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
if "%1" == "help" (
|
||||
:help
|
||||
echo.Please use `make ^<target^>` where ^<target^> is one of
|
||||
echo. html to make standalone HTML files
|
||||
echo. dirhtml to make HTML files named index.html in directories
|
||||
echo. singlehtml to make a single large HTML file
|
||||
echo. pickle to make pickle files
|
||||
echo. json to make JSON files
|
||||
echo. htmlhelp to make HTML files and a HTML help project
|
||||
echo. qthelp to make HTML files and a qthelp project
|
||||
echo. devhelp to make HTML files and a Devhelp project
|
||||
echo. epub to make an epub
|
||||
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
|
||||
echo. text to make text files
|
||||
echo. man to make manual pages
|
||||
echo. texinfo to make Texinfo files
|
||||
echo. gettext to make PO message catalogs
|
||||
echo. changes to make an overview over all changed/added/deprecated items
|
||||
echo. linkcheck to check all external links for integrity
|
||||
echo. doctest to run all doctests embedded in the documentation if enabled
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "clean" (
|
||||
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
|
||||
del /q /s %BUILDDIR%\*
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "html" (
|
||||
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "dirhtml" (
|
||||
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "singlehtml" (
|
||||
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "pickle" (
|
||||
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can process the pickle files.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "json" (
|
||||
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can process the JSON files.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "htmlhelp" (
|
||||
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can run HTML Help Workshop with the ^
|
||||
.hhp project file in %BUILDDIR%/htmlhelp.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "qthelp" (
|
||||
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can run "qcollectiongenerator" with the ^
|
||||
.qhcp project file in %BUILDDIR%/qthelp, like this:
|
||||
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Django-Select2.qhcp
|
||||
echo.To view the help file:
|
||||
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Django-Select2.ghc
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "devhelp" (
|
||||
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "epub" (
|
||||
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The epub file is in %BUILDDIR%/epub.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "latex" (
|
||||
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "text" (
|
||||
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The text files are in %BUILDDIR%/text.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "man" (
|
||||
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The manual pages are in %BUILDDIR%/man.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "texinfo" (
|
||||
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "gettext" (
|
||||
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "changes" (
|
||||
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.The overview file is in %BUILDDIR%/changes.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "linkcheck" (
|
||||
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Link check complete; look for any errors in the above output ^
|
||||
or in %BUILDDIR%/linkcheck/output.txt.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "doctest" (
|
||||
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Testing of doctests in the sources finished, look at the ^
|
||||
results in %BUILDDIR%/doctest/output.txt.
|
||||
goto end
|
||||
)
|
||||
|
||||
:end
|
||||
@@ -0,0 +1,18 @@
|
||||
jQuery
|
||||
Django
|
||||
mixin
|
||||
backend
|
||||
redis
|
||||
memcached
|
||||
AJAX
|
||||
Cloudflare
|
||||
lookup
|
||||
QuerySet
|
||||
pre
|
||||
py
|
||||
lookups
|
||||
functionalities
|
||||
plugin
|
||||
multi
|
||||
Indices
|
||||
clearable
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "django-select2",
|
||||
"version": "0.0.0",
|
||||
"description": "This is a Django integration of Select2.",
|
||||
"files": [
|
||||
"django_select2/static/**/*"
|
||||
],
|
||||
"main": "django_select2/static/django_select2/django_select2.js",
|
||||
"directories": {
|
||||
"doc": "docs",
|
||||
"test": "tests"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "standard"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/applegrew/django-select2.git"
|
||||
},
|
||||
"keywords": [
|
||||
"django",
|
||||
"select2"
|
||||
],
|
||||
"author": "Johannes Hoppe",
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/applegrew/django-select2/issues"
|
||||
},
|
||||
"homepage": "https://github.com/applegrew/django-select2#readme",
|
||||
"peerDependencies": {
|
||||
"select2": "*",
|
||||
"jquery": ">= 1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"standard": "*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
coverage
|
||||
django-appconf
|
||||
isort
|
||||
pytest
|
||||
pytest-django
|
||||
selenium
|
||||
sphinx
|
||||
sphinx_rtd_theme
|
||||
sphinxcontrib-spelling
|
||||
pyenchant
|
||||
django-redis
|
||||
@@ -0,0 +1 @@
|
||||
django-appconf>=0.6.0
|
||||
@@ -1,19 +0,0 @@
|
||||
-e .
|
||||
django-appconf==1.0.1
|
||||
django==1.8.4 # via model-mommy
|
||||
flake8==2.4.1
|
||||
isort==4.2.2
|
||||
mccabe==0.3.1 # via flake8
|
||||
mock==1.3.0 # via model-mommy
|
||||
model-mommy==1.2.5
|
||||
pbr==1.8.0 # via mock
|
||||
pep257==0.5.0
|
||||
pep8-naming==0.3.3
|
||||
pep8==1.5.7 # via flake8
|
||||
py==1.4.30 # via pytest
|
||||
pyflakes==0.8.1 # via flake8
|
||||
pytest-django==2.8.0
|
||||
pytest==2.7.2
|
||||
requests==2.7.0
|
||||
selenium==2.46.0
|
||||
six==1.9.0 # via django-appconf, mock, model-mommy
|
||||
-3086
File diff suppressed because it is too large
Load Diff
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Set the version in NPM's package.json to match the git tag."""
|
||||
import json
|
||||
import os
|
||||
|
||||
if __name__ == '__main__':
|
||||
with open('package.json', 'r+') as f:
|
||||
data = json.load(f)
|
||||
f.seek(0)
|
||||
data['version'] = os.environ['TRAVIS_TAG']
|
||||
json.dump(data, f)
|
||||
f.truncate()
|
||||
@@ -1,5 +1,37 @@
|
||||
[pytest]
|
||||
norecursedirs = env
|
||||
[metadata]
|
||||
name = django-select2
|
||||
author = Johannes Hoppe
|
||||
author-email = info@johanneshoppe.com
|
||||
summary = Select2 option fields for Django.
|
||||
description-file = README.rst
|
||||
home-page = https://github.com/applegrew/django-select2
|
||||
license = Apache-2
|
||||
classifier =
|
||||
Development Status :: 5 - Production/Stable
|
||||
Environment :: Web Environment
|
||||
Intended Audience :: Developers
|
||||
License :: OSI Approved :: Apache Software License
|
||||
Operating System :: OS Independent
|
||||
Programming Language :: Python
|
||||
Programming Language :: Python :: 3
|
||||
Framework :: Django
|
||||
|
||||
[files]
|
||||
packages =
|
||||
django_select2
|
||||
|
||||
[pbr]
|
||||
skip_authors = true
|
||||
skip_changelog = true
|
||||
|
||||
[build_sphinx]
|
||||
builders = spelling,html
|
||||
source-dir = docs
|
||||
build-dir = docs/_build
|
||||
warning-is-error = 1
|
||||
|
||||
[tool:pytest]
|
||||
norecursedirs = env docs .eggs
|
||||
addopts = --tb=short -rxs
|
||||
DJANGO_SETTINGS_MODULE=tests.testapp.settings
|
||||
|
||||
@@ -8,16 +40,20 @@ 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]
|
||||
explain = true
|
||||
count = true
|
||||
match = (?!setup).*.py
|
||||
match-dir = (?!tests|env|docs|\.).*
|
||||
|
||||
[isort]
|
||||
atomic = true
|
||||
multi_line_output = 5
|
||||
line_length = 79
|
||||
skip = manage.py,docs
|
||||
known_first_party = django_select2
|
||||
skip = manage.py,docs,.tox,env
|
||||
known_first_party = django_select2, tests
|
||||
known_third_party = django
|
||||
combine_as_imports = true
|
||||
|
||||
[bdist_rpm]
|
||||
requires = python-django-appconf >= 0.6
|
||||
|
||||
@@ -1,71 +1,8 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import codecs
|
||||
import os
|
||||
|
||||
from setuptools import Command, find_packages, setup
|
||||
|
||||
|
||||
def read(file_name):
|
||||
file_path = os.path.join(os.path.dirname(__file__), file_name)
|
||||
return codecs.open(file_path, encoding='utf-8').read()
|
||||
|
||||
|
||||
PACKAGE = "django_select2"
|
||||
NAME = "Django-Select2"
|
||||
DESCRIPTION = "Select2 option fields for Django"
|
||||
AUTHOR = "Nirupam Biswas, Johannes Hoppe"
|
||||
AUTHOR_EMAIL = "admin@applegrew.com"
|
||||
URL = "https://github.com/applegrew/django-select2"
|
||||
VERSION = __import__(PACKAGE).__version__
|
||||
|
||||
|
||||
class PyTest(Command):
|
||||
user_options = []
|
||||
|
||||
def initialize_options(self):
|
||||
pass
|
||||
|
||||
def finalize_options(self):
|
||||
pass
|
||||
|
||||
def run(self):
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
errno = subprocess.call([sys.executable, 'runtests.py'])
|
||||
raise SystemExit(errno)
|
||||
|
||||
from setuptools import setup
|
||||
|
||||
setup(
|
||||
name=NAME,
|
||||
version=VERSION,
|
||||
description=DESCRIPTION,
|
||||
long_description=read("README.md"),
|
||||
author=AUTHOR,
|
||||
author_email=AUTHOR_EMAIL,
|
||||
license="LICENSE.txt",
|
||||
url=URL,
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
classifiers=[
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Environment :: Web Environment",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: Apache Software License",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 2",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Framework :: Django :: 1.7",
|
||||
"Framework :: Django :: 1.8",
|
||||
"Framework :: Django",
|
||||
],
|
||||
install_requires=[
|
||||
'django-appconf>=0.6.0',
|
||||
],
|
||||
zip_safe=False,
|
||||
cmdclass={'test': PyTest},
|
||||
setup_requires=['pbr'],
|
||||
pbr=True,
|
||||
)
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
__author__ = 'johannes'
|
||||
|
||||
+46
-22
@@ -1,41 +1,65 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
from __future__ import absolute_import, print_function, unicode_literals
|
||||
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
|
||||
import pytest
|
||||
from model_mommy import mommy
|
||||
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(
|
||||
random.choice(string.ascii_uppercase + string.digits)
|
||||
for _ in range(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.headless = True
|
||||
try:
|
||||
b = browsers[request.param]()
|
||||
b = webdriver.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
|
||||
def genres(db):
|
||||
return mommy.make('testapp.Genre', _quantity=100)
|
||||
from .testapp.models import Genre
|
||||
|
||||
return Genre.objects.bulk_create(
|
||||
[Genre(pk=pk, title=random_string(50)) for pk in range(100)]
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def artists(db):
|
||||
return mommy.make('testapp.Artist', _quantity=100)
|
||||
from .testapp.models import Artist
|
||||
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)]
|
||||
)
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
|
||||
def test_default_cache():
|
||||
from django_select2.cache import cache
|
||||
|
||||
|
||||
+320
-33
@@ -1,35 +1,42 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import collections
|
||||
import json
|
||||
import os
|
||||
from collections.abc import Iterable
|
||||
|
||||
import pytest
|
||||
from django.core import signing
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db.models import QuerySet
|
||||
from django.urls import reverse
|
||||
from django.utils import translation
|
||||
from django.utils.encoding import force_text
|
||||
from model_mommy import mommy
|
||||
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.conf import settings
|
||||
from django_select2.forms import (
|
||||
HeavySelect2Widget, ModelSelect2TagWidget, ModelSelect2Widget,
|
||||
Select2Widget
|
||||
HeavySelect2MultipleWidget, HeavySelect2Widget, ModelSelect2TagWidget,
|
||||
ModelSelect2Widget, Select2Widget
|
||||
)
|
||||
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 Artist, City, Country, Genre, Groupie
|
||||
|
||||
|
||||
class TestSelect2Mixin(object):
|
||||
class TestSelect2Mixin:
|
||||
url = reverse('select2_widget')
|
||||
form = forms.AlbumSelect2WidgetForm()
|
||||
multiple_form = forms.AlbumSelect2MultipleWidgetForm()
|
||||
widget_cls = Select2Widget
|
||||
|
||||
def test_initial_data(self, genres):
|
||||
genre = genres[0]
|
||||
form = self.form.__class__(initial={'primary_genre': genre.pk})
|
||||
assert str(genre) in form.as_p()
|
||||
|
||||
def test_initial_form_class(self):
|
||||
widget = self.widget_cls(attrs={'class': 'my-class'})
|
||||
assert 'my-class' in widget.render('name', None)
|
||||
@@ -40,7 +47,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
|
||||
@@ -48,7 +55,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)
|
||||
@@ -78,14 +85,107 @@ 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 tuple(Select2Widget().media._js) == (
|
||||
f'//cdnjs.cloudflare.com/ajax/libs/select2/{settings.SELECT2_LIB_VERSION}/js/select2.min.js',
|
||||
f'//cdnjs.cloudflare.com/ajax/libs/select2/{settings.SELECT2_LIB_VERSION}/js/i18n/de.js',
|
||||
'django_select2/django_select2.js'
|
||||
)
|
||||
|
||||
translation.activate('en')
|
||||
assert tuple(Select2Widget().media._js) == (
|
||||
f'//cdnjs.cloudflare.com/ajax/libs/select2/{settings.SELECT2_LIB_VERSION}/js/select2.min.js',
|
||||
f'//cdnjs.cloudflare.com/ajax/libs/select2/{settings.SELECT2_LIB_VERSION}/js/i18n/en.js',
|
||||
'django_select2/django_select2.js'
|
||||
)
|
||||
|
||||
translation.activate('00')
|
||||
assert tuple(Select2Widget().media._js) == (
|
||||
f'//cdnjs.cloudflare.com/ajax/libs/select2/{settings.SELECT2_LIB_VERSION}/js/select2.min.js',
|
||||
'django_select2/django_select2.js'
|
||||
)
|
||||
|
||||
translation.activate('sr-cyrl')
|
||||
assert tuple(Select2Widget().media._js) == (
|
||||
f'//cdnjs.cloudflare.com/ajax/libs/select2/{settings.SELECT2_LIB_VERSION}/js/select2.min.js',
|
||||
f'//cdnjs.cloudflare.com/ajax/libs/select2/{settings.SELECT2_LIB_VERSION}/js/i18n/sr-Cyrl.js',
|
||||
'django_select2/django_select2.js'
|
||||
)
|
||||
|
||||
pytest.importorskip("django", minversion="2.0.4")
|
||||
|
||||
translation.activate('zh-hans')
|
||||
assert tuple(Select2Widget().media._js) == (
|
||||
f'//cdnjs.cloudflare.com/ajax/libs/select2/{settings.SELECT2_LIB_VERSION}/js/select2.min.js',
|
||||
f'//cdnjs.cloudflare.com/ajax/libs/select2/{settings.SELECT2_LIB_VERSION}/js/i18n/zh-CN.js',
|
||||
'django_select2/django_select2.js'
|
||||
)
|
||||
|
||||
translation.activate('zh-hant')
|
||||
assert tuple(Select2Widget().media._js) == (
|
||||
f'//cdnjs.cloudflare.com/ajax/libs/select2/{settings.SELECT2_LIB_VERSION}/js/select2.min.js',
|
||||
f'//cdnjs.cloudflare.com/ajax/libs/select2/{settings.SELECT2_LIB_VERSION}/js/i18n/zh-TW.js',
|
||||
'django_select2/django_select2.js'
|
||||
)
|
||||
|
||||
|
||||
class TestSelect2MixinSettings:
|
||||
def test_default_media(self):
|
||||
sut = Select2Widget()
|
||||
result = sut.media.render()
|
||||
assert f'//cdnjs.cloudflare.com/ajax/libs/select2/{settings.SELECT2_LIB_VERSION}/js/select2.min.js' in result
|
||||
assert f'//cdnjs.cloudflare.com/ajax/libs/select2/{settings.SELECT2_LIB_VERSION}/css/select2.min.css' in result
|
||||
assert 'django_select2/django_select2.js' in result
|
||||
|
||||
def test_js_setting(self, settings):
|
||||
settings.SELECT2_JS = 'alternate.js'
|
||||
sut = Select2Widget()
|
||||
result = sut.media.render()
|
||||
assert 'alternate.js' in result
|
||||
assert 'django_select2/django_select2.js' in result
|
||||
|
||||
def test_empty_js_setting(self, settings):
|
||||
settings.SELECT2_JS = ''
|
||||
sut = Select2Widget()
|
||||
result = sut.media.render()
|
||||
assert 'django_select2/django_select2.js' in result
|
||||
|
||||
def test_css_setting(self, settings):
|
||||
settings.SELECT2_CSS = 'alternate.css'
|
||||
sut = Select2Widget()
|
||||
result = sut.media.render()
|
||||
assert 'alternate.css' in result
|
||||
|
||||
def test_empty_css_setting(self, settings):
|
||||
settings.SELECT2_CSS = ''
|
||||
sut = Select2Widget()
|
||||
result = sut.media.render()
|
||||
assert '.css' not in result
|
||||
|
||||
|
||||
class TestHeavySelect2Mixin(TestSelect2Mixin):
|
||||
url = reverse('heavy_select2_widget')
|
||||
form = forms.HeavySelect2WidgetForm(initial={'primary_genre': [1]})
|
||||
form = forms.HeavySelect2WidgetForm(initial={'primary_genre': 1})
|
||||
widget_cls = HeavySelect2Widget
|
||||
|
||||
def test_initial_data(self):
|
||||
assert 'One' in self.form.as_p()
|
||||
|
||||
def test_initial_form_class(self):
|
||||
widget = self.widget_cls(data_view='heavy_data', attrs={'class': 'my-class'})
|
||||
widget = self.widget_cls(data_view='heavy_data_1', attrs={'class': 'my-class'})
|
||||
assert 'my-class' in widget.render('name', None)
|
||||
assert 'django-select2' in widget.render('name', None)
|
||||
assert 'django-select2-heavy' in widget.render('name', None), widget.render('name', None)
|
||||
@@ -94,27 +194,81 @@ 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, choices=NUMBER_CHOICES), \
|
||||
not_required_field.widget.render('primary_genre', 1, choices=NUMBER_CHOICES)
|
||||
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):
|
||||
field = HeavySelect2MultipleWidgetForm().fields['genres']
|
||||
widget_output = field.widget.render(
|
||||
'genres', [1, 2],
|
||||
choices=NUMBER_CHOICES)
|
||||
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)
|
||||
with pytest.raises(NoSuchElementException):
|
||||
driver.find_element_by_css_selector('.select2-results')
|
||||
|
||||
elem1, elem2 = driver.find_elements_by_css_selector('.select2-selection')
|
||||
elem1.click()
|
||||
|
||||
result1 = WebDriverWait(driver, 60).until(
|
||||
expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '.select2-results li:first-child'))
|
||||
).text
|
||||
elem2.click()
|
||||
result2 = WebDriverWait(driver, 60).until(
|
||||
expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '.select2-results li:first-child'))
|
||||
).text
|
||||
|
||||
assert result1 != result2
|
||||
|
||||
with pytest.raises(NoSuchElementException):
|
||||
error = driver.find_element_by_xpath('//body[@JSError]')
|
||||
pytest.fail(error.get_attribute('JSError'))
|
||||
|
||||
def test_get_url(self):
|
||||
widget = self.widget_cls(data_view='heavy_data_1', attrs={'class': 'my-class'})
|
||||
assert isinstance(widget.get_url(), str)
|
||||
|
||||
def test_can_not_pickle(self):
|
||||
widget = self.widget_cls(data_view='heavy_data_1', attrs={'class': 'my-class'})
|
||||
|
||||
class NoPickle:
|
||||
pass
|
||||
|
||||
widget.no_pickle = NoPickle()
|
||||
with pytest.raises(NotImplementedError):
|
||||
widget.set_to_cache()
|
||||
|
||||
|
||||
class TestModelSelect2Mixin(TestHeavySelect2Mixin):
|
||||
form = forms.AlbumModelSelect2WidgetForm(initial={'primary_genre': 1})
|
||||
multiple_form = forms.ArtistModelSelect2MultipleWidgetForm()
|
||||
|
||||
def test_initial_data(self, genres):
|
||||
genre = genres[0]
|
||||
form = self.form.__class__(initial={'primary_genre': genre.pk})
|
||||
assert str(genre) in form.as_p()
|
||||
|
||||
def test_label_from_instance_initial(self, genres):
|
||||
genre = genres[0]
|
||||
genre.title = genre.title.lower()
|
||||
genre.save()
|
||||
|
||||
form = self.form.__class__(initial={'primary_genre': genre.pk})
|
||||
assert genre.title not in form.as_p(), form.as_p()
|
||||
assert genre.title.upper() in form.as_p()
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def genres(self, db):
|
||||
return mommy.make(Genre, 100)
|
||||
def genres(self, genres):
|
||||
return genres
|
||||
|
||||
def test_selected_option(self, db, genres):
|
||||
genre = genres[0]
|
||||
@@ -125,12 +279,32 @@ 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):
|
||||
genre = genres[0]
|
||||
genre.title = genre.title.lower()
|
||||
genre.save()
|
||||
|
||||
field = self.form.fields['primary_genre']
|
||||
widget_output = field.widget.render('primary_genre', genre.pk)
|
||||
|
||||
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 all(o not in widget_output for o in get_selected_options(genre))
|
||||
genre.title = genre.title.upper()
|
||||
|
||||
assert any(o in widget_output for o in get_selected_options(genre))
|
||||
|
||||
def test_get_queryset(self):
|
||||
widget = ModelSelect2Widget()
|
||||
with pytest.raises(NotImplementedError):
|
||||
@@ -147,29 +321,34 @@ class TestModelSelect2Mixin(TestHeavySelect2Mixin):
|
||||
widget.get_search_fields()
|
||||
|
||||
widget.search_fields = ['title__icontains']
|
||||
assert isinstance(widget.get_search_fields(), collections.Iterable)
|
||||
assert all(isinstance(x, text_type) for x in widget.get_search_fields())
|
||||
assert isinstance(widget.get_search_fields(), Iterable)
|
||||
assert all(isinstance(x, str) for x in widget.get_search_fields())
|
||||
|
||||
def test_filter_queryset(self, genres):
|
||||
widget = TitleModelSelect2Widget(queryset=Genre.objects.all())
|
||||
assert widget.filter_queryset(genres[0].title[:3]).exists()
|
||||
assert widget.filter_queryset(None, genres[0].title[:3]).exists()
|
||||
|
||||
widget = TitleModelSelect2Widget(search_fields=['title__icontains'],
|
||||
queryset=Genre.objects.all())
|
||||
qs = widget.filter_queryset(None, " ".join([genres[0].title[:3], genres[0].title[3:]]))
|
||||
assert qs.exists()
|
||||
|
||||
def test_model_kwarg(self):
|
||||
widget = ModelSelect2Widget(model=Genre, search_fields=['title__icontains'])
|
||||
genre = Genre.objects.last()
|
||||
result = widget.filter_queryset(genre.title)
|
||||
result = widget.filter_queryset(None, genre.title)
|
||||
assert result.exists()
|
||||
|
||||
def test_queryset_kwarg(self):
|
||||
widget = ModelSelect2Widget(queryset=Genre.objects.all(), search_fields=['title__icontains'])
|
||||
genre = Genre.objects.last()
|
||||
result = widget.filter_queryset(genre.title)
|
||||
result = widget.filter_queryset(None, genre.title)
|
||||
assert result.exists()
|
||||
|
||||
def test_ajax_view_registration(self, client):
|
||||
widget = ModelSelect2Widget(queryset=Genre.objects.all(), search_fields=['title__icontains'])
|
||||
widget.render('name', 'value')
|
||||
url = reverse('django_select2-json')
|
||||
url = reverse('django_select2:auto-json')
|
||||
genre = Genre.objects.last()
|
||||
response = client.get(url, data=dict(field_id=signing.dumps(id(widget)), term=genre.title))
|
||||
assert response.status_code == 200, response.content
|
||||
@@ -182,10 +361,20 @@ class TestModelSelect2Mixin(TestHeavySelect2Mixin):
|
||||
widget.render('name', 'value')
|
||||
cached_widget = cache.get(widget._get_cache_key())
|
||||
assert cached_widget['max_results'] == widget.max_results
|
||||
assert cached_widget['search_fields'] == widget.search_fields
|
||||
assert cached_widget['search_fields'] == tuple(widget.search_fields)
|
||||
qs = widget.get_queryset()
|
||||
assert isinstance(cached_widget['queryset'][0], qs.__class__)
|
||||
assert text_type(cached_widget['queryset'][1]) == text_type(qs.query)
|
||||
assert str(cached_widget['queryset'][1]) == str(qs.query)
|
||||
|
||||
def test_get_url(self):
|
||||
widget = ModelSelect2Widget(queryset=Genre.objects.all(), search_fields=['title__icontains'])
|
||||
assert isinstance(widget.get_url(), str)
|
||||
|
||||
def test_custom_to_field_name(self):
|
||||
the_best_band_in_the_world = Artist.objects.create(title='Take That')
|
||||
groupie = Groupie.objects.create(obsession=the_best_band_in_the_world)
|
||||
form = forms.GroupieForm(instance=groupie)
|
||||
assert '<option value="Take That" selected>TAKE THAT</option>' in form.as_p()
|
||||
|
||||
|
||||
class TestHeavySelect2TagWidget(TestHeavySelect2Mixin):
|
||||
@@ -196,3 +385,101 @@ class TestHeavySelect2TagWidget(TestHeavySelect2Mixin):
|
||||
assert 'data-minimum-input-length="1"' in output
|
||||
assert 'data-tags="true"' in output
|
||||
assert 'data-token-separators' in output
|
||||
|
||||
def test_custom_tag_attrs(self):
|
||||
widget = ModelSelect2TagWidget(
|
||||
queryset=Genre.objects.all(), search_fields=['title__icontains'], attrs={'data-minimum-input-length': '3'})
|
||||
output = widget.render('name', 'value')
|
||||
assert 'data-minimum-input-length="3"' in output
|
||||
|
||||
|
||||
class TestHeavySelect2MultipleWidget:
|
||||
url = reverse('heavy_select2_multiple_widget')
|
||||
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')
|
||||
genres.click()
|
||||
genres.send_keys('o') # results are Zero One Two Four
|
||||
# select second element - One
|
||||
driver.find_element_by_css_selector('.select2-results li:nth-child(2)').click()
|
||||
genres.submit()
|
||||
# there is a ValidationError raised, check for it
|
||||
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:
|
||||
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
|
||||
|
||||
+68
-10
@@ -1,22 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
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 tests.testapp.forms import AlbumModelSelect2WidgetForm
|
||||
from django_select2.cache import cache
|
||||
from django_select2.forms import ModelSelect2Widget
|
||||
from tests.testapp.forms import (
|
||||
AlbumModelSelect2WidgetForm, ArtistCustomTitleWidget
|
||||
)
|
||||
from tests.testapp.models import Genre
|
||||
|
||||
try:
|
||||
from django.urls import reverse
|
||||
except ImportError:
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
|
||||
class TestAutoResponseView(object):
|
||||
class TestAutoResponseView:
|
||||
def test_get(self, client, artists):
|
||||
artist = artists[0]
|
||||
form = AlbumModelSelect2WidgetForm()
|
||||
assert form.as_p()
|
||||
field_id = signing.dumps(id(form.fields['artist'].widget))
|
||||
url = reverse('django_select2-json')
|
||||
url = reverse('django_select2:auto-json')
|
||||
response = client.get(url, {'field_id': field_id, 'term': artist.title})
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.content.decode('utf-8'))
|
||||
@@ -25,19 +31,71 @@ class TestAutoResponseView(object):
|
||||
|
||||
def test_no_field_id(self, client, artists):
|
||||
artist = artists[0]
|
||||
url = reverse('django_select2-json')
|
||||
url = reverse('django_select2:auto-json')
|
||||
response = client.get(url, {'term': artist.title})
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_wrong_field_id(self, client, artists):
|
||||
artist = artists[0]
|
||||
url = reverse('django_select2-json')
|
||||
url = reverse('django_select2:auto-json')
|
||||
response = client.get(url, {'field_id': 123, 'term': artist.title})
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_field_id_not_found(self, client, artists):
|
||||
artist = artists[0]
|
||||
field_id = signing.dumps(123456789)
|
||||
url = reverse('django_select2-json')
|
||||
url = reverse('django_select2:auto-json')
|
||||
response = client.get(url, {'field_id': field_id, 'term': artist.title})
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_pagination(self, genres, client):
|
||||
url = reverse('django_select2:auto-json')
|
||||
widget = ModelSelect2Widget(
|
||||
max_results=10,
|
||||
model=Genre,
|
||||
search_fields=['title__icontains']
|
||||
)
|
||||
widget.render('name', None)
|
||||
field_id = signing.dumps(id(widget))
|
||||
|
||||
response = client.get(url, {'field_id': field_id, 'term': ''})
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.content.decode('utf-8'))
|
||||
assert data['more'] is True
|
||||
|
||||
response = client.get(url, {'field_id': field_id, 'term': '', 'page': 1000})
|
||||
assert response.status_code == 404
|
||||
|
||||
response = client.get(url, {'field_id': field_id, 'term': '', 'page': 'last'})
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.content.decode('utf-8'))
|
||||
assert data['more'] is False
|
||||
|
||||
def test_label_from_instance(self, artists, client):
|
||||
url = reverse('django_select2:auto-json')
|
||||
|
||||
form = AlbumModelSelect2WidgetForm()
|
||||
form.fields['artist'].widget = ArtistCustomTitleWidget()
|
||||
assert form.as_p()
|
||||
field_id = signing.dumps(id(form.fields['artist'].widget))
|
||||
|
||||
artist = artists[0]
|
||||
response = client.get(url, {'field_id': field_id, 'term': artist.title})
|
||||
assert response.status_code == 200
|
||||
|
||||
data = json.loads(response.content.decode('utf-8'))
|
||||
assert data['results']
|
||||
assert {'id': artist.pk, 'text': smart_text(artist.title.upper())} in data['results']
|
||||
|
||||
def test_url_check(self, client, artists):
|
||||
artist = artists[0]
|
||||
form = AlbumModelSelect2WidgetForm()
|
||||
assert form.as_p()
|
||||
field_id = signing.dumps(id(form.fields['artist'].widget))
|
||||
cache_key = form.fields['artist'].widget._get_cache_key()
|
||||
widget_dict = cache.get(cache_key)
|
||||
widget_dict['url'] = 'yet/another/url'
|
||||
cache.set(cache_key, widget_dict)
|
||||
url = reverse('django_select2:auto-json')
|
||||
response = client.get(url, {'field_id': field_id, 'term': artist.title})
|
||||
assert response.status_code == 404
|
||||
|
||||
+84
-19
@@ -1,7 +1,5 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django import forms
|
||||
from django.utils.encoding import force_text
|
||||
|
||||
from django_select2.forms import (
|
||||
HeavySelect2MultipleWidget, HeavySelect2Widget, ModelSelect2MultipleWidget,
|
||||
@@ -9,10 +7,10 @@ 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):
|
||||
class TitleSearchFieldMixin:
|
||||
search_fields = [
|
||||
'title__icontains',
|
||||
'pk__startswith'
|
||||
@@ -34,6 +32,26 @@ class GenreSelect2TagWidget(TitleSearchFieldMixin, ModelSelect2TagWidget):
|
||||
self.get_queryset().create(title=value)
|
||||
|
||||
|
||||
class ArtistCustomTitleWidget(ModelSelect2Widget):
|
||||
model = models.Artist
|
||||
search_fields = [
|
||||
'title__icontains'
|
||||
]
|
||||
|
||||
def label_from_instance(self, obj):
|
||||
return force_text(obj.title).upper()
|
||||
|
||||
|
||||
class GenreCustomTitleWidget(ModelSelect2Widget):
|
||||
model = models.Genre
|
||||
search_fields = [
|
||||
'title__icontains'
|
||||
]
|
||||
|
||||
def label_from_instance(self, obj):
|
||||
return force_text(obj.title).upper()
|
||||
|
||||
|
||||
class AlbumSelect2WidgetForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = models.Album
|
||||
@@ -69,16 +87,14 @@ class AlbumModelSelect2WidgetForm(forms.ModelForm):
|
||||
'primary_genre',
|
||||
)
|
||||
widgets = {
|
||||
'artist': ModelSelect2Widget(
|
||||
model=models.Artist,
|
||||
search_fields=['title__icontains']
|
||||
),
|
||||
'primary_genre': ModelSelect2Widget(
|
||||
model=models.Genre,
|
||||
search_fields=['title__icontains']
|
||||
),
|
||||
'artist': ArtistCustomTitleWidget(),
|
||||
'primary_genre': GenreCustomTitleWidget(),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(AlbumModelSelect2WidgetForm, self).__init__(*args, **kwargs)
|
||||
self.fields['primary_genre'].initial = 2
|
||||
|
||||
|
||||
class AlbumModelSelect2MultipleWidgetRequiredForm(forms.ModelForm):
|
||||
class Meta:
|
||||
@@ -98,7 +114,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'),
|
||||
@@ -114,23 +136,33 @@ class Select2WidgetForm(forms.Form):
|
||||
|
||||
class HeavySelect2WidgetForm(forms.Form):
|
||||
artist = forms.ChoiceField(
|
||||
widget=HeavySelect2Widget(data_view='heavy_data', choices=NUMBER_CHOICES)
|
||||
widget=HeavySelect2Widget(data_view='heavy_data_1'),
|
||||
choices=NUMBER_CHOICES
|
||||
)
|
||||
primary_genre = forms.ChoiceField(
|
||||
widget=HeavySelect2Widget(data_view='heavy_data', choices=NUMBER_CHOICES),
|
||||
required=False
|
||||
widget=HeavySelect2Widget(data_view='heavy_data_2'),
|
||||
required=False,
|
||||
choices=NUMBER_CHOICES
|
||||
)
|
||||
|
||||
|
||||
class HeavySelect2MultipleWidgetForm(forms.Form):
|
||||
title = forms.CharField(max_length=50)
|
||||
genres = forms.MultipleChoiceField(
|
||||
widget=HeavySelect2MultipleWidget(data_view='heavy_data', choices=NUMBER_CHOICES)
|
||||
widget=HeavySelect2MultipleWidget(data_view='heavy_data_1', choices=NUMBER_CHOICES),
|
||||
choices=NUMBER_CHOICES
|
||||
)
|
||||
featured_artists = forms.MultipleChoiceField(
|
||||
widget=HeavySelect2MultipleWidget(data_view='heavy_data', choices=NUMBER_CHOICES),
|
||||
widget=HeavySelect2MultipleWidget(data_view='heavy_data_2', choices=NUMBER_CHOICES),
|
||||
choices=NUMBER_CHOICES,
|
||||
required=False
|
||||
)
|
||||
|
||||
def clean_title(self):
|
||||
if len(self.cleaned_data['title']) < 3:
|
||||
raise forms.ValidationError("Title must have more than 3 characters.")
|
||||
return self.cleaned_data["title"]
|
||||
|
||||
|
||||
class ModelSelect2TagWidgetForm(forms.ModelForm):
|
||||
class Meta:
|
||||
@@ -139,3 +171,36 @@ 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,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class GroupieForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = models.Groupie
|
||||
fields = '__all__'
|
||||
widgets = {
|
||||
'obsession': ArtistCustomTitleWidget
|
||||
}
|
||||
|
||||
+38
-10
@@ -1,34 +1,62 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.db import models
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Genre(models.Model):
|
||||
title = models.CharField(max_length=50)
|
||||
|
||||
class Meta:
|
||||
ordering = ('title',)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Artist(models.Model):
|
||||
title = models.CharField(max_length=50)
|
||||
title = models.CharField(max_length=50, unique=True)
|
||||
genres = models.ManyToManyField(Genre)
|
||||
|
||||
class Meta:
|
||||
ordering = ('title',)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
class Meta:
|
||||
ordering = ('title',)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
|
||||
class Country(models.Model):
|
||||
name = models.CharField(max_length=255)
|
||||
|
||||
class Meta:
|
||||
ordering = ('name',)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class City(models.Model):
|
||||
name = models.CharField(max_length=255)
|
||||
country = models.ForeignKey('Country', related_name="cities", on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
ordering = ('name',)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Groupie(models.Model):
|
||||
obsession = models.ForeignKey(Artist, to_field='title', on_delete=models.CASCADE)
|
||||
|
||||
@@ -20,7 +20,7 @@ INSTALLED_APPS = (
|
||||
'tests.testapp',
|
||||
)
|
||||
|
||||
MIDDLEWARE_CLASSES = (
|
||||
MIDDLEWARE = (
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
@@ -33,18 +33,31 @@ MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
||||
SITE_ID = 1
|
||||
ROOT_URLCONF = 'tests.testapp.urls'
|
||||
|
||||
TEMPLATE_DIRS = (
|
||||
os.path.join(BASE_DIR, "templates"),
|
||||
)
|
||||
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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% load staticfiles %}
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
@@ -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>
|
||||
|
||||
+27
-25
@@ -1,33 +1,35 @@
|
||||
# -*- conding:utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.conf.urls import include, patterns, url
|
||||
from django.urls import include, path
|
||||
|
||||
from .forms import (
|
||||
AlbumModelSelect2WidgetForm, HeavySelect2MultipleWidgetForm,
|
||||
HeavySelect2WidgetForm, ModelSelect2TagWidgetForm, Select2WidgetForm
|
||||
AddressChainedSelect2WidgetForm, AlbumModelSelect2WidgetForm,
|
||||
HeavySelect2MultipleWidgetForm, HeavySelect2WidgetForm,
|
||||
ModelSelect2TagWidgetForm, Select2WidgetForm
|
||||
)
|
||||
from .views import TemplateFormView, heavy_data
|
||||
from .views import TemplateFormView, heavy_data_1, heavy_data_2
|
||||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
url(r'^select2_widget/$',
|
||||
TemplateFormView.as_view(form_class=Select2WidgetForm), name='select2_widget'),
|
||||
url(r'^heavy_select2_widget/$',
|
||||
TemplateFormView.as_view(form_class=HeavySelect2WidgetForm), name='heavy_select2_widget'),
|
||||
url(r'^heavy_select2_multiple_widget/$',
|
||||
TemplateFormView.as_view(form_class=HeavySelect2MultipleWidgetForm),
|
||||
name='heavy_select2_multiple_widget'),
|
||||
urlpatterns = [
|
||||
path('select2_widget',
|
||||
TemplateFormView.as_view(form_class=Select2WidgetForm), name='select2_widget'),
|
||||
path('heavy_select2_widget',
|
||||
TemplateFormView.as_view(form_class=HeavySelect2WidgetForm), name='heavy_select2_widget'),
|
||||
path('heavy_select2_multiple_widget',
|
||||
TemplateFormView.as_view(form_class=HeavySelect2MultipleWidgetForm, success_url='/'),
|
||||
name='heavy_select2_multiple_widget'),
|
||||
|
||||
url(r'^model_select2_widget/$',
|
||||
TemplateFormView.as_view(form_class=AlbumModelSelect2WidgetForm),
|
||||
name='model_select2_widget'),
|
||||
path('model_select2_widget',
|
||||
TemplateFormView.as_view(form_class=AlbumModelSelect2WidgetForm),
|
||||
name='model_select2_widget'),
|
||||
|
||||
url(r'^model_select2_tag_widget/$',
|
||||
TemplateFormView.as_view(form_class=ModelSelect2TagWidgetForm),
|
||||
name='model_select2_tag_widget'),
|
||||
path('model_select2_tag_widget',
|
||||
TemplateFormView.as_view(form_class=ModelSelect2TagWidgetForm),
|
||||
name='model_select2_tag_widget'),
|
||||
|
||||
url(r'^heavy_data/$', heavy_data, name='heavy_data'),
|
||||
path('model_chained_select2_widget',
|
||||
TemplateFormView.as_view(form_class=AddressChainedSelect2WidgetForm),
|
||||
name='model_chained_select2_widget'),
|
||||
|
||||
url(r'^select2/', include('django_select2.urls')),
|
||||
)
|
||||
path('heavy_data_1', heavy_data_1, name='heavy_data_1'),
|
||||
path('heavy_data_2', heavy_data_2, name='heavy_data_2'),
|
||||
|
||||
path('select2/', include('django_select2.urls')),
|
||||
]
|
||||
|
||||
+11
-4
@@ -1,6 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
|
||||
from django.http import HttpResponse
|
||||
@@ -11,7 +8,17 @@ class TemplateFormView(FormView):
|
||||
template_name = 'form.html'
|
||||
|
||||
|
||||
def heavy_data(request):
|
||||
def heavy_data_1(request):
|
||||
term = request.GET.get("term", "")
|
||||
numbers = ['Zero', 'One', 'Two', 'Three', 'Four', 'Five']
|
||||
numbers = filter(lambda num: term.lower() in num.lower(), numbers)
|
||||
results = [{'id': index, 'text': value} for (index, value) in enumerate(numbers)]
|
||||
return HttpResponse(json.dumps({'err': 'nil', 'results': results}), content_type='application/json')
|
||||
|
||||
|
||||
def heavy_data_2(request):
|
||||
term = request.GET.get("term", "")
|
||||
numbers = ['Six', 'Seven', 'Eight', 'Nine', 'Ten', 'Fortytwo']
|
||||
numbers = filter(lambda num: term.lower() in num.lower(), numbers)
|
||||
results = [{'id': index, 'text': value} for (index, value) in enumerate(numbers)]
|
||||
return HttpResponse(json.dumps({'err': 'nil', 'results': results}), content_type='application/json')
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
[tox]
|
||||
envlist = py{35,36,37}-dj{20,21,22,master},docs
|
||||
[testenv]
|
||||
setenv=
|
||||
PYTHONPATH = {toxinidir}
|
||||
passenv=CI
|
||||
deps=
|
||||
-rrequirements-dev.txt
|
||||
dj20: https://github.com/django/django/archive/stable/2.0.x.tar.gz#egg=django
|
||||
dj21: https://github.com/django/django/archive/stable/2.1.x.tar.gz#egg=django
|
||||
dj22: https://github.com/django/django/archive/stable/2.2.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:docs]
|
||||
deps=
|
||||
-rrequirements-dev.txt
|
||||
https://github.com/django/django/archive/stable/2.2.x.tar.gz#egg=django
|
||||
commands=python setup.py build_sphinx
|
||||
Reference in New Issue
Block a user