Add support for Django 1.11

Drop support for Django 1.9
This commit is contained in:
Johannes Hoppe 2017-04-14 11:56:52 +02:00
parent dc6e87e7ba
commit 5840622794
11 changed files with 120 additions and 41 deletions

7
.gitignore vendored
View File

@ -1,6 +1,3 @@
testapp/tt.py
*.pyc
Django_Select2.egg-info
@ -19,3 +16,7 @@ docs/_build
env/
venv/
.cache/
.tox/
geckodriver.log
ghostdriver.log
.coverage

View File

@ -25,16 +25,27 @@ env:
- DISPLAY=:99.0
- GECKO_DRIVER_VERSION=v0.14.0
matrix:
- DJANGO=18
- DJANGO=110
- DJANGO=111
- DJANGO=master
- TOXENV=qa
- TOXENV=docs
- DJANGO=18
- DJANGO=19
- DJANGO=110
- DJANGO=master
matrix:
fast_finish: true
allow_failures:
- env: DJANGO=master
exclude:
- env: DJANGO=master
python: "2.7"
- env: TOXENV=qa
python: "2.7"
- env: TOXENV=qa
python: "3.5"
- env: TOXENV=docs
python: "2.7"
- env: TOXENV=docs
python: "3.5"
install:
- pip install --upgrade pip tox
- pip install -U coveralls

View File

@ -1,6 +1,10 @@
Changelog Summary
=================
### v5.9.0
* Add support for Django 1.11 LTS
* Drop support for Django 1.9
### v5.8.10
* Fixes tests for Django 1.10+
* retain order of choices [299](https://github.com/applegrew/django-select2/pull/299)

View File

@ -9,4 +9,4 @@ The app includes Select2 driven Django Widgets and Form Fields.
"""
__version__ = "5.8.10"
__version__ = "5.9.0"

View File

@ -54,7 +54,6 @@ from pickle import PicklingError
from django import forms
from django.core import signing
from django.core.urlresolvers import reverse
from django.db.models import Q
from django.forms.models import ModelChoiceIterator
from django.utils.encoding import force_text
@ -63,6 +62,11 @@ from django.utils.six.moves.cPickle import PicklingError as cPicklingError
from .cache import cache
from .conf import settings
try:
from django.urls import reverse
except ImportError:
from django.core.urlresolvers import reverse
class Select2Mixin(object):
"""
@ -73,9 +77,9 @@ class Select2Mixin(object):
form media.
"""
def build_attrs(self, extra_attrs=None, **kwargs):
def build_attrs(self, *args, **kwargs):
"""Add select2 data attributes."""
attrs = super(Select2Mixin, self).build_attrs(extra_attrs=extra_attrs, **kwargs)
attrs = super(Select2Mixin, self).build_attrs(*args, **kwargs)
if self.is_required:
attrs.setdefault('data-allow-clear', 'false')
else:
@ -89,9 +93,15 @@ class Select2Mixin(object):
attrs['class'] = 'django-select2'
return attrs
def optgroups(self, name, value, attrs=None):
"""Add empty option for clearable selects."""
if not self.is_required and not self.allow_multiple_selected:
self.choices = list(chain([('', '')], self.choices))
return super(Select2Mixin, self).optgroups(name, value, attrs=attrs)
def render_options(self, *args, **kwargs):
"""Render options including an empty one, if the field is not required."""
output = '<option></option>' if not self.is_required and not self.allow_multiple_selected else ''
output = '<option value=""></option>' if not self.is_required and not self.allow_multiple_selected else ''
output += super(Select2Mixin, self).render_options(*args, **kwargs)
return output
@ -113,12 +123,12 @@ class Select2Mixin(object):
class Select2TagMixin(object):
"""Mixin to add select2 tag functionality."""
def build_attrs(self, extra_attrs=None, **kwargs):
def build_attrs(self, *args, **kwargs):
"""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(extra_attrs, **kwargs)
return super(Select2TagMixin, self).build_attrs(*args, **kwargs)
class Select2Widget(Select2Mixin, forms.Select):
@ -175,7 +185,7 @@ class Select2TagWidget(Select2TagMixin, Select2Mixin, forms.SelectMultiple):
class HeavySelect2Mixin(object):
"""Mixin that adds select2's AJAX options and registers itself on Django's cache."""
def __init__(self, **kwargs):
def __init__(self, attrs=None, choices=(), **kwargs):
"""
Return HeavySelect2Mixin.
@ -184,12 +194,17 @@ class HeavySelect2Mixin(object):
data_url (str): URL
"""
self.choices = choices
if attrs is not None:
self.attrs = attrs.copy()
else:
self.attrs = {}
self.data_view = kwargs.pop('data_view', None)
self.data_url = kwargs.pop('data_url', None)
if not (self.data_view or self.data_url):
raise ValueError('You must ether specify "data_view" or "data_url".')
self.userGetValTextFuncName = kwargs.pop('userGetValTextFuncName', 'null')
super(HeavySelect2Mixin, self).__init__(**kwargs)
def get_url(self):
"""Return URL from instance or by reversing :attr:`.data_view`."""
@ -197,9 +212,9 @@ class HeavySelect2Mixin(object):
return self.data_url
return reverse(self.data_view)
def build_attrs(self, extra_attrs=None, **kwargs):
def build_attrs(self, *args, **kwargs):
"""Set select2's AJAX attributes."""
attrs = super(HeavySelect2Mixin, self).build_attrs(extra_attrs=extra_attrs, **kwargs)
attrs = super(HeavySelect2Mixin, self).build_attrs(*args, **kwargs)
# encrypt instance Id
self.widget_id = signing.dumps(id(self))
@ -247,7 +262,7 @@ class HeavySelect2Mixin(object):
choices = chain(self.choices, choices)
else:
choices = self.choices
output = ['<option></option>' if not self.is_required and not self.allow_multiple_selected else '']
output = ['<option value=""></option>' if not self.is_required and not self.allow_multiple_selected else '']
selected_choices = {force_text(v) for v in selected_choices}
choices = [(k, v) for k, v in choices if force_text(k) in selected_choices]
for option_value, option_label in choices:
@ -401,6 +416,36 @@ class ModelSelect2Mixin(object):
return self.search_fields
raise NotImplementedError('%s, must implement "search_fields".' % self.__class__.__name__)
def optgroups(self, name, value, attrs=None):
"""Return only selected options and set QuerySet from `ModelChoicesIterator`."""
default = (None, [], 0)
groups = [default]
has_selected = False
selected_choices = {force_text(v) for v in value}
if not self.is_required and not self.allow_multiple_selected:
default[1].append(self.create_option(name, '', '', False, 0))
if not isinstance(self.choices, ModelChoiceIterator):
return super(ModelSelect2Mixin, self).optgroups(name, value, attrs=attrs)
selected_choices = {
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:
selected = (
force_text(option_value) in value and
(has_selected is False or self.allow_multiple_selected)
)
if selected is True and has_selected is False:
has_selected = True
index = len(default[1])
subgroup = default[1]
subgroup.append(self.create_option(name, option_value, option_label, selected_choices, index))
return groups
def render_options(self, *args):
"""Render only selected options and set QuerySet from :class:`ModelChoiceIterator`."""
try:
@ -411,7 +456,7 @@ class ModelSelect2Mixin(object):
else:
choices = self.choices
selected_choices = {force_text(v) for v in selected_choices}
output = ['<option></option>' if not self.is_required and not self.allow_multiple_selected else '']
output = ['<option value=""></option>' if not self.is_required and not self.allow_multiple_selected else '']
if isinstance(self.choices, ModelChoiceIterator):
if self.queryset is None:
self.queryset = self.choices.queryset

View File

@ -15,3 +15,4 @@ functionalities
plugin
multi
Indices
clearable

View File

@ -44,7 +44,8 @@ setup(
"Programming Language :: Python :: 3",
"Framework :: Django",
"Framework :: Django :: 1.8",
"Framework :: Django :: 1.9",
"Framework :: Django :: 1.10",
"Framework :: Django :: 1.11",
],
install_requires=[
'django-appconf>=0.6.0',

View File

@ -6,7 +6,6 @@ import json
import pytest
from django.core import signing
from django.core.urlresolvers import reverse
from django.db.models import QuerySet
from django.utils.encoding import force_text
from django.utils.six import text_type
@ -26,6 +25,11 @@ from tests.testapp.forms import (
)
from tests.testapp.models import Genre
try:
from django.urls import reverse
except ImportError:
from django.core.urlresolvers import reverse
class TestSelect2Mixin(object):
url = reverse('select2_widget')
@ -48,7 +52,7 @@ class TestSelect2Mixin(object):
assert required_field.required is True
assert 'data-allow-clear="true"' not in required_field.widget.render('artist', None)
assert 'data-allow-clear="false"' in required_field.widget.render('artist', None)
assert '<option></option>' not in required_field.widget.render('artist', None)
assert '<option value=""></option>' not in required_field.widget.render('artist', None)
not_required_field = self.form.fields['primary_genre']
assert not_required_field.required is False
@ -56,7 +60,7 @@ class TestSelect2Mixin(object):
assert 'data-allow-clear="false"' not in not_required_field.widget.render('primary_genre',
None)
assert 'data-placeholder' in not_required_field.widget.render('primary_genre', None)
assert '<option></option>' in not_required_field.widget.render('primary_genre', None)
assert '<option value=""></option>' in not_required_field.widget.render('primary_genre', None)
def test_no_js_error(self, db, live_server, driver):
driver.get(live_server + self.url)
@ -91,12 +95,12 @@ class TestSelect2Mixin(object):
# https://select2.github.io/options.html#allowClear
single_select = self.form.fields['primary_genre']
assert single_select.required is False
assert '<option></option>' in single_select.widget.render('primary_genre', None)
assert '<option value=""></option>' in single_select.widget.render('primary_genre', None)
multiple_select = self.multiple_form.fields['featured_artists']
assert multiple_select.required is False
assert multiple_select.widget.allow_multiple_selected
assert '<option></option>' not in multiple_select.widget.render('featured_artists', None)
assert '<option value=""></option>' not in multiple_select.widget.render('featured_artists', None)
class TestSelect2MixinSettings(object):
@ -139,7 +143,9 @@ class TestHeavySelect2Mixin(TestSelect2Mixin):
not_required_field = self.form.fields['primary_genre']
assert not_required_field.required is False
assert '<option value="1" selected="selected">One</option>' in \
not_required_field.widget.render('primary_genre', 1), \
not_required_field.widget.render('primary_genre', 1) or \
'<option value="1" selected>One</option>' in \
not_required_field.widget.render('primary_genre', 1), \
not_required_field.widget.render('primary_genre', 1)
def test_many_selected_option(self, db, genres):
@ -147,10 +153,12 @@ class TestHeavySelect2Mixin(TestSelect2Mixin):
field.widget.choices = NUMBER_CHOICES
widget_output = field.widget.render('genres', [1, 2])
selected_option = '<option value="{pk}" selected="selected">{value}</option>'.format(pk=1, value='One')
selected_option_a = '<option value="{pk}" selected>{value}</option>'.format(pk=1, value='One')
selected_option2 = '<option value="{pk}" selected="selected">{value}</option>'.format(pk=2, value='Two')
selected_option2a = '<option value="{pk}" selected>{value}</option>'.format(pk=2, value='Two')
assert selected_option in widget_output, widget_output
assert selected_option2 in widget_output
assert selected_option in widget_output or selected_option_a in widget_output, widget_output
assert selected_option2 in widget_output or selected_option2a in widget_output
def test_multiple_widgets(self, db, live_server, driver):
driver.get(live_server + self.url)
@ -160,11 +168,11 @@ class TestHeavySelect2Mixin(TestSelect2Mixin):
elem1, elem2 = driver.find_elements_by_css_selector('.select2-selection')
elem1.click()
result1 = WebDriverWait(driver, 10).until(
result1 = WebDriverWait(driver, 60).until(
expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '.select2-results li:first-child'))
).text
elem2.click()
result2 = WebDriverWait(driver, 10).until(
result2 = WebDriverWait(driver, 60).until(
expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '.select2-results li:first-child'))
).text
@ -204,7 +212,7 @@ class TestModelSelect2Mixin(TestHeavySelect2Mixin):
genre.save()
form = self.form.__class__(initial={'primary_genre': genre.pk})
assert genre.title not in form.as_p()
assert genre.title not in form.as_p(), form.as_p()
assert genre.title.upper() in form.as_p()
@pytest.fixture(autouse=True)
@ -220,10 +228,12 @@ class TestModelSelect2Mixin(TestHeavySelect2Mixin):
'primary_genre', genre.pk)
selected_option = '<option value="{pk}" selected="selected">{value}</option>'.format(
pk=genre.pk, value=force_text(genre))
selected_option_a = '<option value="{pk}" selected>{value}</option>'.format(
pk=genre.pk, value=force_text(genre))
unselected_option = '<option value="{pk}">{value}</option>'.format(
pk=genre2.pk, value=force_text(genre2))
assert selected_option in widget_output, widget_output
assert selected_option in widget_output or selected_option_a in widget_output, widget_output
assert unselected_option not in widget_output
def test_selected_option_label_from_instance(self, db, genres):
@ -234,14 +244,15 @@ class TestModelSelect2Mixin(TestHeavySelect2Mixin):
field = self.form.fields['primary_genre']
widget_output = field.widget.render('primary_genre', genre.pk)
def get_selected_option(genre):
def get_selected_options(genre):
return '<option value="{pk}" selected="selected">{value}</option>'.format(
pk=genre.pk, value=force_text(genre)), '<option value="{pk}" selected>{value}</option>'.format(
pk=genre.pk, value=force_text(genre))
assert get_selected_option(genre) not in widget_output
assert all(o not in widget_output for o in get_selected_options(genre))
genre.title = genre.title.upper()
assert get_selected_option(genre) in widget_output
assert any(o in widget_output for o in get_selected_options(genre))
def test_get_queryset(self):
widget = ModelSelect2Widget()

View File

@ -4,7 +4,6 @@ from __future__ import absolute_import, unicode_literals
import json
from django.core import signing
from django.core.urlresolvers import reverse
from django.utils.encoding import smart_text
from django_select2.cache import cache
@ -14,6 +13,11 @@ from tests.testapp.forms import (
)
from tests.testapp.models import Genre
try:
from django.urls import reverse
except ImportError:
from django.core.urlresolvers import reverse
class TestAutoResponseView(object):
def test_get(self, client, artists):

View File

@ -25,9 +25,10 @@ class Artist(models.Model):
@python_2_unicode_compatible
class Album(models.Model):
title = models.CharField(max_length=255)
artist = models.ForeignKey(Artist)
artist = models.ForeignKey(Artist, on_delete=models.CASCADE)
featured_artists = models.ManyToManyField(Artist, blank=True, related_name='featured_album_set')
primary_genre = models.ForeignKey(Genre, blank=True, null=True, related_name='primary_album_set')
primary_genre = models.ForeignKey(Genre, on_delete=models.CASCADE, blank=True, null=True,
related_name='primary_album_set')
genres = models.ManyToManyField(Genre)
def __str__(self):

View File

@ -1,5 +1,5 @@
[tox]
envlist = py{27,35,36}-dj{18,19,110,master},qa,docs
envlist = py{27,35,36}-dj{18,110,111,master},qa,docs
[testenv]
setenv=
DISPLAY=:99.0
@ -7,8 +7,8 @@ setenv=
deps=
-rrequirements-dev.txt
dj18: https://github.com/django/django/archive/stable/1.8.x.tar.gz#egg=django
dj19: https://github.com/django/django/archive/stable/1.9.x.tar.gz#egg=django
dj110: https://github.com/django/django/archive/stable/1.10.x.tar.gz#egg=django
dj111: https://github.com/django/django/archive/stable/1.11.x.tar.gz#egg=django
djmaster: https://github.com/django/django/archive/master.tar.gz#egg=django
commands=
coverage run --source=django_select2 -m 'pytest' \