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
View file

@ -1,2 +0,0 @@
github: codingjoe
custom: https://paypal.me/codingjoe

3
.gitignore vendored
View file

@ -20,6 +20,3 @@ venv/
geckodriver.log geckodriver.log
ghostdriver.log ghostdriver.log
.coverage .coverage
coverage.xml
.eggs/

View file

@ -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=

View file

@ -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.

View file

@ -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

View file

@ -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'

View file

@ -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
""" """

View file

@ -1 +0,0 @@
.. include:: ../CONTRIBUTING.rst

View file

@ -1,8 +1,6 @@
import os import os
import sys import sys
from pkg_resources import get_distribution
# This is needed since django_select2 requires django model modules # This is needed since django_select2 requires django model modules
# and those modules assume that django settings is configured and # and those modules assume that django settings is configured and
# have proper DB settings. # have proper DB settings.
@ -15,17 +13,6 @@ 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('../tests.testapp'))
sys.path.insert(0, os.path.abspath('..')) 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 = [ extensions = [
'sphinx.ext.autodoc', 'sphinx.ext.autodoc',
'sphinx.ext.napoleon', 'sphinx.ext.napoleon',

View file

@ -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']

View file

@ -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
------------------- -------------------

View file

@ -15,7 +15,6 @@ Contents:
get_started get_started
django_select2 django_select2
extra extra
CONTRIBUTING
Indices and tables Indices and tables
================== ==================

View file

@ -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

View file

@ -2,4 +2,4 @@
from setuptools import setup from setuptools import setup
setup(name='django-select2', use_scm_version=True) setup(version_format='{tag}')

View file

@ -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()

View file

@ -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

View file

@ -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},
) )
) )