Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1d42bfcad | ||
|
|
1954bf57fa | ||
|
|
08ca1c1c36 | ||
|
|
aef27f48df | ||
|
|
f31beec0c4 | ||
|
|
496cc7c502 | ||
|
|
17af750d88 | ||
|
|
67bf060991 | ||
|
|
d9c899e8f7 | ||
|
|
e4729feb9a | ||
|
|
2ea6ef8edb | ||
|
|
a9aa31146f | ||
|
|
c2331b272a | ||
|
|
5d98048c9f |
@@ -1,6 +1,8 @@
|
||||
Changelog Summary
|
||||
=================
|
||||
|
||||
Changes for django-select2 v6.0.1 and later are logged on the [github releases page](https://github.com/applegrew/django-select2/releases).
|
||||
|
||||
### v6.0.0
|
||||
|
||||
* Add Support for Django 2.0
|
||||
|
||||
+7
-1
@@ -7,6 +7,10 @@ This is a `Django`_ integration of `Select2`_.
|
||||
|
||||
The app includes Select2 driven Django Widgets.
|
||||
|
||||
.. note::
|
||||
Django's admin comes with builtin support for Select2
|
||||
since version 2.0 via the `autocomplete_fields`_ feature.
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
@@ -46,12 +50,14 @@ good example.
|
||||
Changelog
|
||||
---------
|
||||
|
||||
See `CHANGELOG.md`_
|
||||
See `Github releases`_
|
||||
|
||||
|
||||
.. _Django: https://www.djangoproject.com/
|
||||
.. _Select2: http://ivaynberg.github.com/select2/
|
||||
.. _autocomplete_fields: https://docs.djangoproject.com/en/stable/ref/contrib/admin/#django.contrib.admin.ModelAdmin.autocomplete_fields
|
||||
.. _CHANGELOG.md: CHANGELOG.md
|
||||
.. _Github releases: https://github.com/applegrew/django-select2/releases
|
||||
|
||||
.. |version| image:: https://img.shields.io/pypi/v/Django-Select2.svg
|
||||
:target: https://pypi.python.org/pypi/Django-Select2/
|
||||
|
||||
+16
-3
@@ -8,6 +8,9 @@ __all__ = ('settings', 'Select2Conf')
|
||||
class Select2Conf(AppConf):
|
||||
"""Settings for Django-Select2."""
|
||||
|
||||
LIB_VERSION = '4.0.5'
|
||||
"""Version of the Select2 library."""
|
||||
|
||||
CACHE_BACKEND = 'default'
|
||||
"""
|
||||
Django-Select2 uses Django's cache to sure a consistent state across multiple machines.
|
||||
@@ -53,7 +56,7 @@ class Select2Conf(AppConf):
|
||||
It has set `select2_` as a default value, which you can change if needed.
|
||||
"""
|
||||
|
||||
JS = '//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/select2.min.js'
|
||||
JS = f'//cdnjs.cloudflare.com/ajax/libs/select2/{LIB_VERSION}/js/select2.min.js'
|
||||
"""
|
||||
The URI for the Select2 JS file. By default this points to the Cloudflare CDN.
|
||||
|
||||
@@ -62,11 +65,16 @@ class Select2Conf(AppConf):
|
||||
|
||||
SELECT2_JS = 'assets/js/select2.min.js'
|
||||
|
||||
If you provide your own JS and would not like Django-Select2 to load any, change
|
||||
this setting to a blank string like so::
|
||||
|
||||
SELECT2_JS = ''
|
||||
|
||||
.. tip:: Change this setting to a local asset in your development environment to
|
||||
develop without an Internet connection.
|
||||
"""
|
||||
|
||||
CSS = '//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/css/select2.min.css'
|
||||
CSS = f'//cdnjs.cloudflare.com/ajax/libs/select2/{LIB_VERSION}/css/select2.min.css'
|
||||
"""
|
||||
The URI for the Select2 CSS file. By default this points to the Cloudflare CDN.
|
||||
|
||||
@@ -75,11 +83,16 @@ class Select2Conf(AppConf):
|
||||
|
||||
SELECT2_CSS = 'assets/css/select2.css'
|
||||
|
||||
If you provide your own CSS and would not like Django-Select2 to load any, change
|
||||
this setting to a blank string like so::
|
||||
|
||||
SELECT2_CSS = ''
|
||||
|
||||
.. tip:: Change this setting to a local asset in your development environment to
|
||||
develop without an Internet connection.
|
||||
"""
|
||||
|
||||
I18N_PATH = '//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/i18n'
|
||||
I18N_PATH = f'//cdnjs.cloudflare.com/ajax/libs/select2/{LIB_VERSION}/js/i18n'
|
||||
"""
|
||||
The base URI for the Select2 i18n files. By default this points to the Cloudflare CDN.
|
||||
|
||||
|
||||
+32
-17
@@ -47,7 +47,7 @@ Light widgets are normally named, i.e. there is no
|
||||
"""
|
||||
from functools import reduce
|
||||
from itertools import chain
|
||||
from pickle import PicklingError
|
||||
from pickle import PicklingError # nosec
|
||||
|
||||
from django import forms
|
||||
from django.core import signing
|
||||
@@ -96,17 +96,31 @@ class Select2Mixin(object):
|
||||
Construct Media as a dynamic property.
|
||||
|
||||
.. Note:: For more information visit
|
||||
https://docs.djangoproject.com/en/1.8/topics/forms/media/#media-as-a-dynamic-property
|
||||
https://docs.djangoproject.com/en/stable/topics/forms/media/#media-as-a-dynamic-property
|
||||
"""
|
||||
lang = get_language()
|
||||
i18n_name = None
|
||||
select2_js = (settings.SELECT2_JS,) if settings.SELECT2_JS else ()
|
||||
select2_css = (settings.SELECT2_CSS,) if settings.SELECT2_CSS else ()
|
||||
|
||||
try:
|
||||
# get_language() will always return a lower case language code, where some files are named upper case.
|
||||
i = [x.lower() for x in settings.SELECT2_I18N_AVAILABLE_LANGUAGES].index(get_language())
|
||||
i18n_file = ('%s/%s.js' % (settings.SELECT2_I18N_PATH, settings.SELECT2_I18N_AVAILABLE_LANGUAGES[i]), )
|
||||
except ValueError:
|
||||
i18n_file = ()
|
||||
from django.contrib.admin.widgets import SELECT2_TRANSLATIONS
|
||||
i18n_name = SELECT2_TRANSLATIONS.get(lang)
|
||||
if i18n_name not in settings.SELECT2_I18N_AVAILABLE_LANGUAGES:
|
||||
i18n_name = None
|
||||
except ImportError:
|
||||
# TODO: select2 widget feature needs to be backported into Django 1.11
|
||||
try:
|
||||
i = [x.lower() for x in settings.SELECT2_I18N_AVAILABLE_LANGUAGES].index(lang)
|
||||
i18n_name = settings.SELECT2_I18N_AVAILABLE_LANGUAGES[i]
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
i18n_file = ('%s/%s.js' % (settings.SELECT2_I18N_PATH, i18n_name),) if i18n_name else ()
|
||||
|
||||
return forms.Media(
|
||||
js=(settings.SELECT2_JS,) + i18n_file + ('django_select2/django_select2.js',),
|
||||
css={'screen': (settings.SELECT2_CSS,)}
|
||||
js=select2_js + i18n_file + ('django_select2/django_select2.js',),
|
||||
css={'screen': select2_css}
|
||||
)
|
||||
|
||||
media = property(_get_media)
|
||||
@@ -277,7 +291,7 @@ class HeavySelect2Widget(HeavySelect2Mixin, Select2Widget):
|
||||
or::
|
||||
|
||||
class MyForm(forms.Form):
|
||||
my_field = forms.ChoicesField(
|
||||
my_field = forms.ChoiceField(
|
||||
widget=HeavySelect2Widget(
|
||||
data_url='/url/to/json/response'
|
||||
)
|
||||
@@ -370,8 +384,8 @@ class ModelSelect2Mixin(object):
|
||||
term (str): Search term
|
||||
queryset (django.db.models.query.QuerySet): QuerySet to select choices from.
|
||||
**dependent_fields: Dependent fields and their values. If you want to inherit
|
||||
from ModelSelect2Mixin and later call to this method, be sure to pop
|
||||
from kwargs everything if it is not a dependent field.
|
||||
from ModelSelect2Mixin and later call to this method, be sure to pop
|
||||
everything from keyword arguments that is not a dependent field.
|
||||
|
||||
Returns:
|
||||
QuerySet: Filtered QuerySet
|
||||
@@ -433,11 +447,12 @@ class ModelSelect2Mixin(object):
|
||||
c for c in selected_choices
|
||||
if c not in self.choices.field.empty_values
|
||||
}
|
||||
choices = (
|
||||
(obj.pk, self.label_from_instance(obj))
|
||||
for obj in self.choices.queryset.filter(pk__in=selected_choices)
|
||||
)
|
||||
for option_value, option_label in choices:
|
||||
field_name = self.choices.field.to_field_name or 'pk'
|
||||
query = Q(**{'%s__in' % field_name: selected_choices})
|
||||
for obj in self.choices.queryset.filter(query):
|
||||
option_value = self.choices.choice(obj)[0]
|
||||
option_label = self.label_from_instance(obj)
|
||||
|
||||
selected = (
|
||||
str(option_value) in value and
|
||||
(has_selected is False or self.allow_multiple_selected)
|
||||
|
||||
@@ -23,6 +23,34 @@ Installation
|
||||
|
||||
url(r'^select2/', include('django_select2.urls')),
|
||||
|
||||
Quick Start
|
||||
-----------
|
||||
|
||||
Here is a quick example to get you started:
|
||||
|
||||
0. Follow the installation instructions above.
|
||||
|
||||
1. Add a select2 widget to the form. For example if you wanted Select2 with multi-select you would use
|
||||
``Select2MultipleWidget``
|
||||
Replacing::
|
||||
|
||||
class MyForm(forms.Form):
|
||||
things = ModelMultipleChoiceField(queryset=Thing.objects.all())
|
||||
|
||||
with::
|
||||
|
||||
class MyForm(forms.Form):
|
||||
things = ModelMultipleChoiceField(queryset=Thing.objects.all(), widget=Select2MultipleWidget)
|
||||
|
||||
2. Add the CSS to the ``head`` of your Django template::
|
||||
|
||||
{{ form.media.css }}
|
||||
|
||||
3. Add the JavaScript to the end of the ``body`` of your Django template::
|
||||
|
||||
{{ form.media.js }}
|
||||
|
||||
4. Done - enjoy the wonders of Select2!
|
||||
|
||||
External Dependencies
|
||||
---------------------
|
||||
|
||||
@@ -8,7 +8,7 @@ pydocstyle
|
||||
pytest
|
||||
pytest-django
|
||||
selenium
|
||||
sphinx
|
||||
sphinx==1.6.5
|
||||
sphinxcontrib-spelling
|
||||
pyenchant
|
||||
django-redis
|
||||
|
||||
+41
-13
@@ -14,6 +14,7 @@ from selenium.webdriver.support import expected_conditions
|
||||
from selenium.webdriver.support.wait import WebDriverWait
|
||||
|
||||
from django_select2.cache import cache
|
||||
from django_select2.conf import settings
|
||||
from django_select2.forms import (
|
||||
HeavySelect2MultipleWidget, HeavySelect2Widget, ModelSelect2TagWidget,
|
||||
ModelSelect2Widget, Select2Widget
|
||||
@@ -22,7 +23,7 @@ from tests.testapp import forms
|
||||
from tests.testapp.forms import (
|
||||
NUMBER_CHOICES, HeavySelect2MultipleWidgetForm, TitleModelSelect2Widget
|
||||
)
|
||||
from tests.testapp.models import City, Country, Genre
|
||||
from tests.testapp.models import Artist, City, Country, Genre, Groupie
|
||||
|
||||
try:
|
||||
from django.urls import reverse
|
||||
@@ -104,35 +105,44 @@ class TestSelect2Mixin(object):
|
||||
def test_i18n(self):
|
||||
translation.activate('de')
|
||||
assert tuple(Select2Widget().media._js) == (
|
||||
'//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/select2.min.js',
|
||||
'//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/i18n/de.js',
|
||||
f'//cdnjs.cloudflare.com/ajax/libs/select2/{settings.SELECT2_LIB_VERSION}/js/select2.min.js',
|
||||
f'//cdnjs.cloudflare.com/ajax/libs/select2/{settings.SELECT2_LIB_VERSION}/js/i18n/de.js',
|
||||
'django_select2/django_select2.js'
|
||||
)
|
||||
|
||||
translation.activate('en')
|
||||
assert tuple(Select2Widget().media._js) == (
|
||||
'//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/select2.min.js',
|
||||
'//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/i18n/en.js',
|
||||
f'//cdnjs.cloudflare.com/ajax/libs/select2/{settings.SELECT2_LIB_VERSION}/js/select2.min.js',
|
||||
f'//cdnjs.cloudflare.com/ajax/libs/select2/{settings.SELECT2_LIB_VERSION}/js/i18n/en.js',
|
||||
'django_select2/django_select2.js'
|
||||
)
|
||||
|
||||
translation.activate('00')
|
||||
assert tuple(Select2Widget().media._js) == (
|
||||
'//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/select2.min.js',
|
||||
f'//cdnjs.cloudflare.com/ajax/libs/select2/{settings.SELECT2_LIB_VERSION}/js/select2.min.js',
|
||||
'django_select2/django_select2.js'
|
||||
)
|
||||
|
||||
translation.activate('sr-cyrl')
|
||||
assert tuple(Select2Widget().media._js) == (
|
||||
'//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/select2.min.js',
|
||||
'//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/i18n/sr-Cyrl.js',
|
||||
f'//cdnjs.cloudflare.com/ajax/libs/select2/{settings.SELECT2_LIB_VERSION}/js/select2.min.js',
|
||||
f'//cdnjs.cloudflare.com/ajax/libs/select2/{settings.SELECT2_LIB_VERSION}/js/i18n/sr-Cyrl.js',
|
||||
'django_select2/django_select2.js'
|
||||
)
|
||||
|
||||
translation.activate('zh-cn')
|
||||
pytest.importorskip("django", minversion="2.0.4")
|
||||
|
||||
translation.activate('zh-hans')
|
||||
assert tuple(Select2Widget().media._js) == (
|
||||
'//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/select2.min.js',
|
||||
'//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/i18n/zh-CN.js',
|
||||
f'//cdnjs.cloudflare.com/ajax/libs/select2/{settings.SELECT2_LIB_VERSION}/js/select2.min.js',
|
||||
f'//cdnjs.cloudflare.com/ajax/libs/select2/{settings.SELECT2_LIB_VERSION}/js/i18n/zh-CN.js',
|
||||
'django_select2/django_select2.js'
|
||||
)
|
||||
|
||||
translation.activate('zh-hant')
|
||||
assert tuple(Select2Widget().media._js) == (
|
||||
f'//cdnjs.cloudflare.com/ajax/libs/select2/{settings.SELECT2_LIB_VERSION}/js/select2.min.js',
|
||||
f'//cdnjs.cloudflare.com/ajax/libs/select2/{settings.SELECT2_LIB_VERSION}/js/i18n/zh-TW.js',
|
||||
'django_select2/django_select2.js'
|
||||
)
|
||||
|
||||
@@ -141,8 +151,8 @@ class TestSelect2MixinSettings(object):
|
||||
def test_default_media(self):
|
||||
sut = Select2Widget()
|
||||
result = sut.media.render()
|
||||
assert '//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/select2.min.js' in result
|
||||
assert '//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/css/select2.min.css' in result
|
||||
assert f'//cdnjs.cloudflare.com/ajax/libs/select2/{settings.SELECT2_LIB_VERSION}/js/select2.min.js' in result
|
||||
assert f'//cdnjs.cloudflare.com/ajax/libs/select2/{settings.SELECT2_LIB_VERSION}/css/select2.min.css' in result
|
||||
assert 'django_select2/django_select2.js' in result
|
||||
|
||||
def test_js_setting(self, settings):
|
||||
@@ -152,12 +162,24 @@ class TestSelect2MixinSettings(object):
|
||||
assert 'alternate.js' in result
|
||||
assert 'django_select2/django_select2.js' in result
|
||||
|
||||
def test_empty_js_setting(self, settings):
|
||||
settings.SELECT2_JS = ''
|
||||
sut = Select2Widget()
|
||||
result = sut.media.render()
|
||||
assert 'django_select2/django_select2.js' in result
|
||||
|
||||
def test_css_setting(self, settings):
|
||||
settings.SELECT2_CSS = 'alternate.css'
|
||||
sut = Select2Widget()
|
||||
result = sut.media.render()
|
||||
assert 'alternate.css' in result
|
||||
|
||||
def test_empty_css_setting(self, settings):
|
||||
settings.SELECT2_CSS = ''
|
||||
sut = Select2Widget()
|
||||
result = sut.media.render()
|
||||
assert '.css' not in result
|
||||
|
||||
|
||||
class TestHeavySelect2Mixin(TestSelect2Mixin):
|
||||
url = reverse('heavy_select2_widget')
|
||||
@@ -353,6 +375,12 @@ class TestModelSelect2Mixin(TestHeavySelect2Mixin):
|
||||
widget = ModelSelect2Widget(queryset=Genre.objects.all(), search_fields=['title__icontains'])
|
||||
assert isinstance(widget.get_url(), text_type)
|
||||
|
||||
def test_custom_to_field_name(self):
|
||||
the_best_band_in_the_world = Artist.objects.create(title='Take That')
|
||||
groupie = Groupie.objects.create(obsession=the_best_band_in_the_world)
|
||||
form = forms.GroupieForm(instance=groupie)
|
||||
assert '<option value="Take That" selected>TAKE THAT</option>' in form.as_p()
|
||||
|
||||
|
||||
class TestHeavySelect2TagWidget(TestHeavySelect2Mixin):
|
||||
|
||||
|
||||
@@ -195,3 +195,12 @@ class AddressChainedSelect2WidgetForm(forms.Form):
|
||||
max_results=500,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class GroupieForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = models.Groupie
|
||||
fields = '__all__'
|
||||
widgets = {
|
||||
'obsession': ArtistCustomTitleWidget
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ class Genre(models.Model):
|
||||
|
||||
|
||||
class Artist(models.Model):
|
||||
title = models.CharField(max_length=50)
|
||||
title = models.CharField(max_length=50, unique=True)
|
||||
genres = models.ManyToManyField(Genre)
|
||||
|
||||
class Meta:
|
||||
@@ -56,3 +56,7 @@ class City(models.Model):
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Groupie(models.Model):
|
||||
obsession = models.ForeignKey(Artist, to_field='title', on_delete=models.CASCADE)
|
||||
|
||||
Reference in New Issue
Block a user