diff --git a/.travis.yml b/.travis.yml index 9eddffd..518341a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,27 +14,26 @@ addons: - python-enchant - graphviz python: + - "3.5" - "3.6" - "3.7" env: matrix: - - DJANGO=111 - DJANGO=20 - DJANGO=21 + - DJANGO=22 - DJANGO=master - - TOXENV=qa - TOXENV=docs matrix: fast_finish: true allow_failures: - env: DJANGO=master exclude: - - python: "3.7" + - python: "3.6" env: TOXENV=docs - python: "3.7" - env: DJANGO=111 - - python: "3.6" - env: TOXENV=qa + env: TOXENV=docs + install: - pip install --upgrade pip tox - pip install -U codecov diff --git a/django_select2/forms.py b/django_select2/forms.py index c56be82..9aff9bc 100644 --- a/django_select2/forms.py +++ b/django_select2/forms.py @@ -50,6 +50,7 @@ from itertools import chain from pickle import PicklingError # nosec from django import forms +from django.contrib.admin.widgets import SELECT2_TRANSLATIONS from django.core import signing from django.db.models import Q from django.forms.models import ModelChoiceIterator @@ -60,7 +61,7 @@ from .cache import cache from .conf import settings -class Select2Mixin(object): +class Select2Mixin: """ The base mixin of all Select2 widgets. @@ -99,22 +100,12 @@ class Select2Mixin(object): 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: - 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_name = SELECT2_TRANSLATIONS.get(lang) + if i18n_name not in settings.SELECT2_I18N_AVAILABLE_LANGUAGES: + i18n_name = None i18n_file = ('%s/%s.js' % (settings.SELECT2_I18N_PATH, i18n_name),) if i18n_name else () @@ -126,7 +117,7 @@ class Select2Mixin(object): media = property(_get_media) -class Select2TagMixin(object): +class Select2TagMixin: """Mixin to add select2 tag functionality.""" def build_attrs(self, *args, **kwargs): @@ -194,7 +185,7 @@ class Select2TagWidget(Select2TagMixin, Select2Mixin, forms.SelectMultiple): pass -class HeavySelect2Mixin(object): +class HeavySelect2Mixin: """Mixin that adds select2's AJAX options and registers itself on Django's cache.""" dependent_fields = {} @@ -317,7 +308,7 @@ class HeavySelect2TagWidget(HeavySelect2Mixin, Select2TagWidget): # Auto Heavy widgets -class ModelSelect2Mixin(object): +class ModelSelect2Mixin: """Widget mixin that provides attributes and methods for :class:`.AutoResponseView`.""" model = None @@ -352,7 +343,7 @@ class ModelSelect2Mixin(object): self.queryset = kwargs.pop('queryset', self.queryset) self.search_fields = kwargs.pop('search_fields', self.search_fields) self.max_results = kwargs.pop('max_results', self.max_results) - defaults = {'data_view': 'django_select2-json'} + defaults = {'data_view': 'django_select2:auto-json'} defaults.update(kwargs) super(ModelSelect2Mixin, self).__init__(*args, **defaults) @@ -370,13 +361,13 @@ class ModelSelect2Mixin(object): queryset.query, ], 'cls': self.__class__, - 'search_fields': self.search_fields, - 'max_results': self.max_results, - 'url': self.get_url(), - 'dependent_fields': self.dependent_fields, + 'search_fields': tuple(self.search_fields), + 'max_results': int(self.max_results), + 'url': str(self.get_url()), + 'dependent_fields': dict(self.dependent_fields), }) - def filter_queryset(self, term, queryset=None, **dependent_fields): + def filter_queryset(self, request, term, queryset=None, **dependent_fields): """ Return QuerySet filtered by search_fields matching the passed term. diff --git a/django_select2/urls.py b/django_select2/urls.py index 93123e7..345a807 100644 --- a/django_select2/urls.py +++ b/django_select2/urls.py @@ -3,14 +3,18 @@ Django-Select2 URL configuration. Add `django_select` to your ``urlconf`` **if** you use any 'Model' fields:: - url(r'^select2/', include('django_select2.urls')), + from django.urls import path + + + path('select2/', include('django_select2.urls')), """ -from django.conf.urls import url +from django.urls import path from .views import AutoResponseView +app_name = 'django_select2' + urlpatterns = [ - url(r"^fields/auto.json$", - AutoResponseView.as_view(), name="django_select2-json"), + path("fields/auto.json", AutoResponseView.as_view(), name="auto-json"), ] diff --git a/django_select2/views.py b/django_select2/views.py index 72704d6..f65f362 100644 --- a/django_select2/views.py +++ b/django_select2/views.py @@ -54,7 +54,7 @@ class AutoResponseView(BaseListView): for form_field_name, model_field_name in self.widget.dependent_fields.items() if form_field_name in self.request.GET and self.request.GET.get(form_field_name, '') != '' } - return self.widget.filter_queryset(self.term, self.queryset, **kwargs) + return self.widget.filter_queryset(self.request, self.term, self.queryset, **kwargs) def get_paginate_by(self, queryset): """Paginate response by size of widget's `max_results` parameter.""" diff --git a/setup.cfg b/setup.cfg index 93ec250..6ccd638 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,7 +31,7 @@ build-dir = docs/_build warning-is-error = 1 [tool:pytest] -norecursedirs = env docs +norecursedirs = env docs .eggs addopts = --tb=short -rxs DJANGO_SETTINGS_MODULE=tests.testapp.settings diff --git a/tests/conftest.py b/tests/conftest.py index 6330a97..265318b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -22,10 +22,9 @@ def random_name(n): @pytest.yield_fixture(scope='session') def driver(): chrome_options = webdriver.ChromeOptions() - chrome_options.add_argument('headless') - chrome_options.add_argument('window-size=1200x800') + chrome_options.headless = True try: - b = webdriver.Chrome(chrome_options=chrome_options) + b = webdriver.Chrome(options=chrome_options) except WebDriverException as e: pytest.skip(force_text(e)) else: diff --git a/tests/test_forms.py b/tests/test_forms.py index 22ec60d..92ff22f 100644 --- a/tests/test_forms.py +++ b/tests/test_forms.py @@ -1,13 +1,13 @@ -import collections import json import os +from collections.abc import Iterable import pytest from django.core import signing from django.db.models import QuerySet +from django.urls import reverse from django.utils import translation from django.utils.encoding import force_text -from django.utils.six import text_type from selenium.common.exceptions import NoSuchElementException from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions @@ -25,13 +25,8 @@ from tests.testapp.forms import ( ) from tests.testapp.models import Artist, City, Country, Genre, Groupie -try: - from django.urls import reverse -except ImportError: - from django.core.urlresolvers import reverse - -class TestSelect2Mixin(object): +class TestSelect2Mixin: url = reverse('select2_widget') form = forms.AlbumSelect2WidgetForm() multiple_form = forms.AlbumSelect2MultipleWidgetForm() @@ -40,7 +35,7 @@ class TestSelect2Mixin(object): def test_initial_data(self, genres): genre = genres[0] form = self.form.__class__(initial={'primary_genre': genre.pk}) - assert text_type(genre) in form.as_p() + assert str(genre) in form.as_p() def test_initial_form_class(self): widget = self.widget_cls(attrs={'class': 'my-class'}) @@ -147,7 +142,7 @@ class TestSelect2Mixin(object): ) -class TestSelect2MixinSettings(object): +class TestSelect2MixinSettings: def test_default_media(self): sut = Select2Widget() result = sut.media.render() @@ -240,12 +235,12 @@ class TestHeavySelect2Mixin(TestSelect2Mixin): def test_get_url(self): widget = self.widget_cls(data_view='heavy_data_1', attrs={'class': 'my-class'}) - assert isinstance(widget.get_url(), text_type) + assert isinstance(widget.get_url(), str) def test_can_not_pickle(self): widget = self.widget_cls(data_view='heavy_data_1', attrs={'class': 'my-class'}) - class NoPickle(object): + class NoPickle: pass widget.no_pickle = NoPickle() @@ -260,7 +255,7 @@ class TestModelSelect2Mixin(TestHeavySelect2Mixin): def test_initial_data(self, genres): genre = genres[0] form = self.form.__class__(initial={'primary_genre': genre.pk}) - assert text_type(genre) in form.as_p() + assert str(genre) in form.as_p() def test_label_from_instance_initial(self, genres): genre = genres[0] @@ -326,34 +321,34 @@ class TestModelSelect2Mixin(TestHeavySelect2Mixin): widget.get_search_fields() widget.search_fields = ['title__icontains'] - assert isinstance(widget.get_search_fields(), collections.Iterable) - assert all(isinstance(x, text_type) for x in widget.get_search_fields()) + assert isinstance(widget.get_search_fields(), Iterable) + assert all(isinstance(x, str) for x in widget.get_search_fields()) def test_filter_queryset(self, genres): widget = TitleModelSelect2Widget(queryset=Genre.objects.all()) - assert widget.filter_queryset(genres[0].title[:3]).exists() + assert widget.filter_queryset(None, genres[0].title[:3]).exists() widget = TitleModelSelect2Widget(search_fields=['title__icontains'], queryset=Genre.objects.all()) - qs = widget.filter_queryset(" ".join([genres[0].title[:3], genres[0].title[3:]])) + qs = widget.filter_queryset(None, " ".join([genres[0].title[:3], genres[0].title[3:]])) assert qs.exists() def test_model_kwarg(self): widget = ModelSelect2Widget(model=Genre, search_fields=['title__icontains']) genre = Genre.objects.last() - result = widget.filter_queryset(genre.title) + result = widget.filter_queryset(None, genre.title) assert result.exists() def test_queryset_kwarg(self): widget = ModelSelect2Widget(queryset=Genre.objects.all(), search_fields=['title__icontains']) genre = Genre.objects.last() - result = widget.filter_queryset(genre.title) + result = widget.filter_queryset(None, genre.title) assert result.exists() def test_ajax_view_registration(self, client): widget = ModelSelect2Widget(queryset=Genre.objects.all(), search_fields=['title__icontains']) widget.render('name', 'value') - url = reverse('django_select2-json') + url = reverse('django_select2:auto-json') genre = Genre.objects.last() response = client.get(url, data=dict(field_id=signing.dumps(id(widget)), term=genre.title)) assert response.status_code == 200, response.content @@ -366,14 +361,14 @@ class TestModelSelect2Mixin(TestHeavySelect2Mixin): widget.render('name', 'value') cached_widget = cache.get(widget._get_cache_key()) assert cached_widget['max_results'] == widget.max_results - assert cached_widget['search_fields'] == widget.search_fields + assert cached_widget['search_fields'] == tuple(widget.search_fields) qs = widget.get_queryset() assert isinstance(cached_widget['queryset'][0], qs.__class__) - assert text_type(cached_widget['queryset'][1]) == text_type(qs.query) + assert str(cached_widget['queryset'][1]) == str(qs.query) def test_get_url(self): widget = ModelSelect2Widget(queryset=Genre.objects.all(), search_fields=['title__icontains']) - assert isinstance(widget.get_url(), text_type) + assert isinstance(widget.get_url(), str) def test_custom_to_field_name(self): the_best_band_in_the_world = Artist.objects.create(title='Take That') @@ -398,7 +393,7 @@ class TestHeavySelect2TagWidget(TestHeavySelect2Mixin): assert 'data-minimum-input-length="3"' in output -class TestHeavySelect2MultipleWidget(object): +class TestHeavySelect2MultipleWidget: url = reverse('heavy_select2_multiple_widget') form = forms.HeavySelect2MultipleWidgetForm() widget_cls = HeavySelect2MultipleWidget @@ -428,7 +423,7 @@ class TestHeavySelect2MultipleWidget(object): assert result_title == 'One' -class TestAddressChainedSelect2Widget(object): +class TestAddressChainedSelect2Widget: url = reverse('model_chained_select2_widget') form = forms.AddressChainedSelect2WidgetForm() diff --git a/tests/test_views.py b/tests/test_views.py index 3adf47a..a5b96ae 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -16,13 +16,13 @@ except ImportError: from django.core.urlresolvers import reverse -class TestAutoResponseView(object): +class TestAutoResponseView: def test_get(self, client, artists): artist = artists[0] form = AlbumModelSelect2WidgetForm() assert form.as_p() field_id = signing.dumps(id(form.fields['artist'].widget)) - url = reverse('django_select2-json') + url = reverse('django_select2:auto-json') response = client.get(url, {'field_id': field_id, 'term': artist.title}) assert response.status_code == 200 data = json.loads(response.content.decode('utf-8')) @@ -31,25 +31,25 @@ class TestAutoResponseView(object): def test_no_field_id(self, client, artists): artist = artists[0] - url = reverse('django_select2-json') + url = reverse('django_select2:auto-json') response = client.get(url, {'term': artist.title}) assert response.status_code == 404 def test_wrong_field_id(self, client, artists): artist = artists[0] - url = reverse('django_select2-json') + url = reverse('django_select2:auto-json') response = client.get(url, {'field_id': 123, 'term': artist.title}) assert response.status_code == 404 def test_field_id_not_found(self, client, artists): artist = artists[0] field_id = signing.dumps(123456789) - url = reverse('django_select2-json') + url = reverse('django_select2:auto-json') response = client.get(url, {'field_id': field_id, 'term': artist.title}) assert response.status_code == 404 def test_pagination(self, genres, client): - url = reverse('django_select2-json') + url = reverse('django_select2:auto-json') widget = ModelSelect2Widget( max_results=10, model=Genre, @@ -72,7 +72,7 @@ class TestAutoResponseView(object): assert data['more'] is False def test_label_from_instance(self, artists, client): - url = reverse('django_select2-json') + url = reverse('django_select2:auto-json') form = AlbumModelSelect2WidgetForm() form.fields['artist'].widget = ArtistCustomTitleWidget() @@ -96,6 +96,6 @@ class TestAutoResponseView(object): widget_dict = cache.get(cache_key) widget_dict['url'] = 'yet/another/url' cache.set(cache_key, widget_dict) - url = reverse('django_select2-json') + url = reverse('django_select2:auto-json') response = client.get(url, {'field_id': field_id, 'term': artist.title}) assert response.status_code == 404 diff --git a/tests/testapp/forms.py b/tests/testapp/forms.py index cacfb75..4f12fec 100644 --- a/tests/testapp/forms.py +++ b/tests/testapp/forms.py @@ -10,7 +10,7 @@ from tests.testapp import models from tests.testapp.models import Album, City, Country -class TitleSearchFieldMixin(object): +class TitleSearchFieldMixin: search_fields = [ 'title__icontains', 'pk__startswith' diff --git a/tests/testapp/urls.py b/tests/testapp/urls.py index 1baaa58..8ee4aa2 100644 --- a/tests/testapp/urls.py +++ b/tests/testapp/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import include, url +from django.urls import include, path from .forms import ( AddressChainedSelect2WidgetForm, AlbumModelSelect2WidgetForm, @@ -8,28 +8,28 @@ from .forms import ( from .views import TemplateFormView, heavy_data_1, heavy_data_2 urlpatterns = [ - url(r'^select2_widget/$', - TemplateFormView.as_view(form_class=Select2WidgetForm), name='select2_widget'), - url(r'^heavy_select2_widget/$', - TemplateFormView.as_view(form_class=HeavySelect2WidgetForm), name='heavy_select2_widget'), - url(r'^heavy_select2_multiple_widget/$', - TemplateFormView.as_view(form_class=HeavySelect2MultipleWidgetForm, success_url='/'), - name='heavy_select2_multiple_widget'), + path('select2_widget', + TemplateFormView.as_view(form_class=Select2WidgetForm), name='select2_widget'), + path('heavy_select2_widget', + TemplateFormView.as_view(form_class=HeavySelect2WidgetForm), name='heavy_select2_widget'), + path('heavy_select2_multiple_widget', + TemplateFormView.as_view(form_class=HeavySelect2MultipleWidgetForm, success_url='/'), + name='heavy_select2_multiple_widget'), - url(r'^model_select2_widget/$', - TemplateFormView.as_view(form_class=AlbumModelSelect2WidgetForm), - name='model_select2_widget'), + path('model_select2_widget', + TemplateFormView.as_view(form_class=AlbumModelSelect2WidgetForm), + name='model_select2_widget'), - url(r'^model_select2_tag_widget/$', - TemplateFormView.as_view(form_class=ModelSelect2TagWidgetForm), - name='model_select2_tag_widget'), + path('model_select2_tag_widget', + TemplateFormView.as_view(form_class=ModelSelect2TagWidgetForm), + name='model_select2_tag_widget'), - url(r'^model_chained_select2_widget/$', - TemplateFormView.as_view(form_class=AddressChainedSelect2WidgetForm), - name='model_chained_select2_widget'), + path('model_chained_select2_widget', + TemplateFormView.as_view(form_class=AddressChainedSelect2WidgetForm), + name='model_chained_select2_widget'), - url(r'^heavy_data_1/$', heavy_data_1, name='heavy_data_1'), - url(r'^heavy_data_2/$', heavy_data_2, name='heavy_data_2'), + path('heavy_data_1', heavy_data_1, name='heavy_data_1'), + path('heavy_data_2', heavy_data_2, name='heavy_data_2'), - url(r'^select2/', include('django_select2.urls')), + path('select2/', include('django_select2.urls')), ] diff --git a/tox.ini b/tox.ini index fc31457..2e7b726 100644 --- a/tox.ini +++ b/tox.ini @@ -1,14 +1,14 @@ [tox] -envlist = py{36}-dj{111,20,21,master},qa,docs +envlist = py{35,36,37}-dj{20,21,22,master},docs [testenv] setenv= PYTHONPATH = {toxinidir} passenv=CI deps= -rrequirements-dev.txt - dj111: https://github.com/django/django/archive/stable/1.11.x.tar.gz#egg=django dj20: https://github.com/django/django/archive/stable/2.0.x.tar.gz#egg=django 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 commands= coverage run --source=django_select2 -m 'pytest' \ @@ -16,15 +16,8 @@ commands= --ignore=.tox \ {posargs} -[testenv:qa] -changedir={toxinidir} -deps= - -rrequirements-dev.txt -commands= - isort --check-only --recursive --diff {posargs} - [testenv:docs] deps= -rrequirements-dev.txt - https://github.com/django/django/archive/stable/1.11.x.tar.gz#egg=django + https://github.com/django/django/archive/stable/2.2.x.tar.gz#egg=django commands=python setup.py build_sphinx