Skip to content

Commit 5840622

Browse files
committed
Add support for Django 1.11
Drop support for Django 1.9
1 parent dc6e87e commit 5840622

File tree

11 files changed

+119
-40
lines changed

11 files changed

+119
-40
lines changed

.gitignore

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
2-
testapp/tt.py
3-
41
*.pyc
52

63
Django_Select2.egg-info
@@ -19,3 +16,7 @@ docs/_build
1916
env/
2017
venv/
2118
.cache/
19+
.tox/
20+
geckodriver.log
21+
ghostdriver.log
22+
.coverage

.travis.yml

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,27 @@ env:
2525
- DISPLAY=:99.0
2626
- GECKO_DRIVER_VERSION=v0.14.0
2727
matrix:
28-
- TOXENV=qa
29-
- TOXENV=docs
3028
- DJANGO=18
31-
- DJANGO=19
3229
- DJANGO=110
30+
- DJANGO=111
3331
- DJANGO=master
32+
- TOXENV=qa
33+
- TOXENV=docs
3434
matrix:
3535
fast_finish: true
3636
allow_failures:
3737
- env: DJANGO=master
38+
exclude:
39+
- env: DJANGO=master
40+
python: "2.7"
41+
- env: TOXENV=qa
42+
python: "2.7"
43+
- env: TOXENV=qa
44+
python: "3.5"
45+
- env: TOXENV=docs
46+
python: "2.7"
47+
- env: TOXENV=docs
48+
python: "3.5"
3849
install:
3950
- pip install --upgrade pip tox
4051
- pip install -U coveralls

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
Changelog Summary
22
=================
33

4+
### v5.9.0
5+
* Add support for Django 1.11 LTS
6+
* Drop support for Django 1.9
7+
48
### v5.8.10
59
* Fixes tests for Django 1.10+
610
* retain order of choices [299](https://github.com/applegrew/django-select2/pull/299)

django_select2/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@
99
1010
"""
1111

12-
__version__ = "5.8.10"
12+
__version__ = "5.9.0"

django_select2/forms.py

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@
5454

5555
from django import forms
5656
from django.core import signing
57-
from django.core.urlresolvers import reverse
5857
from django.db.models import Q
5958
from django.forms.models import ModelChoiceIterator
6059
from django.utils.encoding import force_text
@@ -63,6 +62,11 @@
6362
from .cache import cache
6463
from .conf import settings
6564

65+
try:
66+
from django.urls import reverse
67+
except ImportError:
68+
from django.core.urlresolvers import reverse
69+
6670

6771
class Select2Mixin(object):
6872
"""
@@ -73,9 +77,9 @@ class Select2Mixin(object):
7377
form media.
7478
"""
7579

76-
def build_attrs(self, extra_attrs=None, **kwargs):
80+
def build_attrs(self, *args, **kwargs):
7781
"""Add select2 data attributes."""
78-
attrs = super(Select2Mixin, self).build_attrs(extra_attrs=extra_attrs, **kwargs)
82+
attrs = super(Select2Mixin, self).build_attrs(*args, **kwargs)
7983
if self.is_required:
8084
attrs.setdefault('data-allow-clear', 'false')
8185
else:
@@ -89,9 +93,15 @@ def build_attrs(self, extra_attrs=None, **kwargs):
8993
attrs['class'] = 'django-select2'
9094
return attrs
9195

96+
def optgroups(self, name, value, attrs=None):
97+
"""Add empty option for clearable selects."""
98+
if not self.is_required and not self.allow_multiple_selected:
99+
self.choices = list(chain([('', '')], self.choices))
100+
return super(Select2Mixin, self).optgroups(name, value, attrs=attrs)
101+
92102
def render_options(self, *args, **kwargs):
93103
"""Render options including an empty one, if the field is not required."""
94-
output = '<option></option>' if not self.is_required and not self.allow_multiple_selected else ''
104+
output = '<option value=""></option>' if not self.is_required and not self.allow_multiple_selected else ''
95105
output += super(Select2Mixin, self).render_options(*args, **kwargs)
96106
return output
97107

@@ -113,12 +123,12 @@ def _get_media(self):
113123
class Select2TagMixin(object):
114124
"""Mixin to add select2 tag functionality."""
115125

116-
def build_attrs(self, extra_attrs=None, **kwargs):
126+
def build_attrs(self, *args, **kwargs):
117127
"""Add select2's tag attributes."""
118128
self.attrs.setdefault('data-minimum-input-length', 1)
119129
self.attrs.setdefault('data-tags', 'true')
120130
self.attrs.setdefault('data-token-separators', '[",", " "]')
121-
return super(Select2TagMixin, self).build_attrs(extra_attrs, **kwargs)
131+
return super(Select2TagMixin, self).build_attrs(*args, **kwargs)
122132

123133

124134
class Select2Widget(Select2Mixin, forms.Select):
@@ -175,7 +185,7 @@ def value_from_datadict(self, data, files, name):
175185
class HeavySelect2Mixin(object):
176186
"""Mixin that adds select2's AJAX options and registers itself on Django's cache."""
177187

178-
def __init__(self, **kwargs):
188+
def __init__(self, attrs=None, choices=(), **kwargs):
179189
"""
180190
Return HeavySelect2Mixin.
181191
@@ -184,22 +194,27 @@ def __init__(self, **kwargs):
184194
data_url (str): URL
185195
186196
"""
197+
self.choices = choices
198+
if attrs is not None:
199+
self.attrs = attrs.copy()
200+
else:
201+
self.attrs = {}
202+
187203
self.data_view = kwargs.pop('data_view', None)
188204
self.data_url = kwargs.pop('data_url', None)
189205
if not (self.data_view or self.data_url):
190206
raise ValueError('You must ether specify "data_view" or "data_url".')
191207
self.userGetValTextFuncName = kwargs.pop('userGetValTextFuncName', 'null')
192-
super(HeavySelect2Mixin, self).__init__(**kwargs)
193208

194209
def get_url(self):
195210
"""Return URL from instance or by reversing :attr:`.data_view`."""
196211
if self.data_url:
197212
return self.data_url
198213
return reverse(self.data_view)
199214

200-
def build_attrs(self, extra_attrs=None, **kwargs):
215+
def build_attrs(self, *args, **kwargs):
201216
"""Set select2's AJAX attributes."""
202-
attrs = super(HeavySelect2Mixin, self).build_attrs(extra_attrs=extra_attrs, **kwargs)
217+
attrs = super(HeavySelect2Mixin, self).build_attrs(*args, **kwargs)
203218

204219
# encrypt instance Id
205220
self.widget_id = signing.dumps(id(self))
@@ -247,7 +262,7 @@ def render_options(self, *args):
247262
choices = chain(self.choices, choices)
248263
else:
249264
choices = self.choices
250-
output = ['<option></option>' if not self.is_required and not self.allow_multiple_selected else '']
265+
output = ['<option value=""></option>' if not self.is_required and not self.allow_multiple_selected else '']
251266
selected_choices = {force_text(v) for v in selected_choices}
252267
choices = [(k, v) for k, v in choices if force_text(k) in selected_choices]
253268
for option_value, option_label in choices:
@@ -401,6 +416,36 @@ def get_search_fields(self):
401416
return self.search_fields
402417
raise NotImplementedError('%s, must implement "search_fields".' % self.__class__.__name__)
403418

419+
def optgroups(self, name, value, attrs=None):
420+
"""Return only selected options and set QuerySet from `ModelChoicesIterator`."""
421+
default = (None, [], 0)
422+
groups = [default]
423+
has_selected = False
424+
selected_choices = {force_text(v) for v in value}
425+
if not self.is_required and not self.allow_multiple_selected:
426+
default[1].append(self.create_option(name, '', '', False, 0))
427+
if not isinstance(self.choices, ModelChoiceIterator):
428+
return super(ModelSelect2Mixin, self).optgroups(name, value, attrs=attrs)
429+
selected_choices = {
430+
c for c in selected_choices
431+
if c not in self.choices.field.empty_values
432+
}
433+
choices = (
434+
(obj.pk, self.label_from_instance(obj))
435+
for obj in self.choices.queryset.filter(pk__in=selected_choices)
436+
)
437+
for option_value, option_label in choices:
438+
selected = (
439+
force_text(option_value) in value and
440+
(has_selected is False or self.allow_multiple_selected)
441+
)
442+
if selected is True and has_selected is False:
443+
has_selected = True
444+
index = len(default[1])
445+
subgroup = default[1]
446+
subgroup.append(self.create_option(name, option_value, option_label, selected_choices, index))
447+
return groups
448+
404449
def render_options(self, *args):
405450
"""Render only selected options and set QuerySet from :class:`ModelChoiceIterator`."""
406451
try:
@@ -411,7 +456,7 @@ def render_options(self, *args):
411456
else:
412457
choices = self.choices
413458
selected_choices = {force_text(v) for v in selected_choices}
414-
output = ['<option></option>' if not self.is_required and not self.allow_multiple_selected else '']
459+
output = ['<option value=""></option>' if not self.is_required and not self.allow_multiple_selected else '']
415460
if isinstance(self.choices, ModelChoiceIterator):
416461
if self.queryset is None:
417462
self.queryset = self.choices.queryset

docs/spelling_wordlist.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ functionalities
1515
plugin
1616
multi
1717
Indices
18+
clearable

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ def read(file_name):
4444
"Programming Language :: Python :: 3",
4545
"Framework :: Django",
4646
"Framework :: Django :: 1.8",
47-
"Framework :: Django :: 1.9",
47+
"Framework :: Django :: 1.10",
48+
"Framework :: Django :: 1.11",
4849
],
4950
install_requires=[
5051
'django-appconf>=0.6.0',

tests/test_forms.py

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
import pytest
88
from django.core import signing
9-
from django.core.urlresolvers import reverse
109
from django.db.models import QuerySet
1110
from django.utils.encoding import force_text
1211
from django.utils.six import text_type
@@ -26,6 +25,11 @@
2625
)
2726
from tests.testapp.models import Genre
2827

28+
try:
29+
from django.urls import reverse
30+
except ImportError:
31+
from django.core.urlresolvers import reverse
32+
2933

3034
class TestSelect2Mixin(object):
3135
url = reverse('select2_widget')
@@ -48,15 +52,15 @@ def test_allow_clear(self, db):
4852
assert required_field.required is True
4953
assert 'data-allow-clear="true"' not in required_field.widget.render('artist', None)
5054
assert 'data-allow-clear="false"' in required_field.widget.render('artist', None)
51-
assert '<option></option>' not in required_field.widget.render('artist', None)
55+
assert '<option value=""></option>' not in required_field.widget.render('artist', None)
5256

5357
not_required_field = self.form.fields['primary_genre']
5458
assert not_required_field.required is False
5559
assert 'data-allow-clear="true"' in not_required_field.widget.render('primary_genre', None)
5660
assert 'data-allow-clear="false"' not in not_required_field.widget.render('primary_genre',
5761
None)
5862
assert 'data-placeholder' in not_required_field.widget.render('primary_genre', None)
59-
assert '<option></option>' in not_required_field.widget.render('primary_genre', None)
63+
assert '<option value=""></option>' in not_required_field.widget.render('primary_genre', None)
6064

6165
def test_no_js_error(self, db, live_server, driver):
6266
driver.get(live_server + self.url)
@@ -91,12 +95,12 @@ def test_empty_option(self, db):
9195
# https://select2.github.io/options.html#allowClear
9296
single_select = self.form.fields['primary_genre']
9397
assert single_select.required is False
94-
assert '<option></option>' in single_select.widget.render('primary_genre', None)
98+
assert '<option value=""></option>' in single_select.widget.render('primary_genre', None)
9599

96100
multiple_select = self.multiple_form.fields['featured_artists']
97101
assert multiple_select.required is False
98102
assert multiple_select.widget.allow_multiple_selected
99-
assert '<option></option>' not in multiple_select.widget.render('featured_artists', None)
103+
assert '<option value=""></option>' not in multiple_select.widget.render('featured_artists', None)
100104

101105

102106
class TestSelect2MixinSettings(object):
@@ -139,18 +143,22 @@ def test_selected_option(self, db):
139143
not_required_field = self.form.fields['primary_genre']
140144
assert not_required_field.required is False
141145
assert '<option value="1" selected="selected">One</option>' in \
142-
not_required_field.widget.render('primary_genre', 1), \
146+
not_required_field.widget.render('primary_genre', 1) or \
147+
'<option value="1" selected>One</option>' in \
148+
not_required_field.widget.render('primary_genre', 1), \
143149
not_required_field.widget.render('primary_genre', 1)
144150

145151
def test_many_selected_option(self, db, genres):
146152
field = HeavySelect2MultipleWidgetForm().fields['genres']
147153
field.widget.choices = NUMBER_CHOICES
148154
widget_output = field.widget.render('genres', [1, 2])
149155
selected_option = '<option value="{pk}" selected="selected">{value}</option>'.format(pk=1, value='One')
156+
selected_option_a = '<option value="{pk}" selected>{value}</option>'.format(pk=1, value='One')
150157
selected_option2 = '<option value="{pk}" selected="selected">{value}</option>'.format(pk=2, value='Two')
158+
selected_option2a = '<option value="{pk}" selected>{value}</option>'.format(pk=2, value='Two')
151159

152-
assert selected_option in widget_output, widget_output
153-
assert selected_option2 in widget_output
160+
assert selected_option in widget_output or selected_option_a in widget_output, widget_output
161+
assert selected_option2 in widget_output or selected_option2a in widget_output
154162

155163
def test_multiple_widgets(self, db, live_server, driver):
156164
driver.get(live_server + self.url)
@@ -160,11 +168,11 @@ def test_multiple_widgets(self, db, live_server, driver):
160168
elem1, elem2 = driver.find_elements_by_css_selector('.select2-selection')
161169
elem1.click()
162170

163-
result1 = WebDriverWait(driver, 10).until(
171+
result1 = WebDriverWait(driver, 60).until(
164172
expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '.select2-results li:first-child'))
165173
).text
166174
elem2.click()
167-
result2 = WebDriverWait(driver, 10).until(
175+
result2 = WebDriverWait(driver, 60).until(
168176
expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '.select2-results li:first-child'))
169177
).text
170178

@@ -204,7 +212,7 @@ def test_label_from_instance_initial(self, genres):
204212
genre.save()
205213

206214
form = self.form.__class__(initial={'primary_genre': genre.pk})
207-
assert genre.title not in form.as_p()
215+
assert genre.title not in form.as_p(), form.as_p()
208216
assert genre.title.upper() in form.as_p()
209217

210218
@pytest.fixture(autouse=True)
@@ -220,10 +228,12 @@ def test_selected_option(self, db, genres):
220228
'primary_genre', genre.pk)
221229
selected_option = '<option value="{pk}" selected="selected">{value}</option>'.format(
222230
pk=genre.pk, value=force_text(genre))
231+
selected_option_a = '<option value="{pk}" selected>{value}</option>'.format(
232+
pk=genre.pk, value=force_text(genre))
223233
unselected_option = '<option value="{pk}">{value}</option>'.format(
224234
pk=genre2.pk, value=force_text(genre2))
225235

226-
assert selected_option in widget_output, widget_output
236+
assert selected_option in widget_output or selected_option_a in widget_output, widget_output
227237
assert unselected_option not in widget_output
228238

229239
def test_selected_option_label_from_instance(self, db, genres):
@@ -234,14 +244,15 @@ def test_selected_option_label_from_instance(self, db, genres):
234244
field = self.form.fields['primary_genre']
235245
widget_output = field.widget.render('primary_genre', genre.pk)
236246

237-
def get_selected_option(genre):
247+
def get_selected_options(genre):
238248
return '<option value="{pk}" selected="selected">{value}</option>'.format(
249+
pk=genre.pk, value=force_text(genre)), '<option value="{pk}" selected>{value}</option>'.format(
239250
pk=genre.pk, value=force_text(genre))
240251

241-
assert get_selected_option(genre) not in widget_output
252+
assert all(o not in widget_output for o in get_selected_options(genre))
242253
genre.title = genre.title.upper()
243254

244-
assert get_selected_option(genre) in widget_output
255+
assert any(o in widget_output for o in get_selected_options(genre))
245256

246257
def test_get_queryset(self):
247258
widget = ModelSelect2Widget()

tests/test_views.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import json
55

66
from django.core import signing
7-
from django.core.urlresolvers import reverse
87
from django.utils.encoding import smart_text
98

109
from django_select2.cache import cache
@@ -14,6 +13,11 @@
1413
)
1514
from tests.testapp.models import Genre
1615

16+
try:
17+
from django.urls import reverse
18+
except ImportError:
19+
from django.core.urlresolvers import reverse
20+
1721

1822
class TestAutoResponseView(object):
1923
def test_get(self, client, artists):

0 commit comments

Comments
 (0)