mirror of
https://github.com/Hopiu/django-select2.git
synced 2026-03-17 14:00:23 +00:00
Compare commits
25 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7fa49a6e2 | ||
|
|
39ab326884 | ||
|
|
d63f410bca | ||
|
|
5680f0daf9 | ||
|
|
8deb2b6a11 | ||
|
|
37e2515be6 | ||
|
|
cc8989a625 | ||
|
|
1ae52d7436 | ||
|
|
dca7dbc5d1 | ||
|
|
8bd7f7209f | ||
|
|
6b1ca10b06 | ||
|
|
4f96e21333 | ||
|
|
8494b10fcc | ||
|
|
ff68bf1050 | ||
|
|
c15de464d5 | ||
|
|
898b2e84dd | ||
|
|
1e4056bd62 | ||
|
|
2bb6f3a974 | ||
|
|
fa9d8baba8 | ||
|
|
df5bf9e893 | ||
|
|
4d0d702aaf | ||
|
|
0c82edf65c | ||
|
|
0021f35b2a | ||
|
|
ad42a01083 | ||
|
|
3a7ecd1666 |
17 changed files with 279 additions and 141 deletions
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
github: codingjoe
|
||||
custom: https://paypal.me/codingjoe
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -20,3 +20,6 @@ venv/
|
|||
geckodriver.log
|
||||
ghostdriver.log
|
||||
.coverage
|
||||
|
||||
coverage.xml
|
||||
.eggs/
|
||||
|
|
|
|||
49
.travis.yml
49
.travis.yml
|
|
@ -4,10 +4,10 @@ dist: xenial
|
|||
python:
|
||||
- "3.6"
|
||||
- "3.7"
|
||||
- "3.8"
|
||||
env:
|
||||
- DJANGO=20
|
||||
- DJANGO=21
|
||||
- DJANGO=22
|
||||
- DJANGO=30
|
||||
- DJANGO=master
|
||||
|
||||
matrix:
|
||||
|
|
@ -34,9 +34,14 @@ script: tox
|
|||
|
||||
after_success: codecov
|
||||
|
||||
stages:
|
||||
- test
|
||||
- name: deploy
|
||||
if: tag is present
|
||||
|
||||
jobs:
|
||||
include:
|
||||
- python: "3.5"
|
||||
- python: "3.7"
|
||||
env: TOXENV=docs
|
||||
addons:
|
||||
apt:
|
||||
|
|
@ -51,28 +56,32 @@ jobs:
|
|||
node_js: lts/*
|
||||
cache: npm
|
||||
- stage: deploy
|
||||
if: tag IS present
|
||||
python: 3.7
|
||||
python: "3.8"
|
||||
install: skip
|
||||
script: skip
|
||||
after_success: skip
|
||||
before_deploy:
|
||||
- git stash --all
|
||||
- ./set_version.py
|
||||
_deploy_provider: &_deploy_provider
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
repo: applegrew/django-select2
|
||||
after_success: true
|
||||
deploy:
|
||||
- <<: *_deploy_provider
|
||||
provider: pypi
|
||||
distributions: sdist bdist_wheel
|
||||
on:
|
||||
tags: true
|
||||
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=
|
||||
- stage: deploy
|
||||
language: node_js
|
||||
node_js: lts/*
|
||||
python: "3.8"
|
||||
install: skip
|
||||
script: skip
|
||||
after_success: true
|
||||
skip_cleanup: true
|
||||
before_deploy:
|
||||
- ./set_version.py
|
||||
deploy:
|
||||
provider: npm
|
||||
on:
|
||||
tags: true
|
||||
email: info@johanneshoppe.com
|
||||
api_key:
|
||||
secure: PV38cQx9qhEFkpSdytbM72UzIMCfhpjmRJ8dzT+bCAaOIs5rEcyKN+h1r5ranunCxWyuFsMW4A2iW/SCxnKCR/oPAguuwUbT5ogBXlsskqPFWUxuoTHYMrd+zB+SC6+bMgq+o5ul+kJCYtEkWP6cMlIEzKyTLab7m5PsnDXNVnI=
|
||||
|
|
|
|||
22
CONTRIBUTING.rst
Normal file
22
CONTRIBUTING.rst
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
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.
|
||||
6
MANIFEST.in
Normal file
6
MANIFEST.in
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
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):
|
||||
"""Settings for Django-Select2."""
|
||||
|
||||
LIB_VERSION = '4.0.5'
|
||||
LIB_VERSION = '4.0.12'
|
||||
"""Version of the Select2 library."""
|
||||
|
||||
CACHE_BACKEND = 'default'
|
||||
|
|
|
|||
|
|
@ -20,10 +20,11 @@ Widgets are generally of two types:
|
|||
drop-in-replacement for Django's default
|
||||
select widgets.
|
||||
|
||||
2. **Heavy** --
|
||||
2(a). **Heavy** --
|
||||
They are suited for scenarios when the number of options
|
||||
are large and need complex queries (from maybe different
|
||||
sources) to get the options.
|
||||
|
||||
This dynamic fetching of options undoubtedly requires
|
||||
Ajax communication with the server. Django-Select2 includes
|
||||
a helper JS file which is included automatically,
|
||||
|
|
@ -31,15 +32,15 @@ Widgets are generally of two types:
|
|||
Although on the server side you do need to create a view
|
||||
specifically to respond to the queries.
|
||||
|
||||
3. **Model** --
|
||||
2(b). **Model** --
|
||||
Model-widgets are a further specialized versions of Heavies.
|
||||
These do not require views to serve Ajax requests.
|
||||
When they are instantiated, they register themselves
|
||||
with one central view which handles Ajax requests for them.
|
||||
|
||||
Heavy widgets have the word 'Heavy' in their name.
|
||||
Light widgets are normally named, i.e. there is no
|
||||
'Light' word in their names.
|
||||
Heavy and Model widgets have respectively the word 'Heavy' and 'Model' in
|
||||
their name. Light widgets are normally named, i.e. there is no 'Light' word
|
||||
in their names.
|
||||
|
||||
.. inheritance-diagram:: django_select2.forms
|
||||
:parts: 1
|
||||
|
|
@ -70,16 +71,20 @@ class Select2Mixin:
|
|||
form media.
|
||||
"""
|
||||
|
||||
def build_attrs(self, *args, **kwargs):
|
||||
"""Add select2 data attributes."""
|
||||
attrs = super(Select2Mixin, self).build_attrs(*args, **kwargs)
|
||||
if self.is_required:
|
||||
attrs.setdefault('data-allow-clear', 'false')
|
||||
else:
|
||||
attrs.setdefault('data-allow-clear', 'true')
|
||||
attrs.setdefault('data-placeholder', '')
|
||||
empty_label = ''
|
||||
|
||||
def build_attrs(self, base_attrs, extra_attrs=None):
|
||||
"""Add select2 data attributes."""
|
||||
default_attrs = {'data-minimum-input-length': 0}
|
||||
if self.is_required:
|
||||
default_attrs['data-allow-clear'] = 'false'
|
||||
else:
|
||||
default_attrs['data-allow-clear'] = 'true'
|
||||
default_attrs['data-placeholder'] = self.empty_label or ""
|
||||
|
||||
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:
|
||||
attrs['class'] += ' django-select2'
|
||||
else:
|
||||
|
|
@ -120,12 +125,15 @@ class Select2Mixin:
|
|||
class Select2TagMixin:
|
||||
"""Mixin to add select2 tag functionality."""
|
||||
|
||||
def build_attrs(self, *args, **kwargs):
|
||||
def build_attrs(self, base_attrs, extra_attrs=None):
|
||||
"""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)
|
||||
default_attrs = {
|
||||
'data-minimum-input-length': 1,
|
||||
'data-tags': 'true',
|
||||
'data-token-separators': '[",", " "]'
|
||||
}
|
||||
default_attrs.update(base_attrs)
|
||||
return super().build_attrs(default_attrs, extra_attrs=extra_attrs)
|
||||
|
||||
|
||||
class Select2Widget(Select2Mixin, forms.Select):
|
||||
|
|
@ -203,6 +211,7 @@ class HeavySelect2Mixin:
|
|||
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:
|
||||
|
|
@ -226,20 +235,26 @@ class HeavySelect2Mixin:
|
|||
return self.data_url
|
||||
return reverse(self.data_view)
|
||||
|
||||
def build_attrs(self, *args, **kwargs):
|
||||
def build_attrs(self, base_attrs, extra_attrs=None):
|
||||
"""Set select2's AJAX attributes."""
|
||||
attrs = super(HeavySelect2Mixin, self).build_attrs(*args, **kwargs)
|
||||
default_attrs = {
|
||||
'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
|
||||
self.widget_id = signing.dumps(id(self))
|
||||
|
||||
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'
|
||||
return attrs
|
||||
|
|
@ -328,6 +343,12 @@ class ModelSelect2Mixin:
|
|||
max_results = 25
|
||||
"""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):
|
||||
"""
|
||||
Overwrite class parameters if passed as keyword arguments.
|
||||
|
|
@ -391,7 +412,7 @@ class ModelSelect2Mixin:
|
|||
term = term.replace('\t', ' ')
|
||||
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,
|
||||
select &= reduce(lambda x, y: x | Q(**{y: t}), search_fields[1:],
|
||||
Q(**{search_fields[0]: t}))
|
||||
if dependent_fields:
|
||||
select &= Q(**dependent_fields)
|
||||
|
|
@ -408,6 +429,8 @@ class ModelSelect2Mixin:
|
|||
"""
|
||||
if self.queryset is not None:
|
||||
queryset = self.queryset
|
||||
elif hasattr(self.choices, 'queryset'):
|
||||
queryset = self.choices.queryset
|
||||
elif self.model is not None:
|
||||
queryset = self.model._default_manager.all()
|
||||
else:
|
||||
|
|
@ -541,14 +564,15 @@ 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)
|
||||
qs = self.queryset.filter(**{'pk__in': list(values)})
|
||||
pks = set(str(getattr(o, pk)) for o in qs)
|
||||
cleaned_values = []
|
||||
for val in value:
|
||||
if str(val) not in pks:
|
||||
val = queryset.create(title=val).pk
|
||||
cleaned_values.append(val)
|
||||
'''Create objects for given non-pimary-key values. Return list of all primary keys.'''
|
||||
values = set(super().value_from_datadict(data, files, name))
|
||||
# This may only work for MyModel, if MyModel has title field.
|
||||
# You need to implement this method yourself, to ensure proper object creation.
|
||||
pks = self.queryset.filter(**{'pk__in': list(values)}).values_list('pk', flat=True)
|
||||
pks = set(map(str, pks))
|
||||
cleaned_values = list(values)
|
||||
for val in values - pks:
|
||||
cleaned_values.append(self.queryset.create(title=val).pk)
|
||||
return cleaned_values
|
||||
|
||||
"""
|
||||
|
|
|
|||
1
docs/CONTRIBUTING.rst
Normal file
1
docs/CONTRIBUTING.rst
Normal file
|
|
@ -0,0 +1 @@
|
|||
.. include:: ../CONTRIBUTING.rst
|
||||
91
docs/conf.py
91
docs/conf.py
|
|
@ -1,39 +1,52 @@
|
|||
import os
|
||||
import sys
|
||||
|
||||
# This is needed since django_select2 requires django model modules
|
||||
# and those modules assume that django settings is configured and
|
||||
# have proper DB 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
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
sys.path.insert(0, os.path.abspath('../tests.testapp'))
|
||||
sys.path.insert(0, os.path.abspath('..'))
|
||||
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.napoleon',
|
||||
'sphinx.ext.inheritance_diagram',
|
||||
'sphinx.ext.intersphinx',
|
||||
'sphinx.ext.viewcode',
|
||||
'sphinx.ext.doctest',
|
||||
]
|
||||
|
||||
intersphinx_mapping = {
|
||||
'python': ('http://docs.python.org/3', None),
|
||||
'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)
|
||||
import os
|
||||
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
|
||||
# have proper DB 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
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
sys.path.insert(0, os.path.abspath('../tests.testapp'))
|
||||
sys.path.insert(0, os.path.abspath('..'))
|
||||
|
||||
|
||||
project = "Django-Select2"
|
||||
author = "Johannes Hoppe"
|
||||
copyright = "2017, Johannes Hoppe"
|
||||
release = get_distribution('django_select2').version
|
||||
version = '.'.join(release.split('.')[:2])
|
||||
|
||||
|
||||
master_doc = 'index' # default in Sphinx v2
|
||||
|
||||
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.napoleon',
|
||||
'sphinx.ext.inheritance_diagram',
|
||||
'sphinx.ext.intersphinx',
|
||||
'sphinx.ext.viewcode',
|
||||
'sphinx.ext.doctest',
|
||||
]
|
||||
|
||||
intersphinx_mapping = {
|
||||
'python': ('http://docs.python.org/3', None),
|
||||
'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,8 +49,9 @@ DjangoSelect2 handles the initialization of select2 fields automatically. Just i
|
|||
``{{ 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.::
|
||||
yourself, DjangoSelect2 provides a jQuery plugin, replacing and enhancing the Select2
|
||||
plugin. It will handle both normal and heavy fields. Simply call
|
||||
``djangoSelect2(options)`` on your select fields.::
|
||||
|
||||
$('.django-select2').djangoSelect2();
|
||||
|
||||
|
|
@ -59,6 +60,9 @@ You can pass see `Select2 options <https://select2.github.io/options.html>`_ if
|
|||
|
||||
$('.django-select2').djangoSelect2({placeholder: 'Select an option'});
|
||||
|
||||
Please replace all your ``.select2`` invocations with the here provided
|
||||
``.djangoSelect2``.
|
||||
|
||||
Security & Authentication
|
||||
-------------------------
|
||||
|
||||
|
|
@ -81,6 +85,6 @@ want to be accessible to the general public. Doing so is easy::
|
|||
kwargs['data_view'] = 'user-select2-view'
|
||||
super(UserSelect2WidgetMixin, self).__init__(*args, **kwargs)
|
||||
|
||||
class MySecretWidget(UserSelect2WidgetMixin, Select2ModelWidget):
|
||||
class MySecretWidget(UserSelect2WidgetMixin, ModelSelect2Widget):
|
||||
model = MySecretModel
|
||||
search_fields = ['title__icontains']
|
||||
|
|
|
|||
|
|
@ -8,6 +8,12 @@ Overview
|
|||
.. automodule:: django_select2
|
||||
:members:
|
||||
|
||||
Assumptions
|
||||
-----------
|
||||
|
||||
* You have a running Django up and running.
|
||||
* You have form fully working without Django-Select2.
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
|
|
@ -17,11 +23,12 @@ Installation
|
|||
|
||||
2. Add ``django_select2`` to your ``INSTALLED_APPS`` in your project settings.
|
||||
|
||||
3. Add ``django_select`` to your ``urlconf``::
|
||||
|
||||
3. Add ``django_select`` to your ``urlconf`` **if** you use any
|
||||
:class:`ModelWidgets <.django_select2.forms.ModelSelect2Mixin>`::
|
||||
path('select2/', include('django_select2.urls')),
|
||||
|
||||
url(r'^select2/', include('django_select2.urls')),
|
||||
You can safely skip this one if you do not use any
|
||||
:class:`ModelWidgets <.django_select2.forms.ModelSelect2Mixin>`
|
||||
|
||||
Quick Start
|
||||
-----------
|
||||
|
|
@ -30,19 +37,22 @@ 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::
|
||||
1. Replace native Django forms widgets with one of the several ``django_select2.form`` widgets.
|
||||
Start by importing them into your ``forms.py``, right next to Django own ones::
|
||||
|
||||
class MyForm(forms.Form):
|
||||
things = ModelMultipleChoiceField(queryset=Thing.objects.all())
|
||||
from django import forms
|
||||
from django_select2 import forms as s2forms
|
||||
|
||||
with::
|
||||
Then let's assume you have a model with a choice, a :class:`.ForeignKey`, and a
|
||||
:class:`.ManyToManyField`, you would add this information to your Form Meta
|
||||
class::
|
||||
|
||||
from django_select2.forms import Select2MultipleWidget
|
||||
|
||||
class MyForm(forms.Form):
|
||||
things = ModelMultipleChoiceField(queryset=Thing.objects.all(), widget=Select2MultipleWidget)
|
||||
widgets = {
|
||||
'category': s2forms.Select2Widget,
|
||||
'author': s2forms.ModelSelect2Widget(model=auth.get_user_model(),
|
||||
search_fields=['first_name__istartswith', 'last_name__icontains']),
|
||||
'attending': s2forms.ModelSelect2MultipleWidget …
|
||||
}
|
||||
|
||||
2. Add the CSS to the ``head`` of your Django template::
|
||||
|
||||
|
|
@ -57,9 +67,9 @@ with::
|
|||
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.
|
||||
* jQuery (version >=2)
|
||||
jQuery is not included in the package since it is expected
|
||||
that in most scenarios jQuery is already loaded.
|
||||
|
||||
Example Application
|
||||
-------------------
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ Contents:
|
|||
get_started
|
||||
django_select2
|
||||
extra
|
||||
CONTRIBUTING
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
|
|
|||
22
setup.cfg
22
setup.cfg
|
|
@ -19,12 +19,12 @@ classifier =
|
|||
|
||||
[options]
|
||||
include_package_data = True
|
||||
packages = find:
|
||||
packages = django_select2
|
||||
install_requires =
|
||||
django>=2.0
|
||||
django>=2.2
|
||||
django-appconf>=0.6.0
|
||||
setup_requires =
|
||||
very-good-setuptools-git-version
|
||||
setuptools_scm
|
||||
sphinx
|
||||
pytest-runner
|
||||
tests_require =
|
||||
|
|
@ -33,13 +33,6 @@ tests_require =
|
|||
pytest-django
|
||||
selenium
|
||||
|
||||
[options.package_data]
|
||||
* = *.txt, *.rst, *.html, *.po
|
||||
|
||||
[options.packages.find]
|
||||
exclude =
|
||||
tests
|
||||
|
||||
[bdist_wheel]
|
||||
universal = 1
|
||||
|
||||
|
|
@ -54,22 +47,19 @@ test = pytest
|
|||
[build_sphinx]
|
||||
source-dir = docs
|
||||
build-dir = docs/_build
|
||||
project = Django-Select2
|
||||
copyright = 2017 Johannes Hoppe
|
||||
|
||||
[tool:pytest]
|
||||
addopts = --cov=django_select2 --cov-report xml
|
||||
DJANGO_SETTINGS_MODULE=tests.testapp.settings
|
||||
|
||||
[tox:tox]
|
||||
envlist = py{35,36,37}-dj{22,21,20,master},docs
|
||||
envlist = py{36,37,38}-dj{22,30,master},docs
|
||||
|
||||
[testenv]
|
||||
passenv=CI
|
||||
deps =
|
||||
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
|
||||
dj22: django~=2.2
|
||||
dj30: django~=3.0
|
||||
djmaster: https://github.com/django/django/archive/master.tar.gz#egg=django
|
||||
commands = python setup.py test
|
||||
|
||||
|
|
|
|||
2
setup.py
2
setup.py
|
|
@ -2,4 +2,4 @@
|
|||
|
||||
from setuptools import setup
|
||||
|
||||
setup(version_format='{tag}')
|
||||
setup(name='django-select2', use_scm_version=True)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import random
|
|||
import string
|
||||
|
||||
import pytest
|
||||
from django.utils.encoding import force_text
|
||||
from selenium import webdriver
|
||||
from selenium.common.exceptions import WebDriverException
|
||||
|
||||
|
|
@ -26,7 +25,7 @@ def driver():
|
|||
try:
|
||||
b = webdriver.Chrome(options=chrome_options)
|
||||
except WebDriverException as e:
|
||||
pytest.skip(force_text(e))
|
||||
pytest.skip(str(e))
|
||||
else:
|
||||
yield b
|
||||
b.quit()
|
||||
|
|
|
|||
|
|
@ -95,7 +95,9 @@ class TestSelect2Mixin:
|
|||
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)
|
||||
output = multiple_select.widget.render('featured_artists', None)
|
||||
assert '<option value=""></option>' not in output
|
||||
assert 'data-placeholder=""' in output
|
||||
|
||||
def test_i18n(self):
|
||||
translation.activate('de')
|
||||
|
|
@ -217,12 +219,17 @@ class TestHeavySelect2Mixin(TestSelect2Mixin):
|
|||
driver.find_element_by_css_selector('.select2-results')
|
||||
|
||||
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(
|
||||
expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '.select2-results li:first-child'))
|
||||
).text
|
||||
|
||||
elem2.click()
|
||||
search2 = driver.find_element_by_css_selector('.select2-search__field')
|
||||
search2.send_keys('fo')
|
||||
result2 = WebDriverWait(driver, 60).until(
|
||||
expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '.select2-results li:first-child'))
|
||||
).text
|
||||
|
|
@ -315,6 +322,37 @@ class TestModelSelect2Mixin(TestHeavySelect2Mixin):
|
|||
widget.queryset = Genre.objects.all()
|
||||
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):
|
||||
widget = ModelSelect2Widget()
|
||||
with pytest.raises(NotImplementedError):
|
||||
|
|
@ -376,13 +414,21 @@ class TestModelSelect2Mixin(TestHeavySelect2Mixin):
|
|||
form = forms.GroupieForm(instance=groupie)
|
||||
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):
|
||||
|
||||
def test_tag_attrs(self):
|
||||
widget = ModelSelect2TagWidget(queryset=Genre.objects.all(), search_fields=['title__icontains'])
|
||||
output = widget.render('name', 'value')
|
||||
assert 'data-minimum-input-length="1"' in output
|
||||
assert 'data-minimum-input-length="2"' in output
|
||||
assert 'data-tags="true"' in output
|
||||
assert 'data-token-separators' in output
|
||||
|
||||
|
|
|
|||
|
|
@ -149,11 +149,19 @@ class HeavySelect2WidgetForm(forms.Form):
|
|||
class HeavySelect2MultipleWidgetForm(forms.Form):
|
||||
title = forms.CharField(max_length=50)
|
||||
genres = forms.MultipleChoiceField(
|
||||
widget=HeavySelect2MultipleWidget(data_view='heavy_data_1', choices=NUMBER_CHOICES),
|
||||
widget=HeavySelect2MultipleWidget(
|
||||
data_view='heavy_data_1',
|
||||
choices=NUMBER_CHOICES,
|
||||
attrs={'data-minimum-input-length': 0},
|
||||
),
|
||||
choices=NUMBER_CHOICES
|
||||
)
|
||||
featured_artists = forms.MultipleChoiceField(
|
||||
widget=HeavySelect2MultipleWidget(data_view='heavy_data_2', choices=NUMBER_CHOICES),
|
||||
widget=HeavySelect2MultipleWidget(
|
||||
data_view='heavy_data_2',
|
||||
choices=NUMBER_CHOICES,
|
||||
attrs={'data-minimum-input-length': 0},
|
||||
),
|
||||
choices=NUMBER_CHOICES,
|
||||
required=False
|
||||
)
|
||||
|
|
@ -178,10 +186,10 @@ class AddressChainedSelect2WidgetForm(forms.Form):
|
|||
queryset=Country.objects.all(),
|
||||
label='Country',
|
||||
widget=ModelSelect2Widget(
|
||||
model=Country,
|
||||
search_fields=['name__icontains'],
|
||||
max_results=500,
|
||||
dependent_fields={'city': 'cities'},
|
||||
attrs={'data-minimum-input-length': 0},
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -189,10 +197,10 @@ class AddressChainedSelect2WidgetForm(forms.Form):
|
|||
queryset=City.objects.all(),
|
||||
label='City',
|
||||
widget=ModelSelect2Widget(
|
||||
model=City,
|
||||
search_fields=['name__icontains'],
|
||||
dependent_fields={'country': 'country'},
|
||||
max_results=500,
|
||||
attrs={'data-minimum-input-length': 0},
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue