mirror of
https://github.com/Hopiu/django-select2.git
synced 2026-04-06 15:10:59 +00:00
Compare commits
No commits in common. "master" and "7.0.2" have entirely different histories.
17 changed files with 145 additions and 282 deletions
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
|
|
@ -1,2 +0,0 @@
|
||||||
github: codingjoe
|
|
||||||
custom: https://paypal.me/codingjoe
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -20,6 +20,3 @@ venv/
|
||||||
geckodriver.log
|
geckodriver.log
|
||||||
ghostdriver.log
|
ghostdriver.log
|
||||||
.coverage
|
.coverage
|
||||||
|
|
||||||
coverage.xml
|
|
||||||
.eggs/
|
|
||||||
|
|
|
||||||
62
.travis.yml
62
.travis.yml
|
|
@ -4,10 +4,10 @@ dist: xenial
|
||||||
python:
|
python:
|
||||||
- "3.6"
|
- "3.6"
|
||||||
- "3.7"
|
- "3.7"
|
||||||
- "3.8"
|
|
||||||
env:
|
env:
|
||||||
|
- DJANGO=20
|
||||||
|
- DJANGO=21
|
||||||
- DJANGO=22
|
- DJANGO=22
|
||||||
- DJANGO=30
|
|
||||||
- DJANGO=master
|
- DJANGO=master
|
||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
|
|
@ -34,14 +34,9 @@ script: tox
|
||||||
|
|
||||||
after_success: codecov
|
after_success: codecov
|
||||||
|
|
||||||
stages:
|
|
||||||
- test
|
|
||||||
- name: deploy
|
|
||||||
if: tag is present
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
include:
|
include:
|
||||||
- python: "3.7"
|
- python: "3.5"
|
||||||
env: TOXENV=docs
|
env: TOXENV=docs
|
||||||
addons:
|
addons:
|
||||||
apt:
|
apt:
|
||||||
|
|
@ -56,32 +51,29 @@ jobs:
|
||||||
node_js: lts/*
|
node_js: lts/*
|
||||||
cache: npm
|
cache: npm
|
||||||
- stage: deploy
|
- stage: deploy
|
||||||
python: "3.8"
|
if: tag IS present
|
||||||
|
python: 3.7
|
||||||
install: skip
|
install: skip
|
||||||
script: skip
|
script: skip
|
||||||
after_success: true
|
after_success: skip
|
||||||
deploy:
|
|
||||||
provider: pypi
|
before_deploy:
|
||||||
distributions: sdist bdist_wheel
|
- git stash --all
|
||||||
on:
|
- ./set_version.py
|
||||||
tags: true
|
_deploy_provider: &_deploy_provider
|
||||||
user: codingjoe
|
skip_cleanup: true
|
||||||
password:
|
on:
|
||||||
secure: fEP9K7y0ZF9fRvQEUN4kgPXQEZvi3Cx3ikUebG2UM/2uhcaUQm0+KpgZ2S+lvOTYcBnNgzPzFsVIZMcVcTxwIKuQDEMq9y2eop2hnisb9KXsIm9qPYSzOnRm74VuiOt4hNOZMe0qVBK2cO3vC9NDXuzdI8A0JynJhthfl4t+kFM=
|
tags: true
|
||||||
- stage: deploy
|
repo: applegrew/django-select2
|
||||||
language: node_js
|
deploy:
|
||||||
node_js: lts/*
|
- <<: *_deploy_provider
|
||||||
python: "3.8"
|
provider: pypi
|
||||||
install: skip
|
distributions: sdist bdist_wheel
|
||||||
script: skip
|
user: codingjoe
|
||||||
after_success: true
|
password:
|
||||||
skip_cleanup: true
|
secure: fEP9K7y0ZF9fRvQEUN4kgPXQEZvi3Cx3ikUebG2UM/2uhcaUQm0+KpgZ2S+lvOTYcBnNgzPzFsVIZMcVcTxwIKuQDEMq9y2eop2hnisb9KXsIm9qPYSzOnRm74VuiOt4hNOZMe0qVBK2cO3vC9NDXuzdI8A0JynJhthfl4t+kFM=
|
||||||
before_deploy:
|
- <<: *_deploy_provider
|
||||||
- ./set_version.py
|
provider: npm
|
||||||
deploy:
|
email: info@johanneshoppe.com
|
||||||
provider: npm
|
api_key:
|
||||||
on:
|
secure: PV38cQx9qhEFkpSdytbM72UzIMCfhpjmRJ8dzT+bCAaOIs5rEcyKN+h1r5ranunCxWyuFsMW4A2iW/SCxnKCR/oPAguuwUbT5ogBXlsskqPFWUxuoTHYMrd+zB+SC6+bMgq+o5ul+kJCYtEkWP6cMlIEzKyTLab7m5PsnDXNVnI=
|
||||||
tags: true
|
|
||||||
email: info@johanneshoppe.com
|
|
||||||
api_key:
|
|
||||||
secure: PV38cQx9qhEFkpSdytbM72UzIMCfhpjmRJ8dzT+bCAaOIs5rEcyKN+h1r5ranunCxWyuFsMW4A2iW/SCxnKCR/oPAguuwUbT5ogBXlsskqPFWUxuoTHYMrd+zB+SC6+bMgq+o5ul+kJCYtEkWP6cMlIEzKyTLab7m5PsnDXNVnI=
|
|
||||||
|
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
Contributing
|
|
||||||
============
|
|
||||||
|
|
||||||
This package uses the pyTest test runner. To run the tests locally simply run::
|
|
||||||
|
|
||||||
python setup.py test
|
|
||||||
|
|
||||||
If you need to the development dependencies installed of you local IDE, you can run::
|
|
||||||
|
|
||||||
python setup.py develop
|
|
||||||
|
|
||||||
Documentation pull requests welcome. The Sphinx documentation can be compiled via::
|
|
||||||
|
|
||||||
python setup.py build_sphinx
|
|
||||||
|
|
||||||
Bug reports welcome, even more so if they include a correct patch. Much
|
|
||||||
more so if you start your patch by adding a failing unit test, and correct
|
|
||||||
the code until zero unit tests fail.
|
|
||||||
|
|
||||||
The list of supported Django and Python version can be found in the CI suite setup.
|
|
||||||
Please make sure to verify that none of the linters or tests failed, before you submit
|
|
||||||
a patch for review.
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
include django_select2/static/django_select2/django_select2.js
|
|
||||||
prune tests
|
|
||||||
prune .github
|
|
||||||
exclude .fussyfox.yml
|
|
||||||
exclude .travis.yml
|
|
||||||
exclude .gitignore
|
|
||||||
|
|
@ -8,7 +8,7 @@ __all__ = ('settings', 'Select2Conf')
|
||||||
class Select2Conf(AppConf):
|
class Select2Conf(AppConf):
|
||||||
"""Settings for Django-Select2."""
|
"""Settings for Django-Select2."""
|
||||||
|
|
||||||
LIB_VERSION = '4.0.12'
|
LIB_VERSION = '4.0.5'
|
||||||
"""Version of the Select2 library."""
|
"""Version of the Select2 library."""
|
||||||
|
|
||||||
CACHE_BACKEND = 'default'
|
CACHE_BACKEND = 'default'
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,10 @@ Widgets are generally of two types:
|
||||||
drop-in-replacement for Django's default
|
drop-in-replacement for Django's default
|
||||||
select widgets.
|
select widgets.
|
||||||
|
|
||||||
2(a). **Heavy** --
|
2. **Heavy** --
|
||||||
They are suited for scenarios when the number of options
|
They are suited for scenarios when the number of options
|
||||||
are large and need complex queries (from maybe different
|
are large and need complex queries (from maybe different
|
||||||
sources) to get the options.
|
sources) to get the options.
|
||||||
|
|
||||||
This dynamic fetching of options undoubtedly requires
|
This dynamic fetching of options undoubtedly requires
|
||||||
Ajax communication with the server. Django-Select2 includes
|
Ajax communication with the server. Django-Select2 includes
|
||||||
a helper JS file which is included automatically,
|
a helper JS file which is included automatically,
|
||||||
|
|
@ -32,15 +31,15 @@ Widgets are generally of two types:
|
||||||
Although on the server side you do need to create a view
|
Although on the server side you do need to create a view
|
||||||
specifically to respond to the queries.
|
specifically to respond to the queries.
|
||||||
|
|
||||||
2(b). **Model** --
|
3. **Model** --
|
||||||
Model-widgets are a further specialized versions of Heavies.
|
Model-widgets are a further specialized versions of Heavies.
|
||||||
These do not require views to serve Ajax requests.
|
These do not require views to serve Ajax requests.
|
||||||
When they are instantiated, they register themselves
|
When they are instantiated, they register themselves
|
||||||
with one central view which handles Ajax requests for them.
|
with one central view which handles Ajax requests for them.
|
||||||
|
|
||||||
Heavy and Model widgets have respectively the word 'Heavy' and 'Model' in
|
Heavy widgets have the word 'Heavy' in their name.
|
||||||
their name. Light widgets are normally named, i.e. there is no 'Light' word
|
Light widgets are normally named, i.e. there is no
|
||||||
in their names.
|
'Light' word in their names.
|
||||||
|
|
||||||
.. inheritance-diagram:: django_select2.forms
|
.. inheritance-diagram:: django_select2.forms
|
||||||
:parts: 1
|
:parts: 1
|
||||||
|
|
@ -71,20 +70,16 @@ class Select2Mixin:
|
||||||
form media.
|
form media.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
empty_label = ''
|
def build_attrs(self, *args, **kwargs):
|
||||||
|
|
||||||
def build_attrs(self, base_attrs, extra_attrs=None):
|
|
||||||
"""Add select2 data attributes."""
|
"""Add select2 data attributes."""
|
||||||
default_attrs = {'data-minimum-input-length': 0}
|
attrs = super(Select2Mixin, self).build_attrs(*args, **kwargs)
|
||||||
if self.is_required:
|
if self.is_required:
|
||||||
default_attrs['data-allow-clear'] = 'false'
|
attrs.setdefault('data-allow-clear', 'false')
|
||||||
else:
|
else:
|
||||||
default_attrs['data-allow-clear'] = 'true'
|
attrs.setdefault('data-allow-clear', 'true')
|
||||||
default_attrs['data-placeholder'] = self.empty_label or ""
|
attrs.setdefault('data-placeholder', '')
|
||||||
|
|
||||||
default_attrs.update(base_attrs)
|
|
||||||
attrs = super().build_attrs(default_attrs, extra_attrs=extra_attrs)
|
|
||||||
|
|
||||||
|
attrs.setdefault('data-minimum-input-length', 0)
|
||||||
if 'class' in attrs:
|
if 'class' in attrs:
|
||||||
attrs['class'] += ' django-select2'
|
attrs['class'] += ' django-select2'
|
||||||
else:
|
else:
|
||||||
|
|
@ -125,15 +120,12 @@ class Select2Mixin:
|
||||||
class Select2TagMixin:
|
class Select2TagMixin:
|
||||||
"""Mixin to add select2 tag functionality."""
|
"""Mixin to add select2 tag functionality."""
|
||||||
|
|
||||||
def build_attrs(self, base_attrs, extra_attrs=None):
|
def build_attrs(self, *args, **kwargs):
|
||||||
"""Add select2's tag attributes."""
|
"""Add select2's tag attributes."""
|
||||||
default_attrs = {
|
self.attrs.setdefault('data-minimum-input-length', 1)
|
||||||
'data-minimum-input-length': 1,
|
self.attrs.setdefault('data-tags', 'true')
|
||||||
'data-tags': 'true',
|
self.attrs.setdefault('data-token-separators', '[",", " "]')
|
||||||
'data-token-separators': '[",", " "]'
|
return super(Select2TagMixin, self).build_attrs(*args, **kwargs)
|
||||||
}
|
|
||||||
default_attrs.update(base_attrs)
|
|
||||||
return super().build_attrs(default_attrs, extra_attrs=extra_attrs)
|
|
||||||
|
|
||||||
|
|
||||||
class Select2Widget(Select2Mixin, forms.Select):
|
class Select2Widget(Select2Mixin, forms.Select):
|
||||||
|
|
@ -211,7 +203,6 @@ class HeavySelect2Mixin:
|
||||||
widget could be dependent on a country.
|
widget could be dependent on a country.
|
||||||
Key is a name of a field in a form.
|
Key is a name of a field in a form.
|
||||||
Value is a name of a field in a model (used in `queryset`).
|
Value is a name of a field in a model (used in `queryset`).
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.choices = choices
|
self.choices = choices
|
||||||
if attrs is not None:
|
if attrs is not None:
|
||||||
|
|
@ -235,26 +226,20 @@ class HeavySelect2Mixin:
|
||||||
return self.data_url
|
return self.data_url
|
||||||
return reverse(self.data_view)
|
return reverse(self.data_view)
|
||||||
|
|
||||||
def build_attrs(self, base_attrs, extra_attrs=None):
|
def build_attrs(self, *args, **kwargs):
|
||||||
"""Set select2's AJAX attributes."""
|
"""Set select2's AJAX attributes."""
|
||||||
default_attrs = {
|
attrs = super(HeavySelect2Mixin, self).build_attrs(*args, **kwargs)
|
||||||
'data-ajax--url': self.get_url(),
|
|
||||||
'data-ajax--cache': "true",
|
|
||||||
'data-ajax--type': "GET",
|
|
||||||
'data-minimum-input-length': 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.dependent_fields:
|
|
||||||
default_attrs['data-select2-dependent-fields'] = " ".join(self.dependent_fields)
|
|
||||||
|
|
||||||
default_attrs.update(base_attrs)
|
|
||||||
|
|
||||||
attrs = super().build_attrs(default_attrs, extra_attrs=extra_attrs)
|
|
||||||
|
|
||||||
# encrypt instance Id
|
# encrypt instance Id
|
||||||
self.widget_id = signing.dumps(id(self))
|
self.widget_id = signing.dumps(id(self))
|
||||||
|
|
||||||
attrs['data-field_id'] = self.widget_id
|
attrs['data-field_id'] = self.widget_id
|
||||||
|
attrs.setdefault('data-ajax--url', self.get_url())
|
||||||
|
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'
|
attrs['class'] += ' django-select2-heavy'
|
||||||
return attrs
|
return attrs
|
||||||
|
|
@ -343,12 +328,6 @@ class ModelSelect2Mixin:
|
||||||
max_results = 25
|
max_results = 25
|
||||||
"""Maximal results returned by :class:`.AutoResponseView`."""
|
"""Maximal results returned by :class:`.AutoResponseView`."""
|
||||||
|
|
||||||
@property
|
|
||||||
def empty_label(self):
|
|
||||||
if isinstance(self.choices, ModelChoiceIterator):
|
|
||||||
return self.choices.field.empty_label
|
|
||||||
return ''
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Overwrite class parameters if passed as keyword arguments.
|
Overwrite class parameters if passed as keyword arguments.
|
||||||
|
|
@ -412,7 +391,7 @@ class ModelSelect2Mixin:
|
||||||
term = term.replace('\t', ' ')
|
term = term.replace('\t', ' ')
|
||||||
term = term.replace('\n', ' ')
|
term = term.replace('\n', ' ')
|
||||||
for t in [t for t in term.split(' ') if not t == '']:
|
for t in [t for t in term.split(' ') if not t == '']:
|
||||||
select &= reduce(lambda x, y: x | Q(**{y: t}), search_fields[1:],
|
select &= reduce(lambda x, y: x | Q(**{y: t}), search_fields,
|
||||||
Q(**{search_fields[0]: t}))
|
Q(**{search_fields[0]: t}))
|
||||||
if dependent_fields:
|
if dependent_fields:
|
||||||
select &= Q(**dependent_fields)
|
select &= Q(**dependent_fields)
|
||||||
|
|
@ -429,8 +408,6 @@ class ModelSelect2Mixin:
|
||||||
"""
|
"""
|
||||||
if self.queryset is not None:
|
if self.queryset is not None:
|
||||||
queryset = self.queryset
|
queryset = self.queryset
|
||||||
elif hasattr(self.choices, 'queryset'):
|
|
||||||
queryset = self.choices.queryset
|
|
||||||
elif self.model is not None:
|
elif self.model is not None:
|
||||||
queryset = self.model._default_manager.all()
|
queryset = self.model._default_manager.all()
|
||||||
else:
|
else:
|
||||||
|
|
@ -564,15 +541,14 @@ class ModelSelect2TagWidget(ModelSelect2Mixin, HeavySelect2TagWidget):
|
||||||
queryset = MyModel.objects.all()
|
queryset = MyModel.objects.all()
|
||||||
|
|
||||||
def value_from_datadict(self, data, files, name):
|
def value_from_datadict(self, data, files, name):
|
||||||
'''Create objects for given non-pimary-key values. Return list of all primary keys.'''
|
values = super().value_from_datadict(self, data, files, name)
|
||||||
values = set(super().value_from_datadict(data, files, name))
|
qs = self.queryset.filter(**{'pk__in': list(values)})
|
||||||
# This may only work for MyModel, if MyModel has title field.
|
pks = set(str(getattr(o, pk)) for o in qs)
|
||||||
# You need to implement this method yourself, to ensure proper object creation.
|
cleaned_values = []
|
||||||
pks = self.queryset.filter(**{'pk__in': list(values)}).values_list('pk', flat=True)
|
for val in value:
|
||||||
pks = set(map(str, pks))
|
if str(val) not in pks:
|
||||||
cleaned_values = list(values)
|
val = queryset.create(title=val).pk
|
||||||
for val in values - pks:
|
cleaned_values.append(val)
|
||||||
cleaned_values.append(self.queryset.create(title=val).pk)
|
|
||||||
return cleaned_values
|
return cleaned_values
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
.. include:: ../CONTRIBUTING.rst
|
|
||||||
91
docs/conf.py
91
docs/conf.py
|
|
@ -1,52 +1,39 @@
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from pkg_resources import get_distribution
|
# This is needed since django_select2 requires django model modules
|
||||||
|
# and those modules assume that django settings is configured and
|
||||||
# This is needed since django_select2 requires django model modules
|
# have proper DB settings.
|
||||||
# and those modules assume that django settings is configured and
|
# Using this we give a proper environment with working django settings.
|
||||||
# have proper DB settings.
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.testapp.settings")
|
||||||
# Using this we give a proper environment with working django settings.
|
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.testapp.settings")
|
# If extensions (or modules to document with autodoc) are in another directory,
|
||||||
|
# add these directories to sys.path here. If the directory is relative to the
|
||||||
# If extensions (or modules to document with autodoc) are in another directory,
|
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||||
# add these directories to sys.path here. If the directory is relative to the
|
sys.path.insert(0, os.path.abspath('../tests.testapp'))
|
||||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
sys.path.insert(0, os.path.abspath('..'))
|
||||||
sys.path.insert(0, os.path.abspath('../tests.testapp'))
|
|
||||||
sys.path.insert(0, os.path.abspath('..'))
|
extensions = [
|
||||||
|
'sphinx.ext.autodoc',
|
||||||
|
'sphinx.ext.napoleon',
|
||||||
project = "Django-Select2"
|
'sphinx.ext.inheritance_diagram',
|
||||||
author = "Johannes Hoppe"
|
'sphinx.ext.intersphinx',
|
||||||
copyright = "2017, Johannes Hoppe"
|
'sphinx.ext.viewcode',
|
||||||
release = get_distribution('django_select2').version
|
'sphinx.ext.doctest',
|
||||||
version = '.'.join(release.split('.')[:2])
|
]
|
||||||
|
|
||||||
|
intersphinx_mapping = {
|
||||||
master_doc = 'index' # default in Sphinx v2
|
'python': ('http://docs.python.org/3', None),
|
||||||
|
'django': ('https://docs.djangoproject.com/en/stable/',
|
||||||
|
'https://docs.djangoproject.com/en/stable/_objects/'),
|
||||||
extensions = [
|
}
|
||||||
'sphinx.ext.autodoc',
|
|
||||||
'sphinx.ext.napoleon',
|
autodoc_default_flags = ['members', 'show-inheritance']
|
||||||
'sphinx.ext.inheritance_diagram',
|
autodoc_member_order = 'bysource'
|
||||||
'sphinx.ext.intersphinx',
|
|
||||||
'sphinx.ext.viewcode',
|
inheritance_graph_attrs = dict(rankdir='TB')
|
||||||
'sphinx.ext.doctest',
|
|
||||||
]
|
inheritance_node_attrs = dict(shape='rect', fontsize=14, fillcolor='gray90',
|
||||||
|
color='gray30', style='filled')
|
||||||
intersphinx_mapping = {
|
|
||||||
'python': ('http://docs.python.org/3', None),
|
inheritance_edge_attrs = dict(penwidth=0.75)
|
||||||
'django': ('https://docs.djangoproject.com/en/stable/',
|
|
||||||
'https://docs.djangoproject.com/en/stable/_objects/'),
|
|
||||||
}
|
|
||||||
|
|
||||||
autodoc_default_flags = ['members', 'show-inheritance']
|
|
||||||
autodoc_member_order = 'bysource'
|
|
||||||
|
|
||||||
inheritance_graph_attrs = dict(rankdir='TB')
|
|
||||||
|
|
||||||
inheritance_node_attrs = dict(shape='rect', fontsize=14, fillcolor='gray90',
|
|
||||||
color='gray30', style='filled')
|
|
||||||
|
|
||||||
inheritance_edge_attrs = dict(penwidth=0.75)
|
|
||||||
|
|
|
||||||
|
|
@ -49,9 +49,8 @@ DjangoSelect2 handles the initialization of select2 fields automatically. Just i
|
||||||
``{{ form.media.js }}`` in your template before the closing ``body`` tag. That's it!
|
``{{ 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
|
If you insert forms after page load or if you want to handle the initialization
|
||||||
yourself, DjangoSelect2 provides a jQuery plugin, replacing and enhancing the Select2
|
yourself, DjangoSelect2 provides a jQuery plugin. It will handle both normal and
|
||||||
plugin. It will handle both normal and heavy fields. Simply call
|
heavy fields. Simply call ``djangoSelect2(options)`` on your select fields.::
|
||||||
``djangoSelect2(options)`` on your select fields.::
|
|
||||||
|
|
||||||
$('.django-select2').djangoSelect2();
|
$('.django-select2').djangoSelect2();
|
||||||
|
|
||||||
|
|
@ -60,9 +59,6 @@ You can pass see `Select2 options <https://select2.github.io/options.html>`_ if
|
||||||
|
|
||||||
$('.django-select2').djangoSelect2({placeholder: 'Select an option'});
|
$('.django-select2').djangoSelect2({placeholder: 'Select an option'});
|
||||||
|
|
||||||
Please replace all your ``.select2`` invocations with the here provided
|
|
||||||
``.djangoSelect2``.
|
|
||||||
|
|
||||||
Security & Authentication
|
Security & Authentication
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
||||||
|
|
@ -85,6 +81,6 @@ want to be accessible to the general public. Doing so is easy::
|
||||||
kwargs['data_view'] = 'user-select2-view'
|
kwargs['data_view'] = 'user-select2-view'
|
||||||
super(UserSelect2WidgetMixin, self).__init__(*args, **kwargs)
|
super(UserSelect2WidgetMixin, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
class MySecretWidget(UserSelect2WidgetMixin, ModelSelect2Widget):
|
class MySecretWidget(UserSelect2WidgetMixin, Select2ModelWidget):
|
||||||
model = MySecretModel
|
model = MySecretModel
|
||||||
search_fields = ['title__icontains']
|
search_fields = ['title__icontains']
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,6 @@ Overview
|
||||||
.. automodule:: django_select2
|
.. automodule:: django_select2
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
Assumptions
|
|
||||||
-----------
|
|
||||||
|
|
||||||
* You have a running Django up and running.
|
|
||||||
* You have form fully working without Django-Select2.
|
|
||||||
|
|
||||||
Installation
|
Installation
|
||||||
------------
|
------------
|
||||||
|
|
||||||
|
|
@ -23,12 +17,11 @@ Installation
|
||||||
|
|
||||||
2. Add ``django_select2`` to your ``INSTALLED_APPS`` in your project settings.
|
2. Add ``django_select2`` to your ``INSTALLED_APPS`` in your project settings.
|
||||||
|
|
||||||
3. Add ``django_select`` to your ``urlconf``::
|
|
||||||
|
|
||||||
path('select2/', include('django_select2.urls')),
|
3. Add ``django_select`` to your ``urlconf`` **if** you use any
|
||||||
|
:class:`ModelWidgets <.django_select2.forms.ModelSelect2Mixin>`::
|
||||||
|
|
||||||
You can safely skip this one if you do not use any
|
url(r'^select2/', include('django_select2.urls')),
|
||||||
:class:`ModelWidgets <.django_select2.forms.ModelSelect2Mixin>`
|
|
||||||
|
|
||||||
Quick Start
|
Quick Start
|
||||||
-----------
|
-----------
|
||||||
|
|
@ -37,22 +30,19 @@ Here is a quick example to get you started:
|
||||||
|
|
||||||
0. Follow the installation instructions above.
|
0. Follow the installation instructions above.
|
||||||
|
|
||||||
1. Replace native Django forms widgets with one of the several ``django_select2.form`` widgets.
|
1. Add a select2 widget to the form. For example if you wanted Select2 with multi-select you would use
|
||||||
Start by importing them into your ``forms.py``, right next to Django own ones::
|
``Select2MultipleWidget``
|
||||||
|
Replacing::
|
||||||
|
|
||||||
from django import forms
|
class MyForm(forms.Form):
|
||||||
from django_select2 import forms as s2forms
|
things = ModelMultipleChoiceField(queryset=Thing.objects.all())
|
||||||
|
|
||||||
Then let's assume you have a model with a choice, a :class:`.ForeignKey`, and a
|
with::
|
||||||
:class:`.ManyToManyField`, you would add this information to your Form Meta
|
|
||||||
class::
|
|
||||||
|
|
||||||
widgets = {
|
from django_select2.forms import Select2MultipleWidget
|
||||||
'category': s2forms.Select2Widget,
|
|
||||||
'author': s2forms.ModelSelect2Widget(model=auth.get_user_model(),
|
class MyForm(forms.Form):
|
||||||
search_fields=['first_name__istartswith', 'last_name__icontains']),
|
things = ModelMultipleChoiceField(queryset=Thing.objects.all(), widget=Select2MultipleWidget)
|
||||||
'attending': s2forms.ModelSelect2MultipleWidget …
|
|
||||||
}
|
|
||||||
|
|
||||||
2. Add the CSS to the ``head`` of your Django template::
|
2. Add the CSS to the ``head`` of your Django template::
|
||||||
|
|
||||||
|
|
@ -67,9 +57,9 @@ Here is a quick example to get you started:
|
||||||
External Dependencies
|
External Dependencies
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
* jQuery (version >=2)
|
* jQuery version 2
|
||||||
jQuery is not included in the package since it is expected
|
This is not included in the package since it is expected
|
||||||
that in most scenarios jQuery is already loaded.
|
that in most scenarios this would already be available.
|
||||||
|
|
||||||
Example Application
|
Example Application
|
||||||
-------------------
|
-------------------
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ Contents:
|
||||||
get_started
|
get_started
|
||||||
django_select2
|
django_select2
|
||||||
extra
|
extra
|
||||||
CONTRIBUTING
|
|
||||||
|
|
||||||
Indices and tables
|
Indices and tables
|
||||||
==================
|
==================
|
||||||
|
|
|
||||||
22
setup.cfg
22
setup.cfg
|
|
@ -19,12 +19,12 @@ classifier =
|
||||||
|
|
||||||
[options]
|
[options]
|
||||||
include_package_data = True
|
include_package_data = True
|
||||||
packages = django_select2
|
packages = find:
|
||||||
install_requires =
|
install_requires =
|
||||||
django>=2.2
|
django>=2.0
|
||||||
django-appconf>=0.6.0
|
django-appconf>=0.6.0
|
||||||
setup_requires =
|
setup_requires =
|
||||||
setuptools_scm
|
very-good-setuptools-git-version
|
||||||
sphinx
|
sphinx
|
||||||
pytest-runner
|
pytest-runner
|
||||||
tests_require =
|
tests_require =
|
||||||
|
|
@ -33,6 +33,13 @@ tests_require =
|
||||||
pytest-django
|
pytest-django
|
||||||
selenium
|
selenium
|
||||||
|
|
||||||
|
[options.package_data]
|
||||||
|
* = *.txt, *.rst, *.html, *.po
|
||||||
|
|
||||||
|
[options.packages.find]
|
||||||
|
exclude =
|
||||||
|
tests
|
||||||
|
|
||||||
[bdist_wheel]
|
[bdist_wheel]
|
||||||
universal = 1
|
universal = 1
|
||||||
|
|
||||||
|
|
@ -47,19 +54,22 @@ test = pytest
|
||||||
[build_sphinx]
|
[build_sphinx]
|
||||||
source-dir = docs
|
source-dir = docs
|
||||||
build-dir = docs/_build
|
build-dir = docs/_build
|
||||||
|
project = Django-Select2
|
||||||
|
copyright = 2017 Johannes Hoppe
|
||||||
|
|
||||||
[tool:pytest]
|
[tool:pytest]
|
||||||
addopts = --cov=django_select2 --cov-report xml
|
addopts = --cov=django_select2 --cov-report xml
|
||||||
DJANGO_SETTINGS_MODULE=tests.testapp.settings
|
DJANGO_SETTINGS_MODULE=tests.testapp.settings
|
||||||
|
|
||||||
[tox:tox]
|
[tox:tox]
|
||||||
envlist = py{36,37,38}-dj{22,30,master},docs
|
envlist = py{35,36,37}-dj{22,21,20,master},docs
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
passenv=CI
|
passenv=CI
|
||||||
deps =
|
deps =
|
||||||
dj22: django~=2.2
|
dj20: https://github.com/django/django/archive/stable/2.0.x.tar.gz#egg=django
|
||||||
dj30: django~=3.0
|
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
|
djmaster: https://github.com/django/django/archive/master.tar.gz#egg=django
|
||||||
commands = python setup.py test
|
commands = python setup.py test
|
||||||
|
|
||||||
|
|
|
||||||
2
setup.py
2
setup.py
|
|
@ -2,4 +2,4 @@
|
||||||
|
|
||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
|
||||||
setup(name='django-select2', use_scm_version=True)
|
setup(version_format='{tag}')
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import random
|
||||||
import string
|
import string
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from django.utils.encoding import force_text
|
||||||
from selenium import webdriver
|
from selenium import webdriver
|
||||||
from selenium.common.exceptions import WebDriverException
|
from selenium.common.exceptions import WebDriverException
|
||||||
|
|
||||||
|
|
@ -25,7 +26,7 @@ def driver():
|
||||||
try:
|
try:
|
||||||
b = webdriver.Chrome(options=chrome_options)
|
b = webdriver.Chrome(options=chrome_options)
|
||||||
except WebDriverException as e:
|
except WebDriverException as e:
|
||||||
pytest.skip(str(e))
|
pytest.skip(force_text(e))
|
||||||
else:
|
else:
|
||||||
yield b
|
yield b
|
||||||
b.quit()
|
b.quit()
|
||||||
|
|
|
||||||
|
|
@ -95,9 +95,7 @@ class TestSelect2Mixin:
|
||||||
multiple_select = self.multiple_form.fields['featured_artists']
|
multiple_select = self.multiple_form.fields['featured_artists']
|
||||||
assert multiple_select.required is False
|
assert multiple_select.required is False
|
||||||
assert multiple_select.widget.allow_multiple_selected
|
assert multiple_select.widget.allow_multiple_selected
|
||||||
output = multiple_select.widget.render('featured_artists', None)
|
assert '<option value=""></option>' not in multiple_select.widget.render('featured_artists', None)
|
||||||
assert '<option value=""></option>' not in output
|
|
||||||
assert 'data-placeholder=""' in output
|
|
||||||
|
|
||||||
def test_i18n(self):
|
def test_i18n(self):
|
||||||
translation.activate('de')
|
translation.activate('de')
|
||||||
|
|
@ -219,17 +217,12 @@ class TestHeavySelect2Mixin(TestSelect2Mixin):
|
||||||
driver.find_element_by_css_selector('.select2-results')
|
driver.find_element_by_css_selector('.select2-results')
|
||||||
|
|
||||||
elem1, elem2 = driver.find_elements_by_css_selector('.select2-selection')
|
elem1, elem2 = driver.find_elements_by_css_selector('.select2-selection')
|
||||||
|
|
||||||
elem1.click()
|
elem1.click()
|
||||||
search1 = driver.find_element_by_css_selector('.select2-search__field')
|
|
||||||
search1.send_keys('fo')
|
|
||||||
result1 = WebDriverWait(driver, 60).until(
|
result1 = WebDriverWait(driver, 60).until(
|
||||||
expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '.select2-results li:first-child'))
|
expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '.select2-results li:first-child'))
|
||||||
).text
|
).text
|
||||||
|
|
||||||
elem2.click()
|
elem2.click()
|
||||||
search2 = driver.find_element_by_css_selector('.select2-search__field')
|
|
||||||
search2.send_keys('fo')
|
|
||||||
result2 = WebDriverWait(driver, 60).until(
|
result2 = WebDriverWait(driver, 60).until(
|
||||||
expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '.select2-results li:first-child'))
|
expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '.select2-results li:first-child'))
|
||||||
).text
|
).text
|
||||||
|
|
@ -322,37 +315,6 @@ class TestModelSelect2Mixin(TestHeavySelect2Mixin):
|
||||||
widget.queryset = Genre.objects.all()
|
widget.queryset = Genre.objects.all()
|
||||||
assert isinstance(widget.get_queryset(), QuerySet)
|
assert isinstance(widget.get_queryset(), QuerySet)
|
||||||
|
|
||||||
def test_tag_attrs_Select2Widget(self):
|
|
||||||
widget = Select2Widget()
|
|
||||||
output = widget.render('name', 'value')
|
|
||||||
assert 'data-minimum-input-length="0"' in output
|
|
||||||
|
|
||||||
def test_custom_tag_attrs_Select2Widget(self):
|
|
||||||
widget = Select2Widget(attrs={'data-minimum-input-length': '3'})
|
|
||||||
output = widget.render('name', 'value')
|
|
||||||
assert 'data-minimum-input-length="3"' in output
|
|
||||||
|
|
||||||
def test_tag_attrs_ModelSelect2Widget(self):
|
|
||||||
widget = ModelSelect2Widget(queryset=Genre.objects.all(), search_fields=['title__icontains'])
|
|
||||||
output = widget.render('name', 'value')
|
|
||||||
assert 'data-minimum-input-length="2"' in output
|
|
||||||
|
|
||||||
def test_tag_attrs_ModelSelect2TagWidget(self):
|
|
||||||
widget = ModelSelect2TagWidget(queryset=Genre.objects.all(), search_fields=['title__icontains'])
|
|
||||||
output = widget.render('name', 'value')
|
|
||||||
assert 'data-minimum-input-length="2"' in output
|
|
||||||
|
|
||||||
def test_tag_attrs_HeavySelect2Widget(self):
|
|
||||||
widget = HeavySelect2Widget(data_url='/foo/bar/')
|
|
||||||
output = widget.render('name', 'value')
|
|
||||||
assert 'data-minimum-input-length="2"' in output
|
|
||||||
|
|
||||||
def test_custom_tag_attrs_ModelSelect2Widget(self):
|
|
||||||
widget = ModelSelect2Widget(
|
|
||||||
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
|
|
||||||
|
|
||||||
def test_get_search_fields(self):
|
def test_get_search_fields(self):
|
||||||
widget = ModelSelect2Widget()
|
widget = ModelSelect2Widget()
|
||||||
with pytest.raises(NotImplementedError):
|
with pytest.raises(NotImplementedError):
|
||||||
|
|
@ -414,21 +376,13 @@ class TestModelSelect2Mixin(TestHeavySelect2Mixin):
|
||||||
form = forms.GroupieForm(instance=groupie)
|
form = forms.GroupieForm(instance=groupie)
|
||||||
assert '<option value="Take That" selected>TAKE THAT</option>' in form.as_p()
|
assert '<option value="Take That" selected>TAKE THAT</option>' in form.as_p()
|
||||||
|
|
||||||
def test_empty_label(self, db):
|
|
||||||
# Empty options is only required for single selects
|
|
||||||
# https://select2.github.io/options.html#allowClear
|
|
||||||
single_select = self.form.fields['primary_genre']
|
|
||||||
single_select.empty_label = 'Hello World'
|
|
||||||
assert single_select.required is False
|
|
||||||
assert 'data-placeholder="Hello World"' in single_select.widget.render('primary_genre', None)
|
|
||||||
|
|
||||||
|
|
||||||
class TestHeavySelect2TagWidget(TestHeavySelect2Mixin):
|
class TestHeavySelect2TagWidget(TestHeavySelect2Mixin):
|
||||||
|
|
||||||
def test_tag_attrs(self):
|
def test_tag_attrs(self):
|
||||||
widget = ModelSelect2TagWidget(queryset=Genre.objects.all(), search_fields=['title__icontains'])
|
widget = ModelSelect2TagWidget(queryset=Genre.objects.all(), search_fields=['title__icontains'])
|
||||||
output = widget.render('name', 'value')
|
output = widget.render('name', 'value')
|
||||||
assert 'data-minimum-input-length="2"' in output
|
assert 'data-minimum-input-length="1"' in output
|
||||||
assert 'data-tags="true"' in output
|
assert 'data-tags="true"' in output
|
||||||
assert 'data-token-separators' in output
|
assert 'data-token-separators' in output
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -149,19 +149,11 @@ class HeavySelect2WidgetForm(forms.Form):
|
||||||
class HeavySelect2MultipleWidgetForm(forms.Form):
|
class HeavySelect2MultipleWidgetForm(forms.Form):
|
||||||
title = forms.CharField(max_length=50)
|
title = forms.CharField(max_length=50)
|
||||||
genres = forms.MultipleChoiceField(
|
genres = forms.MultipleChoiceField(
|
||||||
widget=HeavySelect2MultipleWidget(
|
widget=HeavySelect2MultipleWidget(data_view='heavy_data_1', choices=NUMBER_CHOICES),
|
||||||
data_view='heavy_data_1',
|
|
||||||
choices=NUMBER_CHOICES,
|
|
||||||
attrs={'data-minimum-input-length': 0},
|
|
||||||
),
|
|
||||||
choices=NUMBER_CHOICES
|
choices=NUMBER_CHOICES
|
||||||
)
|
)
|
||||||
featured_artists = forms.MultipleChoiceField(
|
featured_artists = forms.MultipleChoiceField(
|
||||||
widget=HeavySelect2MultipleWidget(
|
widget=HeavySelect2MultipleWidget(data_view='heavy_data_2', choices=NUMBER_CHOICES),
|
||||||
data_view='heavy_data_2',
|
|
||||||
choices=NUMBER_CHOICES,
|
|
||||||
attrs={'data-minimum-input-length': 0},
|
|
||||||
),
|
|
||||||
choices=NUMBER_CHOICES,
|
choices=NUMBER_CHOICES,
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
|
@ -186,10 +178,10 @@ class AddressChainedSelect2WidgetForm(forms.Form):
|
||||||
queryset=Country.objects.all(),
|
queryset=Country.objects.all(),
|
||||||
label='Country',
|
label='Country',
|
||||||
widget=ModelSelect2Widget(
|
widget=ModelSelect2Widget(
|
||||||
|
model=Country,
|
||||||
search_fields=['name__icontains'],
|
search_fields=['name__icontains'],
|
||||||
max_results=500,
|
max_results=500,
|
||||||
dependent_fields={'city': 'cities'},
|
dependent_fields={'city': 'cities'},
|
||||||
attrs={'data-minimum-input-length': 0},
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -197,10 +189,10 @@ class AddressChainedSelect2WidgetForm(forms.Form):
|
||||||
queryset=City.objects.all(),
|
queryset=City.objects.all(),
|
||||||
label='City',
|
label='City',
|
||||||
widget=ModelSelect2Widget(
|
widget=ModelSelect2Widget(
|
||||||
|
model=City,
|
||||||
search_fields=['name__icontains'],
|
search_fields=['name__icontains'],
|
||||||
dependent_fields={'country': 'country'},
|
dependent_fields={'country': 'country'},
|
||||||
max_results=500,
|
max_results=500,
|
||||||
attrs={'data-minimum-input-length': 0},
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue