mirror of
https://github.com/Hopiu/django-select2.git
synced 2026-04-19 12:41:12 +00:00
Compare commits
No commits in common. "master" and "7.0.5" have entirely different histories.
14 changed files with 48 additions and 108 deletions
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
|
|
@ -1,2 +0,0 @@
|
||||||
github: codingjoe
|
|
||||||
custom: https://paypal.me/codingjoe
|
|
||||||
10
.travis.yml
10
.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:
|
||||||
|
|
@ -41,7 +41,7 @@ stages:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
include:
|
include:
|
||||||
- python: "3.7"
|
- python: "3.5"
|
||||||
env: TOXENV=docs
|
env: TOXENV=docs
|
||||||
addons:
|
addons:
|
||||||
apt:
|
apt:
|
||||||
|
|
@ -56,7 +56,7 @@ jobs:
|
||||||
node_js: lts/*
|
node_js: lts/*
|
||||||
cache: npm
|
cache: npm
|
||||||
- stage: deploy
|
- stage: deploy
|
||||||
python: "3.8"
|
python: "3.7"
|
||||||
install: skip
|
install: skip
|
||||||
script: skip
|
script: skip
|
||||||
after_success: true
|
after_success: true
|
||||||
|
|
@ -71,7 +71,7 @@ jobs:
|
||||||
- stage: deploy
|
- stage: deploy
|
||||||
language: node_js
|
language: node_js
|
||||||
node_js: lts/*
|
node_js: lts/*
|
||||||
python: "3.8"
|
python: "3.7"
|
||||||
install: skip
|
install: skip
|
||||||
script: skip
|
script: skip
|
||||||
after_success: true
|
after_success: true
|
||||||
|
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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,8 +70,6 @@ class Select2Mixin:
|
||||||
form media.
|
form media.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
empty_label = ''
|
|
||||||
|
|
||||||
def build_attrs(self, base_attrs, extra_attrs=None):
|
def build_attrs(self, base_attrs, extra_attrs=None):
|
||||||
"""Add select2 data attributes."""
|
"""Add select2 data attributes."""
|
||||||
default_attrs = {'data-minimum-input-length': 0}
|
default_attrs = {'data-minimum-input-length': 0}
|
||||||
|
|
@ -80,7 +77,7 @@ class Select2Mixin:
|
||||||
default_attrs['data-allow-clear'] = 'false'
|
default_attrs['data-allow-clear'] = 'false'
|
||||||
else:
|
else:
|
||||||
default_attrs['data-allow-clear'] = 'true'
|
default_attrs['data-allow-clear'] = 'true'
|
||||||
default_attrs['data-placeholder'] = self.empty_label or ""
|
default_attrs['data-placeholder'] = ''
|
||||||
|
|
||||||
default_attrs.update(base_attrs)
|
default_attrs.update(base_attrs)
|
||||||
attrs = super().build_attrs(default_attrs, extra_attrs=extra_attrs)
|
attrs = super().build_attrs(default_attrs, extra_attrs=extra_attrs)
|
||||||
|
|
@ -211,7 +208,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:
|
||||||
|
|
@ -343,12 +339,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.
|
||||||
|
|
@ -429,8 +419,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 +552,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
|
|
||||||
|
|
@ -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
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
==================
|
==================
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ classifier =
|
||||||
include_package_data = True
|
include_package_data = True
|
||||||
packages = django_select2
|
packages = django_select2
|
||||||
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
|
setuptools_scm
|
||||||
|
|
@ -53,13 +53,14 @@ 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(use_scm_version=True)
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ def random_name(n):
|
||||||
@pytest.yield_fixture(scope='session')
|
@pytest.yield_fixture(scope='session')
|
||||||
def driver():
|
def driver():
|
||||||
chrome_options = webdriver.ChromeOptions()
|
chrome_options = webdriver.ChromeOptions()
|
||||||
chrome_options.headless = True
|
chrome_options.headless = False
|
||||||
try:
|
try:
|
||||||
b = webdriver.Chrome(options=chrome_options)
|
b = webdriver.Chrome(options=chrome_options)
|
||||||
except WebDriverException as e:
|
except WebDriverException as e:
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
|
@ -414,14 +412,6 @@ 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):
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -186,6 +186,7 @@ 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'},
|
||||||
|
|
@ -197,6 +198,7 @@ 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,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue