parent
dc6e87e7ba
commit
5840622794
|
@ -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
|
||||
|
|
19
.travis.yml
19
.travis.yml
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -9,4 +9,4 @@ The app includes Select2 driven Django Widgets and Form Fields.
|
|||
|
||||
"""
|
||||
|
||||
__version__ = "5.8.10"
|
||||
__version__ = "5.9.0"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -15,3 +15,4 @@ functionalities
|
|||
plugin
|
||||
multi
|
||||
Indices
|
||||
clearable
|
||||
|
|
3
setup.py
3
setup.py
|
@ -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',
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
4
tox.ini
4
tox.ini
|
@ -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' \
|
||||
|
|
Loading…
Reference in New Issue