From fda1bd77f56f59a5060a796370b05e6015cd3946 Mon Sep 17 00:00:00 2001 From: Olivier Philippon Date: Fri, 2 Feb 2024 09:50:49 +0000 Subject: [PATCH] Bump django-crispy-forms to 2.0 (#61) * Update test matrix - Add Python 3.12 - Remove Django 2.2, 3.0 and 3.1 - Add Django 4.1 and 4.2 * Make tests pass * Upgrade Flake8, so that it works on Python 3.12 too * Remove `|safe` filters See https://github.com/torchbox/tbxforms/pull/28 * Add tests for HTML escaping --- .github/workflows/test.yml | 2 +- poetry.lock | 132 ++++++++--- pyproject.toml | 18 +- tbxforms/layout/buttons.py | 1 + tbxforms/layout/containers.py | 7 +- tbxforms/layout/fields.py | 2 - tbxforms/templates/tbx/field.html | 4 +- tbxforms/templates/tbx/layout/baseinput.html | 2 +- tbxforms/templates/tbx/layout/button.html | 4 +- tbxforms/templates/tbx/layout/checkboxes.html | 2 +- tbxforms/templates/tbx/layout/div.html | 4 +- tbxforms/templates/tbx/layout/fieldset.html | 6 +- tbxforms/templates/tbx/layout/help_text.html | 2 +- tbxforms/templates/tbx/layout/multifield.html | 2 +- tbxforms/templates/tbx/layout/radios.html | 2 +- tbxforms/templates/tbx/whole_uni_form.html | 2 +- tbxforms/templates/tbx/whole_uni_formset.html | 2 +- tests/forms.py | 12 + .../test_buttons/test_css_class.html | 2 +- .../test_buttons/test_disabled_button.html | 2 +- .../test_buttons/test_extra_attributes.html | 2 +- .../test_buttons/test_primary_button.html | 2 +- .../test_buttons/test_secondary_button.html | 2 +- .../test_buttons/test_warning_button.html | 2 +- tests/test_security.py | 206 ++++++++++++++++++ tox.ini | 8 +- 26 files changed, 365 insertions(+), 67 deletions(-) create mode 100644 tests/test_security.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7eb24f4..20c391f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,7 +28,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: ['3.8', '3.9', '3.10', '3.11'] + python: ['3.8', '3.9', '3.10', '3.11', '3.12'] steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v2 diff --git a/poetry.lock b/poetry.lock index 19bb7f3..8ba5659 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,9 +1,10 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.4.1 and should not be changed by hand. [[package]] name = "asgiref" version = "3.7.2" description = "ASGI specs, helper code, and adapters" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -21,6 +22,7 @@ tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] name = "atomicwrites" version = "1.4.1" description = "Atomic file writes." +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -31,6 +33,7 @@ files = [ name = "attrs" version = "23.1.0" description = "Classes Without Boilerplate" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -49,6 +52,7 @@ tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pyte name = "backports-zoneinfo" version = "0.2.1" description = "Backport of the standard library zoneinfo module" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -77,6 +81,7 @@ tzdata = ["tzdata"] name = "beautifulsoup4" version = "4.9.3" description = "Screen-scraping library" +category = "dev" optional = false python-versions = "*" files = [ @@ -96,6 +101,7 @@ lxml = ["lxml"] name = "black" version = "22.3.0" description = "The uncompromising code formatter." +category = "dev" optional = false python-versions = ">=3.6.2" files = [ @@ -142,6 +148,7 @@ uvloop = ["uvloop (>=0.15.2)"] name = "cachetools" version = "5.3.1" description = "Extensible memoizing collections and decorators" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -153,6 +160,7 @@ files = [ name = "certifi" version = "2023.7.22" description = "Python package for providing Mozilla's CA Bundle." +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -164,6 +172,7 @@ files = [ name = "cfgv" version = "3.4.0" description = "Validate configuration and produce human readable error messages." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -175,6 +184,7 @@ files = [ name = "chardet" version = "5.2.0" description = "Universal encoding detector for Python 3" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -186,6 +196,7 @@ files = [ name = "charset-normalizer" version = "3.2.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "dev" optional = false python-versions = ">=3.7.0" files = [ @@ -270,6 +281,7 @@ files = [ name = "click" version = "8.1.6" description = "Composable command line interface toolkit" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -284,6 +296,7 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." +category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -295,6 +308,7 @@ files = [ name = "colored" version = "1.4.4" description = "Simple library for color and formatting to terminal" +category = "dev" optional = false python-versions = "*" files = [ @@ -305,6 +319,7 @@ files = [ name = "coverage" version = "6.1.1" description = "Code coverage measurement for Python" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -363,6 +378,7 @@ toml = ["tomli"] name = "cssbeautifier" version = "1.14.9" description = "CSS unobfuscator and beautifier." +category = "dev" optional = false python-versions = "*" files = [ @@ -378,6 +394,7 @@ six = ">=1.13.0" name = "detect-secrets" version = "0.13.1" description = "Tool for detecting secrets in the codebase" +category = "dev" optional = false python-versions = "*" files = [ @@ -396,6 +413,7 @@ word-list = ["pyahocorasick"] name = "distlib" version = "0.3.7" description = "Distribution utilities" +category = "dev" optional = false python-versions = "*" files = [ @@ -407,6 +425,7 @@ files = [ name = "django" version = "4.2.4" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -426,19 +445,24 @@ bcrypt = ["bcrypt"] [[package]] name = "django-crispy-forms" -version = "1.14.0" +version = "2.1" description = "Best way to have Django DRY forms" +category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "django-crispy-forms-1.14.0.tar.gz", hash = "sha256:35887b8851a931374dd697207a8f56c57a9c5cb9dbf0b9fa54314da5666cea5b"}, - {file = "django_crispy_forms-1.14.0-py3-none-any.whl", hash = "sha256:bc4d2037f6de602d39c0bc452ac3029d1f5d65e88458872cc4dbc01c3a400604"}, + {file = "django-crispy-forms-2.1.tar.gz", hash = "sha256:4d7ec431933ad4d4b5c5a6de4a584d24613c347db9ac168723c9aaf63af4bb96"}, + {file = "django_crispy_forms-2.1-py3-none-any.whl", hash = "sha256:d592044771412ae1bd539cc377203aa61d4eebe77fcbc07fbc8f12d3746d4f6b"}, ] +[package.dependencies] +django = ">=4.2" + [[package]] name = "djlint" version = "1.23.3" description = "HTML Template Linter and Formatter" +category = "dev" optional = false python-versions = ">=3.8.0,<4.0.0" files = [ @@ -463,6 +487,7 @@ tqdm = ">=4.62.2,<5.0.0" name = "editorconfig" version = "0.12.3" description = "EditorConfig File Locator and Interpreter for Python" +category = "dev" optional = false python-versions = "*" files = [ @@ -474,6 +499,7 @@ files = [ name = "filelock" version = "3.12.2" description = "A platform independent file lock." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -487,24 +513,26 @@ testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "p [[package]] name = "flake8" -version = "4.0.1" +version = "5.0.4" description = "the modular source code checker: pep8 pyflakes and co" +category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.6.1" files = [ - {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, - {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, + {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, + {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, ] [package.dependencies] -mccabe = ">=0.6.0,<0.7.0" -pycodestyle = ">=2.8.0,<2.9.0" -pyflakes = ">=2.4.0,<2.5.0" +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.9.0,<2.10.0" +pyflakes = ">=2.5.0,<2.6.0" [[package]] name = "html-tag-names" version = "0.1.2" description = "List of known HTML tag names" +category = "dev" optional = false python-versions = ">=3.7,<4.0" files = [ @@ -516,6 +544,7 @@ files = [ name = "html-void-elements" version = "0.1.0" description = "List of HTML void tag names." +category = "dev" optional = false python-versions = ">=3.7,<4.0" files = [ @@ -527,6 +556,7 @@ files = [ name = "icdiff" version = "2.0.6" description = "improved colored diff" +category = "dev" optional = false python-versions = "*" files = [ @@ -537,6 +567,7 @@ files = [ name = "identify" version = "2.5.26" description = "File identification library for Python" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -551,6 +582,7 @@ license = ["ukkonen"] name = "idna" version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" +category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -562,6 +594,7 @@ files = [ name = "importlib-metadata" version = "6.8.0" description = "Read metadata from Python packages" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -581,6 +614,7 @@ testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -592,6 +626,7 @@ files = [ name = "isort" version = "5.10.1" description = "A Python utility / library to sort Python imports." +category = "dev" optional = false python-versions = ">=3.6.1,<4.0" files = [ @@ -609,6 +644,7 @@ requirements-deprecated-finder = ["pip-api", "pipreqs"] name = "jsbeautifier" version = "1.14.9" description = "JavaScript unobfuscator and beautifier." +category = "dev" optional = false python-versions = "*" files = [ @@ -623,6 +659,7 @@ six = ">=1.13.0" name = "markdown" version = "3.4.4" description = "Python implementation of John Gruber's Markdown." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -639,19 +676,21 @@ testing = ["coverage", "pyyaml"] [[package]] name = "mccabe" -version = "0.6.1" +version = "0.7.0" description = "McCabe checker, plugin for flake8" +category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.6" files = [ - {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, - {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, ] [[package]] name = "mypy-extensions" version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." +category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -663,6 +702,7 @@ files = [ name = "nodeenv" version = "1.8.0" description = "Node.js virtual environment builder" +category = "dev" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" files = [ @@ -677,6 +717,7 @@ setuptools = "*" name = "packaging" version = "23.1" description = "Core utilities for Python packages" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -688,6 +729,7 @@ files = [ name = "pathspec" version = "0.11.2" description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -699,6 +741,7 @@ files = [ name = "platformdirs" version = "3.10.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -714,6 +757,7 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-co name = "pluggy" version = "1.2.0" description = "plugin and hook calling mechanisms for python" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -729,6 +773,7 @@ testing = ["pytest", "pytest-benchmark"] name = "pprintpp" version = "0.4.0" description = "A drop-in replacement for pprint that's actually pretty" +category = "dev" optional = false python-versions = "*" files = [ @@ -740,6 +785,7 @@ files = [ name = "pre-commit" version = "2.15.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." +category = "dev" optional = false python-versions = ">=3.6.1" files = [ @@ -759,6 +805,7 @@ virtualenv = ">=20.0.8" name = "py" version = "1.11.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -768,30 +815,33 @@ files = [ [[package]] name = "pycodestyle" -version = "2.8.0" +version = "2.9.1" description = "Python style guide checker" +category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.6" files = [ - {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, - {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, + {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, + {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, ] [[package]] name = "pyflakes" -version = "2.4.0" +version = "2.5.0" description = "passive checker of Python programs" +category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.6" files = [ - {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, - {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, + {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, + {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, ] [[package]] name = "pymdown-extensions" version = "9.0" description = "Extension pack for Python Markdown." +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -806,6 +856,7 @@ Markdown = ">=3.2" name = "pyproject-api" version = "1.5.3" description = "API to interact with the python pyproject.toml based projects" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -825,6 +876,7 @@ testing = ["covdefaults (>=2.3)", "importlib-metadata (>=6.6)", "pytest (>=7.3.1 name = "pytest" version = "7.0.0" description = "pytest: simple powerful testing with Python" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -849,6 +901,7 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2. name = "pytest-icdiff" version = "0.5" description = "use icdiff for better error messages in pytest assertions" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -864,6 +917,7 @@ pytest = "*" name = "pyyaml" version = "6.0.1" description = "YAML parser and emitter for Python" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -872,6 +926,7 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -879,8 +934,15 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -897,6 +959,7 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -904,6 +967,7 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -913,6 +977,7 @@ files = [ name = "regex" version = "2023.8.8" description = "Alternative regular expression module, to replace re." +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1010,6 +1075,7 @@ files = [ name = "requests" version = "2.31.0" description = "Python HTTP for Humans." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1031,6 +1097,7 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] name = "setuptools" version = "68.0.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1047,6 +1114,7 @@ testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs ( name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -1058,6 +1126,7 @@ files = [ name = "soupsieve" version = "2.4.1" description = "A modern CSS selector implementation for Beautiful Soup." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1069,6 +1138,7 @@ files = [ name = "sqlparse" version = "0.4.4" description = "A non-validating SQL parser." +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -1085,6 +1155,7 @@ test = ["pytest", "pytest-cov"] name = "syrupy" version = "4.0.1" description = "Pytest Snapshot Test Utility" +category = "dev" optional = false python-versions = ">=3.8.1,<4" files = [ @@ -1100,6 +1171,7 @@ pytest = ">=7.0.0,<8.0.0" name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -1111,6 +1183,7 @@ files = [ name = "tomli" version = "2.0.1" description = "A lil' TOML parser" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1122,6 +1195,7 @@ files = [ name = "tox" version = "4.4.12" description = "tox is a generic virtualenv management and test command line tool" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1149,6 +1223,7 @@ testing = ["build[virtualenv] (>=0.10)", "covdefaults (>=2.3)", "devpi-process ( name = "tqdm" version = "4.66.1" description = "Fast, Extensible Progress Meter" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1169,6 +1244,7 @@ telegram = ["requests"] name = "typing-extensions" version = "4.7.1" description = "Backported and Experimental Type Hints for Python 3.7+" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1180,6 +1256,7 @@ files = [ name = "tzdata" version = "2023.3" description = "Provider of IANA time zone data" +category = "main" optional = false python-versions = ">=2" files = [ @@ -1191,6 +1268,7 @@ files = [ name = "urllib3" version = "2.0.4" description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1208,6 +1286,7 @@ zstd = ["zstandard (>=0.18.0)"] name = "virtualenv" version = "20.24.3" description = "Virtual Python Environment builder" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1228,6 +1307,7 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess name = "zipp" version = "3.16.2" description = "Backport of pathlib-compatible object wrapper for zip files" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1241,5 +1321,5 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" -python-versions = ">=3.8.1,<4.0" -content-hash = "f728850aac0e3b9b0459c1af336c3ccf861efdfbbe6b6d75c4161df1e09d8521" +python-versions = ">=3.8.1,<4.0" # Cannot remove upper-bound as isort needs >=3.6.1,<4.0 +content-hash = "8f4d872dc89179e4a4b4fe5f181e357a5a5db3b3f07a1a1a5bfb3a7c59f1bf72" diff --git a/pyproject.toml b/pyproject.toml index 6455c6d..f2d8f88 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,18 +17,22 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python :: 3", "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", "Framework :: Django", - "Framework :: Django :: 2.2", - "Framework :: Django :: 3.0", - "Framework :: Django :: 3.1", "Framework :: Django :: 3.2", "Framework :: Django :: 4.0", + "Framework :: Django :: 4.1", + "Framework :: Django :: 4.2", + "Framework :: Django :: 5.0", "Framework :: Wagtail", "Framework :: Wagtail :: 2", + "Framework :: Wagtail :: 3", + "Framework :: Wagtail :: 4", + "Framework :: Wagtail :: 5", ] packages = [ { include = "tbxforms" }, @@ -54,8 +58,8 @@ keywords = [ [tool.poetry.dependencies] python = ">=3.8.1,<4.0" # Cannot remove upper-bound as isort needs >=3.6.1,<4.0 -Django = ">=2.2" -django-crispy-forms = ">=1.13.0,<2.0" +Django = ">=3.2" +django-crispy-forms = ">=2.1,<3.0" [tool.poetry.dev-dependencies] pre-commit = "2.15.0" @@ -68,7 +72,7 @@ tox = ">=4.4,<4.5" # Linters etc. black = "22.3.0" detect-secrets = "~0.13" -flake8 = "4.0.1" +flake8 = "5.0.4" isort = "5.10.1" djlint = "1.23.3" diff --git a/tbxforms/layout/buttons.py b/tbxforms/layout/buttons.py index f817189..5ca0e8e 100644 --- a/tbxforms/layout/buttons.py +++ b/tbxforms/layout/buttons.py @@ -54,6 +54,7 @@ class to get them to display as secondary or warning buttons. """ + input_type = "button" template = "%s/layout/button.html" @classmethod diff --git a/tbxforms/layout/containers.py b/tbxforms/layout/containers.py index cfb052e..9957329 100644 --- a/tbxforms/layout/containers.py +++ b/tbxforms/layout/containers.py @@ -141,16 +141,13 @@ def __init__( self.template = kwargs.pop("template", self.template) self.flat_attrs = flatatt(kwargs) - def render( - self, form, form_style, context, template_pack=TEMPLATE_PACK, **kwargs - ): + def render(self, form, context, template_pack=TEMPLATE_PACK, **kwargs): fields = self.get_rendered_fields( - form, form_style, context, template_pack, **kwargs + form, context, template_pack, **kwargs ) context = { "fieldset": self, "fields": fields, - "form_style": form_style, } context.update(self.context) template = self.get_template_name(template_pack) diff --git a/tbxforms/layout/fields.py b/tbxforms/layout/fields.py index a21f4f5..5f312f9 100644 --- a/tbxforms/layout/fields.py +++ b/tbxforms/layout/fields.py @@ -435,7 +435,6 @@ def add_attributes(self, **kwargs): def render( self, form, - form_style, context, template_pack=TEMPLATE_PACK, **kwargs, @@ -443,7 +442,6 @@ def render( template = self.get_template_name(template_pack) return self.get_rendered_fields( form, - form_style, context, template_pack, template=template, diff --git a/tbxforms/templates/tbx/field.html b/tbxforms/templates/tbx/field.html index 71a1509..8b150a6 100644 --- a/tbxforms/templates/tbx/field.html +++ b/tbxforms/templates/tbx/field.html @@ -25,7 +25,7 @@ {% if field.label and not field|is_checkbox and form_show_labels %} {% if label_tag %}<{{ label_tag }} class="tbxforms-label-wrapper">{% endif %} {% if label_tag %}{% endif %} @@ -37,7 +37,7 @@
{% crispy_tbx_field field %}
diff --git a/tbxforms/templates/tbx/layout/baseinput.html b/tbxforms/templates/tbx/layout/baseinput.html index aaee414..e209c3e 100644 --- a/tbxforms/templates/tbx/layout/baseinput.html +++ b/tbxforms/templates/tbx/layout/baseinput.html @@ -6,5 +6,5 @@ class="{{ input.css_class }}" id="{% if input.id %}{{ input.id }}{% else %}{{ input.input_type }}-id-{{ input.name|slugify }}{% endif %}" {% endif %} - {{ input.flat_attrs|safe }} + {{ input.flat_attrs }} /> diff --git a/tbxforms/templates/tbx/layout/button.html b/tbxforms/templates/tbx/layout/button.html index 50f8133..e1a3c9e 100644 --- a/tbxforms/templates/tbx/layout/button.html +++ b/tbxforms/templates/tbx/layout/button.html @@ -2,5 +2,5 @@ name="{% if input.name|wordcount > 1 %}{{ input.name|slugify }}{% else %}{{ input.name }}{% endif %}" class="{% if input.css_class %}{{ input.css_class }}{% else %}tbxforms-button tbxforms-button--primary{% endif %}" id="{% if input.id %}{{ input.id }}{% else %}id_{{ input.name|slugify }}{% endif %}" - {{ input.flat_attrs|safe }} ->{{ input.value|safe }} + {{ input.flat_attrs }} +>{{ input.value }} diff --git a/tbxforms/templates/tbx/layout/checkboxes.html b/tbxforms/templates/tbx/layout/checkboxes.html index e383b64..917f72f 100644 --- a/tbxforms/templates/tbx/layout/checkboxes.html +++ b/tbxforms/templates/tbx/layout/checkboxes.html @@ -10,7 +10,7 @@ {% if field.label %} {% if legend_tag %}<{{ legend_tag }} class="tbxforms-fieldset__heading">{% endif %} - {{ field.label|safe }} + {{ field.label }} {% if not field|show_as_required %} {% trans "(optional)" %}{% endif %} {% if legend_tag %}{% endif %} diff --git a/tbxforms/templates/tbx/layout/div.html b/tbxforms/templates/tbx/layout/div.html index fb6747a..963f154 100644 --- a/tbxforms/templates/tbx/layout/div.html +++ b/tbxforms/templates/tbx/layout/div.html @@ -1,7 +1,7 @@
- {{ fields|safe }} + {{ fields }}
diff --git a/tbxforms/templates/tbx/layout/fieldset.html b/tbxforms/templates/tbx/layout/fieldset.html index 650f164..316e32c 100644 --- a/tbxforms/templates/tbx/layout/fieldset.html +++ b/tbxforms/templates/tbx/layout/fieldset.html @@ -1,17 +1,17 @@
{% if legend %} {% if legend_tag %}<{{ legend_tag }} class="tbxforms-fieldset__heading">{% endif %} - {{ legend|safe }} + {{ legend }} {% if legend_tag %}{% endif %} {% endif %} - {{ fields|safe }} + {{ fields }}
diff --git a/tbxforms/templates/tbx/layout/help_text.html b/tbxforms/templates/tbx/layout/help_text.html index 49524d9..c68998f 100644 --- a/tbxforms/templates/tbx/layout/help_text.html +++ b/tbxforms/templates/tbx/layout/help_text.html @@ -1,3 +1,3 @@ {% if field.help_text %} - {{ field.help_text|safe }} + {{ field.help_text }} {% endif %} diff --git a/tbxforms/templates/tbx/layout/multifield.html b/tbxforms/templates/tbx/layout/multifield.html index 218bec7..50dd63e 100644 --- a/tbxforms/templates/tbx/layout/multifield.html +++ b/tbxforms/templates/tbx/layout/multifield.html @@ -10,7 +10,7 @@ {% if field.label %} {% if legend_tag %}<{{ legend_tag }} class="tbxforms-fieldset__heading">{% endif %} - {{ field.label|safe }} + {{ field.label }} {% if not field|show_as_required %} {% trans "(optional)" %}{% endif %} {% if legend_tag %}{% endif %} diff --git a/tbxforms/templates/tbx/layout/radios.html b/tbxforms/templates/tbx/layout/radios.html index 12e1f94..7b2c3a1 100644 --- a/tbxforms/templates/tbx/layout/radios.html +++ b/tbxforms/templates/tbx/layout/radios.html @@ -10,7 +10,7 @@ {% if field.label %} {% if legend_tag %}<{{ legend_tag }} class="tbxforms-fieldset__heading">{% endif %} - {{ field.label|safe }} + {{ field.label }} {% if not field|show_as_required %} {% trans "(optional)" %}{% endif %} {% if legend_tag %}{% endif %} diff --git a/tbxforms/templates/tbx/whole_uni_form.html b/tbxforms/templates/tbx/whole_uni_form.html index 3ac45fd..1fbafe5 100644 --- a/tbxforms/templates/tbx/whole_uni_form.html +++ b/tbxforms/templates/tbx/whole_uni_form.html @@ -1,5 +1,5 @@ {% if form_tag %} -
+ {% endif %} {% if form_method|lower == 'post' and not disable_csrf %} diff --git a/tbxforms/templates/tbx/whole_uni_formset.html b/tbxforms/templates/tbx/whole_uni_formset.html index 0eacf3c..e4e502d 100644 --- a/tbxforms/templates/tbx/whole_uni_formset.html +++ b/tbxforms/templates/tbx/whole_uni_formset.html @@ -1,7 +1,7 @@ {% load crispy_forms_tags %} {% if formset_tag %} - + {% endif %} {% if formset_method|lower == 'post' and not disable_csrf %} diff --git a/tests/forms.py b/tests/forms.py index 0ac004b..338b798 100644 --- a/tests/forms.py +++ b/tests/forms.py @@ -4,12 +4,24 @@ from tbxforms.fields import DateInputField from tbxforms.forms import TbxFormsMixin from tbxforms.layout import ( + Button, Field, Fieldset, Layout, ) +class ButtonForm(TbxFormsMixin, forms.Form): + # A "([factory], [factory_args])" tuple specifies the button to display + button_spec = (Button.primary, ("name", "Title")) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.helper.layout = Layout( + self.button_spec[0](*self.button_spec[1]), + ) + + class CheckboxForm(TbxFormsMixin, forms.Form): accept = forms.BooleanField( label="I accept the terms of service", diff --git a/tests/layout/__snapshots__/test_buttons/test_css_class.html b/tests/layout/__snapshots__/test_buttons/test_css_class.html index 12fa742..f68ff99 100644 --- a/tests/layout/__snapshots__/test_buttons/test_css_class.html +++ b/tests/layout/__snapshots__/test_buttons/test_css_class.html @@ -1,3 +1,3 @@ + id="button-id-name">Title diff --git a/tests/layout/__snapshots__/test_buttons/test_disabled_button.html b/tests/layout/__snapshots__/test_buttons/test_disabled_button.html index 10bde0f..152d195 100644 --- a/tests/layout/__snapshots__/test_buttons/test_disabled_button.html +++ b/tests/layout/__snapshots__/test_buttons/test_disabled_button.html @@ -1,5 +1,5 @@ diff --git a/tests/layout/__snapshots__/test_buttons/test_extra_attributes.html b/tests/layout/__snapshots__/test_buttons/test_extra_attributes.html index 61ffafa..dca2fef 100644 --- a/tests/layout/__snapshots__/test_buttons/test_extra_attributes.html +++ b/tests/layout/__snapshots__/test_buttons/test_extra_attributes.html @@ -1,4 +1,4 @@ diff --git a/tests/layout/__snapshots__/test_buttons/test_primary_button.html b/tests/layout/__snapshots__/test_buttons/test_primary_button.html index cad2131..fc3e3ea 100644 --- a/tests/layout/__snapshots__/test_buttons/test_primary_button.html +++ b/tests/layout/__snapshots__/test_buttons/test_primary_button.html @@ -1,3 +1,3 @@ + id="button-id-name">Title diff --git a/tests/layout/__snapshots__/test_buttons/test_secondary_button.html b/tests/layout/__snapshots__/test_buttons/test_secondary_button.html index a7e754f..df8bcc6 100644 --- a/tests/layout/__snapshots__/test_buttons/test_secondary_button.html +++ b/tests/layout/__snapshots__/test_buttons/test_secondary_button.html @@ -1,3 +1,3 @@ + id="button-id-name">Title diff --git a/tests/layout/__snapshots__/test_buttons/test_warning_button.html b/tests/layout/__snapshots__/test_buttons/test_warning_button.html index d359000..5b05f46 100644 --- a/tests/layout/__snapshots__/test_buttons/test_warning_button.html +++ b/tests/layout/__snapshots__/test_buttons/test_warning_button.html @@ -1,3 +1,3 @@ + id="button-id-name">Title diff --git a/tests/test_security.py b/tests/test_security.py new file mode 100644 index 0000000..0b17f31 --- /dev/null +++ b/tests/test_security.py @@ -0,0 +1,206 @@ +from html import escape +from typing import TYPE_CHECKING + +import pytest + +from tbxforms.layout import Button + +from .forms import ButtonForm as ButtonFormBase +from .forms import ( + CheckboxesForm, + CheckboxForm, + DateInputForm, + FieldsetForm, + FileUploadForm, + RadiosForm, + SelectForm, + TextareaForm, + TextInputForm, +) +from .utils import render_form + +if TYPE_CHECKING: + # As we support Python 3.8 we have to use `typing.*` for tuples, dicts, etc + from typing import ( + Callable, + Dict, + Tuple, + Type, + ) + + from django.forms import Form + + +@pytest.mark.parametrize( + ("button_factory", "button_factory_args"), + ( + (Button.primary, ("name", "
Title
")), + (Button.secondary, ("name", "
Title
")), + (Button.warning, ("name", "
Title
")), + ), +) +def test_html_escaping_buttons( + button_factory: "Callable", + button_factory_args: "Tuple[str, ...]", +): + class ButtonForm(ButtonFormBase): + button_spec = (button_factory, button_factory_args) + + form = ButtonForm() + rendered_form = render_form(form) + + for button_arg in button_factory_args: + assert button_arg not in rendered_form + button_arg_escaped = escape(button_arg) + assert button_arg_escaped in rendered_form + # Let's also make sure we were testing the right value: + assert len(button_arg_escaped) > len(button_arg) + + +@pytest.mark.parametrize( + ("input_form_class", "form_fields_operations"), + ( + pytest.param( + CheckboxForm, + { + "accept": { + "label": "Accept", + "help_text": "This is mandatory", + } + }, + id="CheckboxForm", + ), + pytest.param( + CheckboxesForm, + { + "method": { + "choices": ( + ("email", "Email"), + ("phone", "Phone"), + ("text", "Text message"), + ), + "label": "Accept", + "help_text": "This is mandatory", + } + }, + id="CheckboxesForm", + ), + pytest.param( + DateInputForm, + { + "date": { + "label": "Passport issue date", + "help_text": "This is mandatory", + } + }, + id="DateInputForm", + ), + pytest.param( + FileUploadForm, + { + "file": { + "label": "Upload a file", + "help_text": "This is mandatory", + } + }, + id="FileUploadForm", + ), + pytest.param( + RadiosForm, + { + "method": { + "choices": ( + ("email", "Email"), + ("phone", "Phone"), + ("text", "Text message"), + ), + "label": "Accept", + "help_text": "This is mandatory", + } + }, + id="RadiosForm", + ), + pytest.param( + SelectForm, + { + "method": { + "choices": ( + ("", "Choose"), + ("email", "Email"), + ("phone", "Phone"), + ("text", "Text message"), + ), + "label": "Accept", + "help_text": "This is mandatory", + } + }, + id="SelectForm", + ), + pytest.param( + TextInputForm, + { + "name": { + "label": "Name", + "help_text": "This is mandatory", + } + }, + id="TextInputForm", + ), + pytest.param( + TextareaForm, + { + "description": { + "label": "Description", + "help_text": "This is mandatory", + } + }, + id="TextareaForm", + ), + pytest.param( + FieldsetForm, + { + "name": { + "label": "Name", + "help_text": "This is mandatory", + }, + "email": { + "label": "Email", + "help_text": "This is mandatory too", + }, + }, + id="FieldsetForm", + ), + ), +) +def test_html_escaping_fields( + input_form_class: "Type[Form]", + form_fields_operations: "Dict[str, Dict[str, str]]", +): + form = input_form_class() + for field_name, field_operations in form_fields_operations.items(): + for ( + field_operation_name, + field_operation_value, + ) in field_operations.items(): + setattr( + form.fields[field_name], + field_operation_name, + field_operation_value, + ) + + rendered_form = render_form(form) + + for field_operations in form_fields_operations.values(): + for field_name, field_values in field_operations.items(): + if isinstance(field_values, tuple): + values = field_values + else: + values = (field_values,) + for value in values: + if field_name == "choices": + value = value[1] + assert value not in rendered_form + value_escaped = escape(value) + assert value_escaped in rendered_form + # Let's also make sure we were testing the right value: + assert len(value_escaped) > len(value) diff --git a/tox.ini b/tox.ini index e181336..08cd969 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py{38,39,310,311}-dj{22,30,31,32,40} +envlist = py{38,39,310,311,312}-dj{32,40,41,42} skip_missing_interpreters = True isolated_build = True basepython = python3 @@ -10,6 +10,7 @@ python = 3.9: python3.9 3.10: python3.10 3.11: python3.11 + 3.12: python3.12 [testenv] allowlist_externals = pytest @@ -17,11 +18,10 @@ setenv = PYTHONPATH = {toxinidir} DJANGO_SETTINGS_MODULE = tests.settings deps = + dj42: Django>=4.2,<5.0 + dj41: Django>=4.1,<4.2 dj40: Django>=4.0,<4.1 dj32: Django>=3.2,<4.0 - dj31: Django>=3.1,<3.2 - dj30: Django>=3.0,<3.1 - dj22: Django>=2.2,<2.3 extras = test commands = pytest