diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index aed54fa..1c1260e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,13 +1,42 @@ name: Test -on: push +on: [push, pull_request] jobs: build: - runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - python-version: [3.7, 3.8, 3.9] + include: + - django-version: "2.2" + python-version: "3.8" + - django-version: "2.2" + python-version: "3.8" + grappelli: "1" + - django-version: "3.2" + python-version: "3.9" + - django-version: "3.2" + python-version: "3.10" + grappelli: "1" + - django-version: "4.2" + python-version: "3.10" + - django-version: "4.2" + python-version: "3.11" + grappelli: "1" + - django-version: "5.1" + python-version: "3.12" + - django-version: "5.2" + python-version: "3.12" + - django-version: "5.2" + python-version: "3.13" + grappelli: "1" + + runs-on: ubuntu-latest + name: Django ${{ matrix.django-version }} (Python ${{ matrix.python-version }})${{ matrix.grappelli == '1' && ' + grappelli' || '' }}${{ matrix.s3 == '1' && ' + s3' || '' }} + env: + PYTHON: ${{ matrix.python-version }} + DJANGO: ${{ matrix.django-version }} + GRAPPELLI: ${{ matrix.grappelli || '0' }} steps: - uses: actions/checkout@v3 @@ -17,16 +46,59 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: Setup chromedriver - uses: nanasess/setup-chromedriver@v1.0.5 - - - name: Install dependencies + - name: Install tox run: | - python -m pip install --upgrade pip - pip install tox tox-gh-actions + python3 -m pip install tox tox-gh-actions - - name: Test with tox + - name: Run tests run: | tox -- -v --selenosis-driver=chrome-headless || \ tox -- -v --selenosis-driver=chrome-headless || \ tox -- -v --selenosis-driver=chrome-headless + + - name: Upload junit xml + if: always() + uses: actions/upload-artifact@v4 + with: + name: junit-reports-django-${{ matrix.django-version }}-python${{ matrix.python-version }}${{ matrix.grappelli == '1' && '-grappelli' || '' }}${{ matrix.s3 == '1' && '-s3' || '' }}-${{ github.sha }} + path: reports/*.xml + overwrite: true + + - name: Combine coverage + run: tox -e coverage-report + + - name: Upload coverage + uses: codecov/codecov-action@v5 + with: + name: ${{ github.workflow }} + files: .tox/coverage/coverage.xml + env_vars: "DJANGO,GRAPPELLI,PYTHON" + token: ${{ secrets.CODECOV_TOKEN }} + + report: + if: always() + needs: build + runs-on: ubuntu-latest + name: "Report Test Results" + steps: + - uses: actions/download-artifact@v4 + with: + merge-multiple: true + + - name: Publish Unit Test Results + if: always() + uses: mikepenz/action-junit-report@1a91e26932fb7ba410a31fab1f09266a96d29971 + with: + report_paths: ./*.xml + require_tests: true + fail_on_failure: true + check_name: Test Report + github_token: ${{ secrets.GITHUB_TOKEN }} + + success: + needs: [report] + runs-on: ubuntu-latest + name: Test Successful + steps: + - name: Success + run: echo Test Successful diff --git a/.gitignore b/.gitignore index a3ff1f7..33d0bbd 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ ghostdriver.log .tox venv/ .python-version +reports +coverage.xml +.coverage diff --git a/README.rst b/README.rst index e9866ae..6913b38 100644 --- a/README.rst +++ b/README.rst @@ -14,10 +14,8 @@ developers at `The Atlantic `_. Support ======= -Being that Django added select2 support in 2.0, we will support up to that version -for compatibility purposes. - -* ~=v3.0: Python >=3.7,<3.9 | Django 2.2,3.1,3.2 (current release) +django-select2-forms should work with all currently supported releases as well +as the two most recent unsupported LTS versions. Local Development & Testing =========================== diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b7e73d3 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,60 @@ +[build-system] +build-backend = "setuptools.build_meta" +requires = ["setuptools>=75.3.2"] + +[project] +name = "django-select2-forms" +dynamic = ["version"] +description = "Django form fields using the Select2 jQuery plugin" +readme = "README.rst" +license = { text = "BSD-2-Clause" } +requires-python = ">=3" +authors = [ + { name = "The Atlantic", email = "programmers@theatlantic.com" }, +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Framework :: Django", + "Framework :: Django :: 2.2", + "Framework :: Django :: 3.2", + "Framework :: Django :: 4.2", + "Framework :: Django :: 5.0", + "Framework :: Django :: 5.1", + "Framework :: Django :: 5.2", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] +dependencies = [ + "django-sortedm2m", +] + +[project.urls] +Homepage = "https://github.com/theatlantic/django-select2-forms" + +[tool.setuptools] +zip-safe = false +platforms = ["any"] +include-package-data = true + +[tool.setuptools.packages.find] +include = ["select2*"] +namespaces = false + +[tool.setuptools.dynamic] +version = { attr = "select2.__version__" } +readme = { file = ["README.rst"] } + +[tool.pytest.ini_options] +python_files = "tests.py test_*.py *_test.py" +DJANGO_SETTINGS_MODULE = "tests.settings" +addopts = "--tb=short --create-db --cov=select2 --cov-branch --cov-report=xml" +django_find_project = false +testpaths = "tests" +pythonpath = "." diff --git a/select2/__init__.py b/select2/__init__.py index c9667ba..f5f41e5 100644 --- a/select2/__init__.py +++ b/select2/__init__.py @@ -1,6 +1 @@ -import pkg_resources - -try: - __version__ = pkg_resources.get_distribution('django-select2-forms').version -except pkg_resources.DistributionNotFound: - __version__ = None +__version__ = "3.1.0" diff --git a/select2/fields.py b/select2/fields.py index 144976a..d345d63 100644 --- a/select2/fields.py +++ b/select2/fields.py @@ -79,10 +79,7 @@ def choices(self): @choices.setter def choices(self, value): - self._set_choices(value) - - def _set_choices(self, value): - self._choices = value + super(self.__class__, self.__class__).choices.__set__(self, value) class ChoiceField(Select2FieldMixin, forms.ChoiceField): @@ -133,12 +130,15 @@ def __init__(self, search_field=None, case_sensitive=False, *args, **kwargs): self.choice_iterator_cls = kwargs.pop('choice_iterator_cls', self.choice_iterator_cls) super(Select2ModelFieldMixin, self).__init__(*args, **kwargs) - def _get_choices(self): - if hasattr(self, '_choices'): - return self._choices - return self.choice_iterator_cls(self) + @property + def choices(self): + if not hasattr(self, '_choices'): + return self.choice_iterator_cls(self) + return self._choices - choices = property(_get_choices, forms.ChoiceField._set_choices) + @choices.setter + def choices(self, value): + super(self.__class__, self.__class__).choices.__set__(self, value) class ModelChoiceField(Select2ModelFieldMixin, forms.ModelChoiceField): diff --git a/select2/static/select2/js/select2.jquery_ready.js b/select2/static/select2/js/select2.jquery_ready.js index b87ae0d..823b919 100644 --- a/select2/static/select2/js/select2.jquery_ready.js +++ b/select2/static/select2/js/select2.jquery_ready.js @@ -65,6 +65,9 @@ var DjangoSelect2 = window.DjangoSelect2 || {}; DjangoSelect2.init(this); }); $(document).on('formset:added', function(event, $form) { + if (typeof $form === "undefined") { + $form = $(event.target); + } $form.find('.django-select2:not([name*="__prefix__"])').each(function() { DjangoSelect2.init(this); }); diff --git a/setup.py b/setup.py index e1639c9..6068493 100755 --- a/setup.py +++ b/setup.py @@ -1,41 +1,3 @@ -#!/usr/bin/env python -import codecs -import os -from setuptools import setup, find_packages +from setuptools import setup -readme_rst = os.path.join(os.path.dirname(__file__), 'README.rst') - -setup( - name='django-select2-forms', - version='3.0.0', - description='Django form fields using the Select2 jQuery plugin', - long_description=codecs.open(readme_rst, encoding='utf-8').read(), - author='Frankie Dintino', - author_email='fdintino@theatlantic.com', - url='https://github.com/theatlantic/django-select2-forms', - packages=find_packages(), - license='BSD', - platforms='any', - install_requires=[ - 'django-sortedm2m', - ], - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Environment :: Web Environment', - 'Intended Audience :: Developers', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Framework :: Django', - 'Framework :: Django :: 2.2', - 'Framework :: Django :: 3.1', - 'Framework :: Django :: 3.2', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - - ], - include_package_data=True, - zip_safe=False -) +setup() diff --git a/tests/admin.py b/tests/admin.py index 755364d..e0e087f 100644 --- a/tests/admin.py +++ b/tests/admin.py @@ -1,3 +1,4 @@ +from django.conf import settings from django.contrib import admin from .models import Publisher, Author, Book, Library @@ -7,6 +8,17 @@ class BookInline(admin.StackedInline): model = Book extra = 0 + @property + def classes(self): + if "grappelli" in settings.INSTALLED_APPS: + return ("grp-collapse grp-open",) + else: + return None + + @property + def inline_classes(self): + return self.classes + @admin.register(Library) class LibraryAdmin(admin.ModelAdmin): diff --git a/tests/settings.py b/tests/settings.py index 20e2288..ad67fbf 100755 --- a/tests/settings.py +++ b/tests/settings.py @@ -18,6 +18,13 @@ 'tests', ] +try: + import grappelli # noqa +except ImportError: + pass +else: + INSTALLED_APPS.insert(0, 'grappelli') + MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', diff --git a/tests/test_admin.py b/tests/test_admin.py index 2e697ac..a628e30 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -5,6 +5,7 @@ from django.conf import settings from selenosis.testcases import AdminSelenosisTestCase from selenium.webdriver.common.keys import Keys +from selenium.webdriver.common.by import By from .models import Author, Publisher, Book, Library @@ -93,7 +94,8 @@ def get_count(): yield el def get_dropdown_count(self): - return len(self.selenium.find_elements_by_css_selector( + return len(self.selenium.find_elements( + By.CSS_SELECTOR, '.select2-drop-active .select2-results ' '.select2-result-selectable:not(.select2-selected)')) @@ -260,12 +262,14 @@ def test_m2m_ajax_custom_search_field(self): def test_inline_add_init(self): if django.VERSION < (1, 9): raise unittest.SkipTest("Django 1.8 does not have the formset:added event") - if 'grappelli' in settings.INSTALLED_APPS: - raise unittest.SkipTest("django-grappelli does not have the formset:added event") library = Library.objects.create(name="Princeton University Library") columbia_univ_press = Publisher.objects.get(name='Columbia University Press') self.load_admin(library) - with self.clickable_selector(".add-row a") as el: + if "grappelli" in settings.INSTALLED_APPS: + add_row_selector = ".grp-add-handler:not(.grp-icon)" + else: + add_row_selector = ".add-row a" + with self.clickable_selector(add_row_selector) as el: el.click() with self.clickable_selector('#id_book_set-0-title') as el: el.send_keys('Difference and Repetition') diff --git a/tox.ini b/tox.ini index 8cd40e3..6f2f085 100644 --- a/tox.ini +++ b/tox.ini @@ -1,73 +1,76 @@ [tox] envlist = - py3{7,8,9}-django{22,31,32} + py{36,37,38,39}-dj22-{grp,nogrp} + py{36,37,38,39,310}-dj32-{grp,nogrp} + py{38,39,310}-dj40-{grp,nogrp} + py{38,39,310,311}-dj41-{grp,nogrp} + py{38,39,310,311,312}-dj42-{grp,nogrp} + py{310,311,312}-dj{50,51}-{grp,nogrp} + py{310,311,312,313}-dj52-{grp,nogrp} +skipsdist = true + +[gh-actions] +python = + 3.7: py37 + 3.8: py38 + 3.9: py39 + 3.10: py310 + 3.11: py311 + 3.12: py312 + 3.13: py313 -[pytest] -django_find_project = false -DJANGO_SETTINGS_MODULE=tests.settings +[gh-actions:env] +DJANGO = + 2.2: dj22 + 3.2: dj32 + 4.0: dj40 + 4.1: dj41 + 4.2: dj42 + 5.0: dj50 + 5.1: dj51 + 5.2: dj52 +GRAPPELLI = + 0: nogrp + 1: grp [testenv] -description = Run tests in {envname} environment +commands = + pytest --junitxml={toxinidir}/reports/test-{envname}.xml {posargs} +usedevelop = True setenv = - PYTHONPATH = {toxinidir}:{env:PYTHONPATH:} -commands = pytest {posargs} + COVERAGE_FILE={toxworkdir}/coverage/.coverage.{envname} deps = - pytest>=5.2.0 + -e . + pytest + pytest-cov pytest-django - selenium==3.141.0 + selenium django-selenosis - django22: Django>=2.2,<3.0 - django31: Django>=3.1,<3.2 - django32: Django>=3.2,<4.0 - django22-grp: django-grappelli==2.13.4 - django31-grp: django-grappelli==2.14.4 - django32-grp: django-grappelli==2.15.1 - -[testenv:clean] -description = Clean all build and test artifacts -skipsdist = true -skip_install = true -deps = -whitelist_externals = - find - rm -commands = - find {toxinidir} -type f -name "*.pyc" -delete - find {toxinidir} -type d -name "__pycache__" -delete - rm -f {toxinidir}/tests/db.sqlite {toxworkdir} {toxinidir}/.pytest_cache {toxinidir}/build - -[testenv:docs] -description = Build Sphinx documentation -skipsdist = true -skip_install = true -commands = - sphinx-build -b html docs/source docs -deps = - sphinx - sphinx_rtd_theme + coverage + dj22: Django>=2.2,<3.0 + dj32: Django>=3.2,<4.0 + dj40: Django>=4.0,<4.1 + dj41: Django>=4.1,<4.2 + dj42: Django>=4.2,<4.3 + dj50: Django>=5.0,<5.1 + dj51: Django>=5.1a1,<5.2 + dj52: Django>=5.2,<5.3 + dj22-grp: django-grappelli>=2.13,<2.14 + dj32-grp: django-grappelli>=2.15,<2.16 + dj40-grp: django-grappelli==3.0.8 + dj41-grp: django-grappelli==3.0.8 + dj42-grp: django-grappelli>=3.0,<3.1 + dj50-grp: django-grappelli>=4.0,<4.1 + dj51-grp: django-grappelli>=4.0,<4.1 + dj52-grp: django-grappelli>=4.0,<4.1 + ipdb -[testenv:pep8] -description = Run PEP8 flake8 against the select2/ package directory -skipsdist = true +[testenv:coverage-report] skip_install = true -basepython = python3.7 -deps = flake8 -commands = flake8 select2 tests - -[testenv:coverage] -description = Run test coverage and display results -deps = - {[testenv]deps} - coverage - pytest-cov -whitelist_externals = - echo +deps = coverage +setenv=COVERAGE_FILE=.coverage +changedir = {toxworkdir}/coverage commands = - pytest --cov-config .coveragerc --cov-report html --cov-report term --cov=select2 - echo HTML coverage report: {toxinidir}/build/coverage/index.html - -[gh-actions] -python = - 3.7: py37 - 3.8: py38 - 3.9: py39 + coverage combine + coverage report + coverage xml