From 6b8e6f1da97825bea6536708db47c7a30516d1a6 Mon Sep 17 00:00:00 2001 From: Leonardo Cavallucci Date: Tue, 8 Aug 2023 11:10:19 +0200 Subject: [PATCH 01/25] Upgrade package tooling, drop Django<3.2, DjangoCMS<3.9 and Python<3.9 --- .bumpversion.cfg | 25 ++++ .github/ISSUE_TEMPLATE/--bug-report.md | 48 +++++++ .github/ISSUE_TEMPLATE/--feature-request.md | 42 ++++++ .github/workflows/codeql.yml | 76 ++++++++++ .github/workflows/lint.yml | 42 ++++++ .github/workflows/logger.yml | 11 ++ .github/workflows/publish.yml | 38 +++++ .github/workflows/test.yml | 60 ++++++++ .pre-commit-config.yaml | 53 +++++++ .pyup.yml | 7 + .travis.yml | 2 - CONTRIBUTING.rst | 114 +++++++++++---- HISTORY.rst | 41 ++++-- MANIFEST.in | 1 + README.rst | 25 ++-- changes/33.feature | 1 + cms_helper.py | 104 +++++++------- djangocms_multisite/__init__.py | 2 +- djangocms_multisite/middleware.py | 17 +-- pyproject.toml | 48 +++++++ requirements-test.txt | 4 +- setup.cfg | 61 +++++--- setup.py | 53 +------ tasks.py | 122 ++++++++++++++++ tests/test_middleware.py | 145 ++++++++++---------- tests/test_utils/urls1.py | 16 +-- tests/test_utils/urls2.py | 16 +-- tests/utils.py | 5 +- tox.ini | 121 +++++++++++++--- 29 files changed, 1009 insertions(+), 291 deletions(-) create mode 100644 .bumpversion.cfg create mode 100644 .github/ISSUE_TEMPLATE/--bug-report.md create mode 100644 .github/ISSUE_TEMPLATE/--feature-request.md create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/logger.yml create mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/test.yml create mode 100644 .pre-commit-config.yaml create mode 100644 .pyup.yml create mode 100644 changes/33.feature create mode 100644 pyproject.toml mode change 100755 => 100644 setup.py create mode 100644 tasks.py diff --git a/.bumpversion.cfg b/.bumpversion.cfg new file mode 100644 index 0000000..bcf7feb --- /dev/null +++ b/.bumpversion.cfg @@ -0,0 +1,25 @@ +[bumpversion] +current_version = 0.3.1 +parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\.?)(?P[a-z]*)(?P\d*) +serialize = + {major}.{minor}.{patch}.{release}{relver} + {major}.{minor}.{patch} +commit = True +tag = False +sign_tags = True +tag_name = {new_version} +message = Release {new_version} + +[bumpversion:part:release] +optional_value = gamma +values = + dev + a + b + rc + gamma + +[bumpversion:part:relver] +first_value = 1 + +[bumpversion:file:djangocms_multisite/__init__.py] diff --git a/.github/ISSUE_TEMPLATE/--bug-report.md b/.github/ISSUE_TEMPLATE/--bug-report.md new file mode 100644 index 0000000..19d08d8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/--bug-report.md @@ -0,0 +1,48 @@ +--- +name: "\U0001F41B Bug report" +about: Create a report to help us improve +title: '' +labels: 'type: bug' +assignees: '' + +--- + + + +## Description + + + +## Steps to reproduce + + + +## Versions + + + +## Expected behaviour + + + +## Actual behaviour + + + +## Additional information + + diff --git a/.github/ISSUE_TEMPLATE/--feature-request.md b/.github/ISSUE_TEMPLATE/--feature-request.md new file mode 100644 index 0000000..5b74e5f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/--feature-request.md @@ -0,0 +1,42 @@ +--- +name: "\U0001F389 Feature request" +about: Share your idea, let's discuss it! +title: '' +labels: 'type: feature' +assignees: '' + +--- + + + +## Description + + + +## Use cases + + + +## Proposed solution + + + +## Alternatives + + + +## Additional information + + diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..0310222 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,76 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "master" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "master" ] + schedule: + - cron: '37 3 * * 3' + +jobs: + analyze: + name: Analyze + runs-on: 'ubuntu-latest' + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'python' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Use only 'java' to analyze code written in Java, Kotlin or both + # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..701b10e --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,42 @@ +name: Code quality + +on: [push, pull_request] + +jobs: + lint: + runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, '[skip ci]')" + strategy: + matrix: + python-version: ["3.11.x"] + toxenv: [ruff, isort, black, pypi-description, docs, towncrier] + steps: + - uses: actions/checkout@v3 + with: + repository: ${{ github.event.pull_request.head.repo.full_name }} + ref: ${{ github.event.pull_request.head.ref }} + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Cache pip + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ matrix.toxenv }} + restore-keys: | + ${{ runner.os }}-pip-${{ matrix.toxenv }} + - name: Cache tox + uses: actions/cache@v3 + with: + path: .tox + key: ${{ runner.os }}-lint-${{ matrix.toxenv }}-${{ hashFiles('setup.cfg') }} + restore-keys: | + ${{ runner.os }}-lint-${{ matrix.toxenv }}- + - name: Install dependencies + run: | + python -m pip install --upgrade pip setuptools tox>4 + - name: Test with tox + if: ${{ matrix.toxenv != 'towncrier' || (!contains(github.event.head_commit.message, '[pre-commit.ci]') && !contains(github.event.pull_request.body, 'pre-commit.ci start')) }} + run: | + tox -e${{ matrix.toxenv }} diff --git a/.github/workflows/logger.yml b/.github/workflows/logger.yml new file mode 100644 index 0000000..7b47896 --- /dev/null +++ b/.github/workflows/logger.yml @@ -0,0 +1,11 @@ +name: Event Logger +on: push + +jobs: + log-github-event-goodies: + name: "LOG Everything on GitHub Event" + runs-on: ubuntu-latest + steps: + - name: Logging + run: | + echo '${{toJSON(github.event)}}' diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..641c0a7 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,38 @@ +name: Upload Python Package + +on: + release: + types: [published,prereleased] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + - name: Cache pip + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ matrix.toxenv }} + restore-keys: | + ${{ runner.os }}-pip-${{ matrix.toxenv }} + - name: Cache tox + uses: actions/cache@v3 + with: + path: .tox + key: ${{ runner.os }}-tox-release-${{ hashFiles('setup.cfg') }} + restore-keys: | + ${{ runner.os }}-tox-release- + - name: Install dependencies + run: | + python -m pip install --upgrade pip setuptools tox>4 + - name: Build and publish + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: | + tox -erelease diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..8bc4fe0 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,60 @@ +name: Tox tests + +on: [push, pull_request] + +jobs: + test: + if: "!contains(github.event.head_commit.message, '[skip ci]')" + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.continue-on-error }} + strategy: + matrix: + python-version: ["3.11", "3.10", "3.9"] + django: [42, 41, 32] + cms: [311, 39] + continue-on-error: [true] + exclude: + - django: 41 + cms: 39 + - django: 42 + cms: 39 + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Cache pip + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ matrix.toxenv }} + restore-keys: | + ${{ runner.os }}-pip-${{ matrix.toxenv }} + - name: Cache tox + uses: actions/cache@v3 + with: + path: .tox + key: ${{ runner.os }}-tox-${{ format('{{py{0}-django{1}-cms{2}}}', matrix.python-version, matrix.django, matrix.cms) }}-${{ hashFiles('setup.cfg') }} + restore-keys: | + ${{ runner.os }}-tox-${{ format('{{py{0}-django{1}-cms{2}}}', matrix.python-version, matrix.django, matrix.cms) }}- + - name: Install dependencies + run: | + sudo apt-get install gettext + python -m pip install --upgrade pip setuptools tox>4 + - name: Test with tox + env: + TOX_ENV: ${{ format('py-django{1}-cms{2}', matrix.python-version, matrix.django, matrix.cms) }} + COMMAND: coverage run + COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COVERALLS_SERVICE_NAME: github + run: | + tox -e$TOX_ENV + .tox/$TOX_ENV/bin/coverage xml + .tox/$TOX_ENV/bin/coveralls + - uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + flags: unittests + files: ./coverage.xml + fail_ci_if_error: false diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..2f6c968 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,53 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +exclude: "(.idea|node_modules|.tox)" +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: trailing-whitespace + exclude: ".+cfg" + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - id: check-builtin-literals + - id: check-executables-have-shebangs + - id: check-merge-conflict + - id: check-toml + - id: fix-encoding-pragma + args: + - --remove + - repo: https://github.com/PyCQA/isort + rev: "5.12.0" + hooks: + - id: isort + - repo: https://github.com/psf/black + rev: 23.7.0 + hooks: + - id: black + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: 'v0.0.281' + hooks: + - id: ruff + - repo: https://github.com/asottile/pyupgrade + rev: v3.10.1 + hooks: + - id: pyupgrade + args: + - --py3-plus + - repo: https://github.com/adamchainz/django-upgrade + rev: "1.14.0" + hooks: + - id: django-upgrade + args: [--target-version, "3.2"] + - repo: local + hooks: + - id: towncrier + name: towncrier + entry: inv towncrier-check + language: system + pass_filenames: false + always_run: true +ci: + skip: + - towncrier diff --git a/.pyup.yml b/.pyup.yml new file mode 100644 index 0000000..2809577 --- /dev/null +++ b/.pyup.yml @@ -0,0 +1,7 @@ +update: all +pin: False +branch: +schedule: "every day" +search: True +branch_prefix: pyup/ +close_prs: True diff --git a/.travis.yml b/.travis.yml index d2cadbb..1667ff6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -62,5 +62,3 @@ matrix: cache: directories: - $HOME/.cache/pip - - diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index b616dd9..cc6cb46 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,6 +1,6 @@ -============ +############ Contributing -============ +############ Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. @@ -8,10 +8,10 @@ little bit helps, and credit will always be given. You can contribute in many ways: Types of Contributions ----------------------- +********************** Report Bugs -~~~~~~~~~~~ +=========== Report bugs at https://github.com/nephila/djangocms-multisite/issues. @@ -22,26 +22,26 @@ If you are reporting a bug, please include: * Detailed steps to reproduce the bug. Fix Bugs -~~~~~~~~ +======== Look through the GitHub issues for bugs. Anything tagged with "bug" is open to whoever wants to implement it. Implement Features -~~~~~~~~~~~~~~~~~~ +================== Look through the GitHub issues for features. Anything tagged with "feature" is open to whoever wants to implement it. Write Documentation -~~~~~~~~~~~~~~~~~~~ +=================== djangocms-multisite could always use more documentation, whether as part of the official djangocms-multisite docs, in docstrings, or even on the web in blog posts, articles, and such. Submit Feedback -~~~~~~~~~~~~~~~ +=============== The best way to send feedback is to file an issue at https://github.com/nephila/djangocms-multisite/issues. @@ -52,21 +52,24 @@ If you are proposing a feature: * Remember that this is a volunteer-driven project, and that contributions are welcome :) +************ Get Started! ------------- +************ -Ready to contribute? Here's how to set up `djangocms-multisite` for local development. +Ready to contribute? Here's how to set up ``djangocms-multisite`` for local development. -1. Fork the `djangocms-multisite` repo on GitHub. +1. Fork the ``djangocms-multisite`` repo on GitHub. 2. Clone your fork locally:: $ git clone git@github.com:your_name_here/djangocms-multisite.git -3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: +3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper + installed, this is how you set up your fork for local development:: $ mkvirtualenv djangocms-multisite $ cd djangocms-multisite/ - $ python setup.py develop + $ pip install -r requirements-test.txt + $ pip install -e . 4. Create a branch for local development:: @@ -77,11 +80,9 @@ Now you can make your changes locally. 5. When you're done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox:: - $ flake8 djangocms_multisite tests - $ python setup.py test $ tox -To get flake8 and tox, just pip install them into your virtualenv. +To get tox, pip install it into your virtualenv. 6. Commit your changes and push your branch to GitHub:: @@ -91,22 +92,75 @@ To get flake8 and tox, just pip install them into your virtualenv. 7. Submit a pull request through the GitHub website. -Pull Request Guidelines ------------------------ +Development tips +---------------- -Before you submit a pull request, check that it meets these guidelines: +This project allows you to use `pre-commit `_ to ensure an easy compliance +to the project code styles. + +If you want to use it, install it globally (for example with ``pip3 install --user precommit``, +but check `installation instruction `_. +When first cloning the project ensure you install the git hooks by running ``pre-commit install``. + +From now on every commit will be checked against our code style. + +Check also the available tox environments with ``tox -l``: the ones not marked with a python version number are tools +to help you work on the project buy checking / formatting code style, running docs etc. -1. The pull request should include tests. -2. If the pull request adds functionality, the docs should be updated. Put - your new functionality into a function with a docstring, and add the - feature to the list in README.rst. -3. The pull request should work for Python 2.6, 2.7, and 3.3, and for PyPy. Check - https://travis-ci.org/nephila/djangocms-multisite/pull_requests - and make sure that the tests pass for all supported Python versions. +Testing tips +---------------- +You can test your project using any specific combination of python, django and django cms. -Tips ----- +For example ``tox -py37-django30-cms37`` runs the tests on python 3.7, Django 3.0 and django CMS 3.7. -To run a subset of tests:: - $ python -m unittest tests.test_djangocms_multisite +Pull Request Guidelines +======================= + +Before you submit a pull request, check that it meets these guidelines: + +#. Pull request must be named with the following naming scheme: + + ``/(-)-description`` + + See below for available types. + +#. The pull request should include tests. +#. If the pull request adds functionality, the docs should be updated. + Documentation must be added in ``docs`` directory, and must include usage + information for the end user. + In case of public API method, add extended docstrings with full parameters + description and usage example. +#. Add a changes file in ``changes`` directory describing the contribution in + one line. It will be added automatically to the history file upon release. + File must be named as ``.`` with type being: + + * ``.feature``: For new features. + * ``.bugfix``: For bug fixes. + * ``.doc``: For documentation improvement. + * ``.removal``: For deprecation or removal of public API. + * ``.misc``: For general issues. + + Check `towncrier`_ documentation for more details. + +#. The pull request should work for all python / django / django CMS versions + declared in tox.ini. + Check the CI and make sure that the tests pass for all supported versions. + +Release a version +================= + +#. Update authors file +#. Merge ``develop`` on ``master`` branch +#. Bump release via task: ``inv tag-release (major|minor|patch)`` +#. Update changelog via towncrier: ``towncrier --yes`` +#. Commit changelog with ``git commit --amend`` to merge with bumpversion commit +#. Create tag ``git tag `` +#. Push tag to github +#. Publish the release from the tags page +#. If pipeline succeeds, push ``master`` +#. Merge ``master`` back on ``develop`` +#. Bump developement version via task: ``inv tag-dev -l (major|minor|patch)`` +#. Push ``develop`` + +.. _towncrier: https://pypi.org/project/towncrier/#news-fragments diff --git a/HISTORY.rst b/HISTORY.rst index 04ea180..cd2e242 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,27 +1,42 @@ .. :changelog: +******* History -======= +******* -0.3.1 (unreleased) -^^^^^^^^^^^^^^^^^^ +.. towncrier release notes start -* Fix history formatting preventing proper pypi release +0.3.1 (2020-07-06) +================== + +Bugfixes +-------- + +- Fix history formatting preventing proper pypi release 0.3.0 (2020-07-05) -^^^^^^^^^^^^^^^^^^ +================== + +Features +-------- -* Add Django 2.2 / django CMS 3.7+ support -* Drop older Django / django CMS versions +- Add Django 2.2 / django CMS 3.7+ support +- Drop older Django / django CMS versions 0.2.0 (2018-09-25) -^^^^^^^^^^^^^^^^^^ +================== -* Add Django 1.11 / django CMS 3.4+ support -* Drop older Django / django CMS versions -* Improve documentation +Features +-------- + +- Add Django 1.11 / django CMS 3.4+ support +- Drop older Django / django CMS versions +- Improve documentation 0.1.0 (unreleased) -^^^^^^^^^^^^^^^^^^ +================== + +Features +-------- -* First experimental release +- First experimental release diff --git a/MANIFEST.in b/MANIFEST.in index d352a68..e16a729 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,4 +3,5 @@ include CONTRIBUTING.rst include HISTORY.rst include LICENSE include README.rst +include Makefile recursive-include djangocms_multisite *.html *.png *.gif *js *jpg *jpeg *svg *py *css *po *mo diff --git a/README.rst b/README.rst index ff17bae..9fd1dbc 100644 --- a/README.rst +++ b/README.rst @@ -6,8 +6,9 @@ djangocms-multisite |Gitter| |PyPiVersion| |PyVersion| |Status| |TestCoverage| |TestCoveralls| |CodeClimate| |License| +******* Support -======= +******* Supported *Django* versions: @@ -18,19 +19,22 @@ Supported django CMS versions: * django CMS 3.7 +*********** Assumptions -=========== +*********** #. A virtualenv up and running #. DjangoCMS working +************ Installation -============ +************ ``pip install djangocms-multisite`` +***** Usage -===== +***** #. Open your ``settings.py`` file @@ -95,30 +99,31 @@ Usage #. Run ``python manage.py migrate`` to apply the `django-multisite `_ migrations - +******************** Settings explanation -==================== +******************** MULTISITE_CMS_URLS -^^^^^^^^^^^^^^^^^^ +================== Dictionary (or OrderedDict) containing the mapping between the domain (as configured in django ``sites``) and the corresponding urlconf. MULTISITE_CMS_FALLBACK -^^^^^^^^^^^^^^^^^^^^^^ +====================== The default domain to load if any of the above does not match. MULTISITE_CMS_ALIASES -^^^^^^^^^^^^^^^^^^^^^ +===================== Dictionary (or OrderedDict) containing the mapping between the domain (as configured in django ``sites``) and a list of aliases. This is optional if all the aliases are configured as ``django-multisite`` aliases +*************** Troubleshooting -=============== +*************** * Domains in ``MULTISITE_CMS_URLS`` must be the same created in your database (via the interface in ``Home › Sites › Sites``). diff --git a/changes/33.feature b/changes/33.feature new file mode 100644 index 0000000..53f5405 --- /dev/null +++ b/changes/33.feature @@ -0,0 +1 @@ +Upgrade package tooling, drop Django<3.2, DjangoCMS<3.9 and Python<3.9 diff --git a/cms_helper.py b/cms_helper.py index e8ae1cf..47658e3 100755 --- a/cms_helper.py +++ b/cms_helper.py @@ -1,100 +1,106 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -from __future__ import absolute_import, print_function, unicode_literals from tempfile import mkdtemp -from multisite import SiteID +from multisite import SiteID, checks # NOQA -def gettext(s): return s +def gettext(s): + return s # NOQA HELPER_SETTINGS = dict( - ROOT_URLCONF='tests.test_utils.urls1', + ROOT_URLCONF="tests.test_utils.urls1", INSTALLED_APPS=[ - 'multisite', - 'djangocms_text_ckeditor', + "multisite", + "djangocms_text_ckeditor", ], - LANGUAGE_CODE='en', - LANGUAGES=( - ('en', gettext('English')), - ), + LANGUAGE_CODE="en", + LANGUAGES=(("en", gettext("English")),), CMS_LANGUAGES={ 1: [ { - 'code': 'en', - 'name': gettext('English'), - 'public': True, + "code": "en", + "name": gettext("English"), + "public": True, }, ], 2: [ { - 'code': 'en', - 'name': gettext('English'), - 'public': True, + "code": "en", + "name": gettext("English"), + "public": True, }, ], - 'default': { - 'hide_untranslated': False, + "default": { + "hide_untranslated": False, }, }, MIDDLEWARE_CLASSES=( - 'multisite.middleware.DynamicSiteMiddleware', - 'djangocms_multisite.middleware.CMSMultiSiteMiddleware', + "multisite.middleware.DynamicSiteMiddleware", + "djangocms_multisite.middleware.CMSMultiSiteMiddleware", ), MIGRATION_MODULES={}, USE_TZ=True, - TIME_ZONE='UTC', + TIME_ZONE="UTC", FILE_UPLOAD_TEMP_DIR=mkdtemp(), SITE_ID=SiteID(default=1), MULTISITE_CMS_URLS={ - 'www.example.com': 'tests.test_utils.urls1', - 'www.example2.com': 'tests.test_utils.urls2', + "www.example.com": "tests.test_utils.urls1", + "www.example2.com": "tests.test_utils.urls2", }, MULTISITE_CMS_ALIASES={ - 'www.example.com': ('alias1.example.com', 'alias2.example.com',), - 'www.example2.com': ('alias1.example2.com', 'alias2.example2.com', 'alias3.example2.com:8000'), + "www.example.com": ( + "alias1.example.com", + "alias2.example.com", + ), + "www.example2.com": ("alias1.example2.com", "alias2.example2.com", "alias3.example2.com:8000"), }, - MULTISITE_CMS_FALLBACK='www.example.com', - ALLOWED_HOSTS=['*'], + MULTISITE_CMS_FALLBACK="www.example.com", + ALLOWED_HOSTS=["*"], ) try: import djangocms_blog # NOQA - HELPER_SETTINGS['INSTALLED_APPS'].extend([ - 'filer', - 'easy_thumbnails', - 'aldryn_apphooks_config', - 'cmsplugin_filer_image', - 'parler', - 'taggit', - 'taggit_autosuggest', - 'meta', - 'djangocms_blog', - ]) - HELPER_SETTINGS['THUMBNAIL_PROCESSORS'] = ( - 'easy_thumbnails.processors.colorspace', - 'easy_thumbnails.processors.autocrop', - 'filer.thumbnail_processors.scale_and_crop_with_subject_location', - 'easy_thumbnails.processors.filters', + + HELPER_SETTINGS["INSTALLED_APPS"].extend( + [ + "filer", + "easy_thumbnails", + "aldryn_apphooks_config", + "cmsplugin_filer_image", + "parler", + "taggit", + "taggit_autosuggest", + "meta", + "djangocms_blog", + ] + ) + HELPER_SETTINGS["THUMBNAIL_PROCESSORS"] = ( + "easy_thumbnails.processors.colorspace", + "easy_thumbnails.processors.autocrop", + "filer.thumbnail_processors.scale_and_crop_with_subject_location", + "easy_thumbnails.processors.filters", ) - HELPER_SETTINGS['META_SITE_PROTOCOL'] = 'http' - HELPER_SETTINGS['META_USE_SITES'] = True + HELPER_SETTINGS["META_SITE_PROTOCOL"] = "http" + HELPER_SETTINGS["META_USE_SITES"] = True except ImportError: pass def run(): from djangocms_helper import runner - runner.cms('djangocms_multisite') + + runner.cms("djangocms_multisite") def setup(): import sys + from djangocms_helper import runner - runner.setup('djangocms_multisite', sys.modules[__name__], use_cms=True) + + runner.setup("djangocms_multisite", sys.modules[__name__], use_cms=True) -if __name__ == '__main__': +if __name__ == "__main__": run() diff --git a/djangocms_multisite/__init__.py b/djangocms_multisite/__init__.py index e1424ed..260c070 100644 --- a/djangocms_multisite/__init__.py +++ b/djangocms_multisite/__init__.py @@ -1 +1 @@ -__version__ = '0.3.1' +__version__ = "0.3.1" diff --git a/djangocms_multisite/middleware.py b/djangocms_multisite/middleware.py index eac275b..da93dce 100644 --- a/djangocms_multisite/middleware.py +++ b/djangocms_multisite/middleware.py @@ -8,14 +8,13 @@ class CMSMultiSiteMiddleware(MiddlewareMixin): - @staticmethod def _get_sites(): - return getattr(settings, 'MULTISITE_CMS_URLS', {}) + return getattr(settings, "MULTISITE_CMS_URLS", {}) @staticmethod def _get_aliases(): - return getattr(settings, 'MULTISITE_CMS_ALIASES', {}) + return getattr(settings, "MULTISITE_CMS_ALIASES", {}) @classmethod def _get_domain(cls, request): @@ -25,7 +24,7 @@ def _get_domain(cls, request): sites = cls._get_sites() aliases = cls._get_aliases() parsed = urlparse(request.build_absolute_uri()) - host = parsed.hostname.split(':')[0] + host = parsed.hostname.split(":")[0] if host in sites: return host else: @@ -44,16 +43,12 @@ def _get_urlconf(cls, domain): resulting in setting the default urlconf. """ sites = cls._get_sites() - MULTISITE_CMS_FALLBACK = getattr(settings, 'MULTISITE_CMS_FALLBACK', '') # noqa + MULTISITE_CMS_FALLBACK = getattr(settings, "MULTISITE_CMS_FALLBACK", "") # noqa try: urlconf = sites[domain] except KeyError: urlconf = None - if ( - not urlconf and - MULTISITE_CMS_FALLBACK and - MULTISITE_CMS_FALLBACK in sites.keys() - ): + if not urlconf and MULTISITE_CMS_FALLBACK and MULTISITE_CMS_FALLBACK in sites.keys(): urlconf = sites[MULTISITE_CMS_FALLBACK] return urlconf @@ -68,7 +63,7 @@ def process_request(self, request): reload_urlconf() def process_response(self, request, response): - patch_vary_headers(response, ('Host',)) + patch_vary_headers(response, ("Host",)) # set back to default urlconf set_urlconf(None) return response diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8186771 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,48 @@ +[build-system] +requires = ["setuptools>=40.6.0", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.black] +line-length = 119 +target-version = ["py310"] + +[tool.towncrier] +package = "djangocms_multisite" +directory = "changes" +filename = "HISTORY.rst" +title_format = "{version} ({project_date})" + +[tool.interrogate] +ignore-init-method = true +ignore-init-module = true +ignore-magic = false +ignore-semiprivate = false +ignore-private = false +ignore-module = true +ignore-nested-functions = true +fail-under = 0 +exclude = ["docs", ".tox"] +ignore-regex = ["^get$", "^mock_.*", ".*BaseClass.*"] +verbose = 0 +quiet = false +whitelist-regex = [] +color = true + +[tool.isort] +profile = "black" +combine_as_imports = true +default_section = "THIRDPARTY" +force_grid_wrap = 0 +include_trailing_comma = true +known_first_party = "djangocms_multisite" +line_length = 119 +multi_line_output = 3 +use_parentheses = true + +[tool.ruff] +ignore = [] +line-length = 119 +target-version = "py310" + +[tool.ruff.mccabe] +max-complexity = 10 diff --git a/requirements-test.txt b/requirements-test.txt index 2345d03..7cea711 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,6 +1,6 @@ -r requirements.txt -coverage -coveralls +coverage>5 +coveralls>2 mock>=1.0.1 nose>=1.3.0 django-nose>=1.2 diff --git a/setup.cfg b/setup.cfg index a189366..9a1cd59 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,19 +1,48 @@ -[flake8] -exclude = *.egg-info,.git,.settings,.tox,build,dist,docs,requirements,tmp,*migrations*,*south_migrations*,tests,data -max-line-length = 119 - [metadata] -license-file = LICENSE +name = djangocms-multisite +version = attr: djangocms_multisite.__version__ +url = https://github.com/nephila/djangocms-multisite +author = Iacopo Spalletti +author_email = i.spalletti@nephila.digital +description = django-multisite support for django CMS +long_description = file: README.rst, HISTORY.rst +long_description_content_type = text/x-rst +license = BSD +license_file = LICENSE +keywords = djangocms-multisite, django +classifiers = + Development Status :: 5 - Production/Stable + Framework :: Django + Intended Audience :: Developers + License :: OSI Approved :: BSD License + Natural Language :: English + Framework :: Django + Framework :: Django :: 3.2 + Framework :: Django :: 4.1 + Framework :: Django :: 4.2 + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 -[wheel] -universal = 1 +[options] +include_package_data = True +install_requires = + django-multisite + django-cms +setup_requires = + setuptools +packages = djangocms_multisite +python_requires = >=3.7 +test_suite = cms_helper.run +zip_safe = False + +[options.package_data] +* = *.txt, *.rst +djangocms_multisite = *.html *.png *.gif *js *jpg *jpeg *svg *py *mo *po -[isort] -line_length = 119 -skip = migrations, .tox -combine_as_imports = true -default_section = THIRDPARTY -include_trailing_comma = true -known_first_party = djangocms_multisite -multi_line_output = 5 -not_skip = __init__.py +[sdist] +formats = zip + +[bdist_wheel] +universal = 1 diff --git a/setup.py b/setup.py old mode 100755 new mode 100644 index cc8b619..b908cbe --- a/setup.py +++ b/setup.py @@ -1,52 +1,3 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import djangocms_multisite +import setuptools -try: - from setuptools import setup -except ImportError: - from distutils.core import setup - -version = djangocms_multisite.__version__ - - -readme = open('README.rst').read() -history = open('HISTORY.rst').read().replace('.. :changelog:', '') - - -setup( - name='djangocms-multisite', - version=version, - description='django-multisite supporto for django CMS', - long_description=readme + '\n\n' + history, - author='Iacopo Spalletti', - author_email='i.spalletti@nephila.it', - url='https://github.com/nephila/djangocms-multisite', - packages=[ - 'djangocms_multisite', - ], - include_package_data=True, - install_requires=[ - 'django-multisite', - 'django-cms', - ], - license='BSD', - zip_safe=False, - keywords='djangocms-multisite, django', - test_suite='cms_helper.run', - classifiers=[ - 'Development Status :: 3 - Alpha', - 'Framework :: Django', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Natural Language :: English', - 'Framework :: Django', - 'Framework :: Django :: 2.2', - 'Framework :: Django :: 3.0', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - ], -) +setuptools.setup() diff --git a/tasks.py b/tasks.py new file mode 100644 index 0000000..35a5ce9 --- /dev/null +++ b/tasks.py @@ -0,0 +1,122 @@ +import io +import os +import re +import sys +from glob import glob + +from invoke import task + +#: branch prefixes for which some checks are skipped +SPECIAL_BRANCHES = ("master", "develop", "release", "support/4.0.x") + + +@task +def clean(c): + """Remove artifacts and binary files.""" + c.run("python setup.py clean --all") + patterns = ["build", "dist"] + patterns.extend(glob("*.egg*")) + patterns.append("**/*.pyc") + for pattern in patterns: + c.run("rm -rf {}".format(pattern)) + + +@task +def lint(c): + """Run linting tox environments.""" + c.run("tox -epep8,isort,black,pypi-description") + + +@task # NOQA +def format(c): # NOQA + """Run code formatting tasks.""" + c.run("tox -eblacken,isort_format") + + +@task +def towncrier_check(c): # NOQA + """Check towncrier files.""" + output = io.StringIO() + c.run("git branch --contains HEAD", out_stream=output) + skipped_branch_prefix = ["pull/", "develop", "master", "HEAD"] + # cleanup branch names by removing PR-only names in local, remote and disconnected branches to ensure the current + # (i.e. user defined) branch name is used + branches = list( + filter( + lambda x: x and all(not x.startswith(part) for part in skipped_branch_prefix), + ( + branch.replace("origin/", "").replace("remotes/", "").strip("* (") + for branch in output.getvalue().split("\n") + ), + ) + ) + print("Candidate branches", ", ".join(output.getvalue().split("\n"))) + if not branches: + # if no branch name matches, we are in one of the excluded branches above, so we just exit + print("Skip check, branch excluded by configuration") + return + branch = branches[0] + towncrier_file = None + for branch in branches: + if any(branch.startswith(prefix) for prefix in SPECIAL_BRANCHES): + sys.exit(0) + try: + parts = re.search(r"(?P\w+)/\D*(?P\d+)\D*", branch).groups() + towncrier_file = os.path.join("changes", "{1}.{0}".format(*parts)) + if not os.path.exists(towncrier_file) or os.path.getsize(towncrier_file) == 0: + print( + "=========================\n" + "Current tree does not contain the towncrier file {} or file is empty\n" + "please check CONTRIBUTING documentation.\n" + "=========================" + "".format(towncrier_file) + ) + sys.exit(2) + else: + break + except AttributeError: + pass + if not towncrier_file: + print( + "=========================\n" + "Branch {} does not respect the '/(-)-description' format\n" + "=========================\n" + "".format(branch) + ) + sys.exit(1) + + +@task +def test(c): + """Run test in local environment.""" + c.run("python setup.py test") + + +@task +def test_all(c): + """Run all tox environments.""" + c.run("tox") + + +@task +def coverage(c): + """Run test with coverage in local environment.""" + c.run("coverage erase") + c.run("run setup.py test") + c.run("report -m") + + +@task +def tag_release(c, level, new_version=""): + """Tag release version.""" + if new_version: + new_version = f" --new-version {new_version}" + c.run(f"bumpversion --list {level} --no-tag{new_version}") + + +@task +def tag_dev(c, level="patch", new_version=""): + """Tag development version.""" + if new_version: + new_version = f" --new-version {new_version}" + c.run(f"bumpversion --list {level} --message='Bump develop version [ci skip]' --no-tag{new_version}") diff --git a/tests/test_middleware.py b/tests/test_middleware.py index 226b9b9..cca7eee 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -1,3 +1,5 @@ +from unittest import mock + from app_helper.base_test import BaseTestCase from django.contrib.sites.models import Site from django.http import Http404, HttpResponse @@ -14,87 +16,90 @@ class CMSMultiSiteMiddlewareTest(BaseTestCase): def setUp(self): Site.objects.all().delete() - self.site = Site.objects.create(pk=1, domain='www.example.com') - self.site2 = Site.objects.create(pk=2, domain='www.example2.com') + self.site = Site.objects.create(pk=1, domain="www.example.com") + self.site2 = Site.objects.create(pk=2, domain="www.example2.com") def test_match_domain(self): """Resolve the request domain against the list of configured main and aliases.""" - request = RequestFactory(host='www.example.com').get('/') - self.assertEqual(CMSMultiSiteMiddleware._get_domain(request), 'www.example.com') + request = RequestFactory(host="www.example.com").get("/") + self.assertEqual(CMSMultiSiteMiddleware._get_domain(request), "www.example.com") - request = RequestFactory(host='alias1.example.com').get('/') - self.assertEqual(CMSMultiSiteMiddleware._get_domain(request), 'www.example.com') + request = RequestFactory(host="alias1.example.com").get("/") + self.assertEqual(CMSMultiSiteMiddleware._get_domain(request), "www.example.com") - request = RequestFactory(host='alias3.example.com').get('/') + request = RequestFactory(host="alias3.example.com").get("/") self.assertEqual(CMSMultiSiteMiddleware._get_domain(request), None) - request = RequestFactory(host='blabla.com').get('/') + request = RequestFactory(host="blabla.com").get("/") self.assertEqual(CMSMultiSiteMiddleware._get_domain(request), None) - request = RequestFactory(host='www.example2.com').get('/') - self.assertEqual(CMSMultiSiteMiddleware._get_domain(request), 'www.example2.com') + request = RequestFactory(host="www.example2.com").get("/") + self.assertEqual(CMSMultiSiteMiddleware._get_domain(request), "www.example2.com") - request = RequestFactory(host='alias2.example2.com').get('/') - self.assertEqual(CMSMultiSiteMiddleware._get_domain(request), 'www.example2.com') + request = RequestFactory(host="alias2.example2.com").get("/") + self.assertEqual(CMSMultiSiteMiddleware._get_domain(request), "www.example2.com") # port is always ignored, only domain is considered - request = RequestFactory(host='alias2.example2.com:8000').get('/') - self.assertEqual(CMSMultiSiteMiddleware._get_domain(request), 'www.example2.com') + request = RequestFactory(host="alias2.example2.com:8000").get("/") + self.assertEqual(CMSMultiSiteMiddleware._get_domain(request), "www.example2.com") # don't set port in MULTISITE_CMS_ALIASES, otherwise it will not be matched - request = RequestFactory(host='alias3.example2.com:8000').get('/') + request = RequestFactory(host="alias3.example2.com:8000").get("/") self.assertEqual(CMSMultiSiteMiddleware._get_domain(request), None) def test_match_urlconf(self): """Match main domain return the correct one - Any other domain -including alias- return the default.""" - self.assertEqual(CMSMultiSiteMiddleware._get_urlconf('www.example.com'), 'tests.test_utils.urls1') - self.assertEqual(CMSMultiSiteMiddleware._get_urlconf('www.example2.com'), 'tests.test_utils.urls2') - self.assertEqual(CMSMultiSiteMiddleware._get_urlconf('alias1.example.com'), 'tests.test_utils.urls1') - self.assertEqual(CMSMultiSiteMiddleware._get_urlconf('alias2.example2.com'), 'tests.test_utils.urls1') + self.assertEqual(CMSMultiSiteMiddleware._get_urlconf("www.example.com"), "tests.test_utils.urls1") + self.assertEqual(CMSMultiSiteMiddleware._get_urlconf("www.example2.com"), "tests.test_utils.urls2") + self.assertEqual(CMSMultiSiteMiddleware._get_urlconf("alias1.example.com"), "tests.test_utils.urls1") + self.assertEqual(CMSMultiSiteMiddleware._get_urlconf("alias2.example2.com"), "tests.test_utils.urls1") @override_settings(SITE_ID=1) def test_process_site_1(self): - request = RequestFactory(host='www.example.com').get('/') - CMSMultiSiteMiddleware().process_request(request) - self.assertEqual(get_urlconf(), 'tests.test_utils.urls1') + request = RequestFactory(host="www.example.com").get("/") + get_response = mock.MagicMock() + CMSMultiSiteMiddleware(get_response).process_request(request) + self.assertEqual(get_urlconf(), "tests.test_utils.urls1") - request = RequestFactory(host='alias1.example.com').get('/') - CMSMultiSiteMiddleware().process_request(request) - self.assertEqual(get_urlconf(), 'tests.test_utils.urls1') + request = RequestFactory(host="alias1.example.com").get("/") + CMSMultiSiteMiddleware(get_response).process_request(request) + self.assertEqual(get_urlconf(), "tests.test_utils.urls1") @override_settings(SITE_ID=2) def test_process_site_2(self): - request = RequestFactory(host='www.example2.com').get('/') - CMSMultiSiteMiddleware().process_request(request) - self.assertEqual(get_urlconf(), 'tests.test_utils.urls2') + request = RequestFactory(host="www.example2.com").get("/") + get_response = mock.MagicMock() + CMSMultiSiteMiddleware(get_response).process_request(request) + self.assertEqual(get_urlconf(), "tests.test_utils.urls2") - request = RequestFactory(host='alias2.example2.com').get('/') - CMSMultiSiteMiddleware().process_request(request) - self.assertEqual(get_urlconf(), 'tests.test_utils.urls2') + request = RequestFactory(host="alias2.example2.com").get("/") + CMSMultiSiteMiddleware(get_response).process_request(request) + self.assertEqual(get_urlconf(), "tests.test_utils.urls2") # don't set port in MULTISITE_CMS_ALIASES, otherwise it will not be matched - request = RequestFactory(host='alias3.example2.com').get('/') - CMSMultiSiteMiddleware().process_request(request) - self.assertEqual(get_urlconf(), 'tests.test_utils.urls1') + request = RequestFactory(host="alias3.example2.com").get("/") + CMSMultiSiteMiddleware(get_response).process_request(request) + self.assertEqual(get_urlconf(), "tests.test_utils.urls1") # don't set port in MULTISITE_CMS_ALIASES, otherwise it will not be matched - request = RequestFactory(host='alias3.example2.com:8000').get('/') - CMSMultiSiteMiddleware().process_request(request) - self.assertEqual(get_urlconf(), 'tests.test_utils.urls1') + request = RequestFactory(host="alias3.example2.com:8000").get("/") + CMSMultiSiteMiddleware(get_response).process_request(request) + self.assertEqual(get_urlconf(), "tests.test_utils.urls1") @override_settings(SITE_ID=2) def test_process_reponse(self): - request = RequestFactory(host='www.example2.com').get('/') - CMSMultiSiteMiddleware().process_request(request) - self.assertEqual(get_urlconf(), 'tests.test_utils.urls2') - CMSMultiSiteMiddleware().process_response(request, HttpResponse('')) + request = RequestFactory(host="www.example2.com").get("/") + get_response = mock.MagicMock() + CMSMultiSiteMiddleware(get_response).process_request(request) + self.assertEqual(get_urlconf(), "tests.test_utils.urls2") + CMSMultiSiteMiddleware(get_response).process_response(request, HttpResponse("")) # Default is restored after request is processed self.assertIsNone(get_urlconf()) - request = RequestFactory(host='alias2.example2.com').get('/') - CMSMultiSiteMiddleware().process_request(request) - self.assertEqual(get_urlconf(), 'tests.test_utils.urls2') - CMSMultiSiteMiddleware().process_response(request, HttpResponse('')) + request = RequestFactory(host="alias2.example2.com").get("/") + CMSMultiSiteMiddleware(get_response).process_request(request) + self.assertEqual(get_urlconf(), "tests.test_utils.urls2") + CMSMultiSiteMiddleware(get_response).process_response(request, HttpResponse("")) # Default is restored after request is processed self.assertIsNone(get_urlconf()) @@ -102,37 +107,39 @@ def test_process_reponse(self): class CMSMultiSiteMiddlewareAliasTest(BaseTestCase): def setUp(self): Site.objects.all().delete() - self.site = Site.objects.create(pk=1, domain='www.example.com') - self.site2 = Site.objects.create(pk=2, domain='www.example2.com') - Alias.objects.create(domain='alias1.example.com', site=self.site) - Alias.objects.create(domain='alias2.example.com', site=self.site, redirect_to_canonical=True) + self.site = Site.objects.create(pk=1, domain="www.example.com") + self.site2 = Site.objects.create(pk=2, domain="www.example2.com") + Alias.objects.create(domain="alias1.example.com", site=self.site) + Alias.objects.create(domain="alias2.example.com", site=self.site, redirect_to_canonical=True) - Alias.objects.create(domain='alias1.example2.com', site=self.site2) - Alias.objects.create(domain='alias2.example2.com', site=self.site2, redirect_to_canonical=True) + Alias.objects.create(domain="alias1.example2.com", site=self.site2) + Alias.objects.create(domain="alias2.example2.com", site=self.site2, redirect_to_canonical=True) def test_process_site_1(self): - request = RequestFactory(host='www.example.com').get('/') - DynamicSiteMiddleware().process_request(request) - CMSMultiSiteMiddleware().process_request(request) - self.assertEqual(get_urlconf(), 'tests.test_utils.urls1') + request = RequestFactory(host="www.example.com").get("/") + get_response = mock.MagicMock() + DynamicSiteMiddleware(get_response).process_request(request) + CMSMultiSiteMiddleware(get_response).process_request(request) + self.assertEqual(get_urlconf(), "tests.test_utils.urls1") - request = RequestFactory(host='alias1.example.com').get('/') - DynamicSiteMiddleware().process_request(request) - CMSMultiSiteMiddleware().process_request(request) - self.assertEqual(get_urlconf(), 'tests.test_utils.urls1') + request = RequestFactory(host="alias1.example.com").get("/") + DynamicSiteMiddleware(get_response).process_request(request) + CMSMultiSiteMiddleware(get_response).process_request(request) + self.assertEqual(get_urlconf(), "tests.test_utils.urls1") def test_process_site_2(self): - request = RequestFactory(host='www.example2.com').get('/') - DynamicSiteMiddleware().process_request(request) - CMSMultiSiteMiddleware().process_request(request) - self.assertEqual(get_urlconf(), 'tests.test_utils.urls2') + request = RequestFactory(host="www.example2.com").get("/") + get_response = mock.MagicMock() + DynamicSiteMiddleware(get_response).process_request(request) + CMSMultiSiteMiddleware(get_response).process_request(request) + self.assertEqual(get_urlconf(), "tests.test_utils.urls2") - request = RequestFactory(host='alias2.example2.com').get('/') - DynamicSiteMiddleware().process_request(request) - CMSMultiSiteMiddleware().process_request(request) - self.assertEqual(get_urlconf(), 'tests.test_utils.urls2') + request = RequestFactory(host="alias2.example2.com").get("/") + DynamicSiteMiddleware(get_response).process_request(request) + CMSMultiSiteMiddleware(get_response).process_request(request) + self.assertEqual(get_urlconf(), "tests.test_utils.urls2") # aliases not configured on django-multisite will not be recognizes - request = RequestFactory(host='alias3.example2.com').get('/') + request = RequestFactory(host="alias3.example2.com").get("/") with self.assertRaises(Http404): - DynamicSiteMiddleware().process_request(request) + DynamicSiteMiddleware(get_response).process_request(request) diff --git a/tests/test_utils/urls1.py b/tests/test_utils/urls1.py index 540a416..3e46fa7 100644 --- a/tests/test_utils/urls1.py +++ b/tests/test_utils/urls1.py @@ -1,25 +1,25 @@ from cms.utils.conf import get_cms_setting from django.conf import settings -from django.conf.urls import include, url from django.conf.urls.i18n import i18n_patterns from django.contrib import admin from django.contrib.staticfiles.urls import staticfiles_urlpatterns +from django.urls import include, path, re_path from django.views.i18n import JavaScriptCatalog from django.views.static import serve admin.autodiscover() urlpatterns = [ - url(r'^media/(?P.*)$', serve, - {'document_root': settings.MEDIA_ROOT, 'show_indexes': True}), - url(r'^media/cms/(?P.*)$', serve, - {'document_root': get_cms_setting('MEDIA_ROOT'), 'show_indexes': True}), - url(r'^jsi18n/$', JavaScriptCatalog.as_view(), name='javascript-catalog'), + re_path(r"^media/(?P.*)$", serve, {"document_root": settings.MEDIA_ROOT, "show_indexes": True}), + re_path( + r"^media/cms/(?P.*)$", serve, {"document_root": get_cms_setting("MEDIA_ROOT"), "show_indexes": True} + ), + path("jsi18n/", JavaScriptCatalog.as_view(), name="javascript-catalog"), ] urlpatterns += staticfiles_urlpatterns() urlpatterns += i18n_patterns( - url(r'^admin/', admin.site.urls), - url(r'^', include('cms.urls')), + re_path(r"^admin/", admin.site.urls), + path("", include("cms.urls")), ) diff --git a/tests/test_utils/urls2.py b/tests/test_utils/urls2.py index 6efc265..480b26b 100644 --- a/tests/test_utils/urls2.py +++ b/tests/test_utils/urls2.py @@ -1,25 +1,25 @@ from cms.utils.conf import get_cms_setting from django.conf import settings -from django.conf.urls import include, url from django.conf.urls.i18n import i18n_patterns from django.contrib import admin from django.contrib.staticfiles.urls import staticfiles_urlpatterns +from django.urls import include, path, re_path from django.views.i18n import JavaScriptCatalog from django.views.static import serve admin.autodiscover() urlpatterns = [ - url(r'^media/(?P.*)$', serve, - {'document_root': settings.MEDIA_ROOT, 'show_indexes': True}), - url(r'^media/cms/(?P.*)$', serve, - {'document_root': get_cms_setting('MEDIA_ROOT'), 'show_indexes': True}), - url(r'^jsi18n/$', JavaScriptCatalog.as_view(), name='javascript-catalog'), + re_path(r"^media/(?P.*)$", serve, {"document_root": settings.MEDIA_ROOT, "show_indexes": True}), + re_path( + r"^media/cms/(?P.*)$", serve, {"document_root": get_cms_setting("MEDIA_ROOT"), "show_indexes": True} + ), + path("jsi18n/", JavaScriptCatalog.as_view(), name="javascript-catalog"), ] urlpatterns += staticfiles_urlpatterns() urlpatterns += i18n_patterns( - url(r'^admin/', admin.site.urls), - url(r'^subpath/', include('cms.urls')), + re_path(r"^admin/", admin.site.urls), + path("subpath/", include("cms.urls")), ) diff --git a/tests/utils.py b/tests/utils.py index 0c97ed1..680166a 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -2,13 +2,12 @@ class RequestFactory(DjangoRequestFactory): - # borrowed from django-multisite, we can't import it directly due to pytest upstream dependencies def __init__(self, host): - super(RequestFactory, self).__init__() + super().__init__() self.host = host def get(self, path, data=None, host=None, **extra): if host is None: host = self.host - return super(RequestFactory, self).get(path=path, data=data, HTTP_HOST=host, **extra) + return super().get(path=path, data=data, HTTP_HOST=host, **extra) diff --git a/tox.ini b/tox.ini index 3b9d951..c1a9802 100644 --- a/tox.ini +++ b/tox.ini @@ -1,32 +1,117 @@ [tox] -envlist = pep8,isort,py{38,37,36,35}-django{30,22}-cms{37} +envlist = + black + blacken + docs + isort + isort_format + ruff + pypi-description + towncrier + py{311,310,39}-django{42,41}-cms{311} + py{311,310,39}-django{32}-cms{311,39} +minversion = 3.22 [testenv] -commands = {env:COMMAND:python} cms_helper.py +commands = {env:COMMAND:python} cms_helper.py djangocms_multisite test {posargs} deps = - django30: Django>=3.0,<3.1 - django22: Django>=2.2,<3.0 - cms37: https://github.com/divio/django-cms/archive/release/3.7.x.zip - cms37: djangocms-text-ckeditor>=3.5 + django32: Django~=3.2.0 + django41: Django~=4.1.0 + django42: Django~=4.2.0 + cms39: https://github.com/django-cms/django-cms/archive/release/3.9.x.zip + cms311: https://github.com/yakky/django-cms/archive/release/3.11.x.zip + djangocms-text-ckeditor>=4 + https://github.com/protoroto/django-multisite/archive/refs/heads/feature/add-django4-support.zip -r{toxinidir}/requirements-test.txt +passenv = + COMMAND + PYTEST_* +[testenv:ruff] +commands = + {envpython} -m ruff check djangocms_multisite tests {posargs} + {envpython} -minterrogate -c pyproject.toml djangocms_multisite tests +deps = + interrogate + ruff +skip_install = true [testenv:isort] -deps = isort<5.0 -commands = isort -c -rc -df +commands = + {envpython} -m isort -c --df djangocms_multisite tests +deps = isort>=5.12.0,<5.13.0 +skip_install = true + +[testenv:isort_format] +commands = + {envpython} -m isort djangocms_multisite tests +deps = {[testenv:isort]deps} skip_install = true -[testenv:pep8] -deps = flake8 -commands = flake8 djangocms_multisite tests +[testenv:black] +commands = + {envpython} -m black --check --diff . +deps = black skip_install = true -[testenv:docs] +[testenv:blacken] +commands = + {envpython} -m black . +deps = {[testenv:black]deps} +skip_install = true + +[testenv:towncrier] +commands = + {envpython} -m invoke towncrier-check deps = - sphinx - sphinx-rtd-theme - -rrequirements-test.txt -changedir=docs + invoke skip_install = true -commands= - sphinx-build -W -b html -d {envtmpdir}/doctrees . {toxinidir}/docs/_build/html + +[testenv:pypi-description] +commands = + {envpython} -m invoke clean + {envpython} -m check_manifest + {envpython} -m build . + {envpython} -m twine check dist/* +deps = + invoke + check-manifest + build + twine +skip_install = true + +[testenv:release] +commands = + {envpython} -m invoke clean + {envpython} -m check_manifest + {envpython} -m build . + {envpython} -m twine upload {posargs} dist/* +deps = {[testenv:pypi-description]deps} +passenv = + TWINE_* +skip_install = true + +[check-manifest] +ignore = + .* + *.ini + *.toml + *.json + *.txt + *.yml + *.yaml + .tx/** + changes/** + cms_helper.py + aldryn_config.py + tasks.py + tests/** + *.mo +ignore-bad-ideas = + *.mo + +[pytest] +DJANGO_SETTINGS_MODULE = cms_helper +python_files = test_*.py +traceback = short +addopts = --reuse-db From a93af22cdc2f314c7f88e620a30d7be8f6a8647d Mon Sep 17 00:00:00 2001 From: Iacopo Spalletti Date: Sun, 21 Jun 2026 11:32:01 +0200 Subject: [PATCH 02/25] Fix thread-safety: replace MiddlewareMixin with try/finally __call__ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Drop MiddlewareMixin in favour of a plain new-style middleware so that set_urlconf(None) is guaranteed via try/finally even when a BaseException (SystemExit, KeyboardInterrupt, …) escapes Django's convert_exception_to_response wrapper, which only catches Exception. - Move reload_urlconf() before set_urlconf(urlconf) so that any set_urlconf call that reload_urlconf() may issue internally cannot override the site-specific value we are about to set. - Update tests: replace direct process_request/process_response calls with the __call__ contract; add test_urlconf_restored_after_exception to assert the try/finally guarantee against BaseException. --- djangocms_multisite/middleware.py | 29 +++++--- tests/test_middleware.py | 120 +++++++++++++++--------------- 2 files changed, 78 insertions(+), 71 deletions(-) diff --git a/djangocms_multisite/middleware.py b/djangocms_multisite/middleware.py index da93dce..ba5c85e 100644 --- a/djangocms_multisite/middleware.py +++ b/djangocms_multisite/middleware.py @@ -4,10 +4,12 @@ from django.conf import settings from django.urls import set_urlconf from django.utils.cache import patch_vary_headers -from django.utils.deprecation import MiddlewareMixin -class CMSMultiSiteMiddleware(MiddlewareMixin): +class CMSMultiSiteMiddleware: + def __init__(self, get_response): + self.get_response = get_response + @staticmethod def _get_sites(): return getattr(settings, "MULTISITE_CMS_URLS", {}) @@ -52,18 +54,21 @@ def _get_urlconf(cls, domain): urlconf = sites[MULTISITE_CMS_FALLBACK] return urlconf - def process_request(self, request): + def __call__(self, request): domain = self._get_domain(request) urlconf = self._get_urlconf(domain) - # sets urlconf for current thread, so that code that does not know - # about the request (e.g MyModel.get_absolute_url()) get the correct - # urlconf. - # urlconf might be None, in that case, the default is set - set_urlconf(urlconf) + # Reload URL patterns before setting the thread-local urlconf so that + # any set_urlconf call inside reload_urlconf cannot override ours. reload_urlconf() - - def process_response(self, request, response): + # Sets the thread-local urlconf so code outside the request/response + # cycle (e.g. Model.get_absolute_url()) resolves URLs against the + # correct site configuration. urlconf may be None, restoring the default. + set_urlconf(urlconf) + try: + response = self.get_response(request) + finally: + # Guaranteed cleanup: resets the thread-local even if a BaseException + # (e.g. SystemExit, KeyboardInterrupt) bypasses Django's exception handler. + set_urlconf(None) patch_vary_headers(response, ("Host",)) - # set back to default urlconf - set_urlconf(None) return response diff --git a/tests/test_middleware.py b/tests/test_middleware.py index cca7eee..a9a725b 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -54,53 +54,52 @@ def test_match_urlconf(self): self.assertEqual(CMSMultiSiteMiddleware._get_urlconf("alias1.example.com"), "tests.test_utils.urls1") self.assertEqual(CMSMultiSiteMiddleware._get_urlconf("alias2.example2.com"), "tests.test_utils.urls1") + def _get_urlconf_during_request(self, host): + """Run the middleware and return the urlconf observed inside get_response.""" + captured = [] + + def get_response(request): + captured.append(get_urlconf()) + return HttpResponse("") + + CMSMultiSiteMiddleware(get_response)(RequestFactory(host=host).get("/")) + return captured[0] + @override_settings(SITE_ID=1) def test_process_site_1(self): - request = RequestFactory(host="www.example.com").get("/") - get_response = mock.MagicMock() - CMSMultiSiteMiddleware(get_response).process_request(request) - self.assertEqual(get_urlconf(), "tests.test_utils.urls1") - - request = RequestFactory(host="alias1.example.com").get("/") - CMSMultiSiteMiddleware(get_response).process_request(request) - self.assertEqual(get_urlconf(), "tests.test_utils.urls1") + self.assertEqual(self._get_urlconf_during_request("www.example.com"), "tests.test_utils.urls1") + self.assertEqual(self._get_urlconf_during_request("alias1.example.com"), "tests.test_utils.urls1") @override_settings(SITE_ID=2) def test_process_site_2(self): - request = RequestFactory(host="www.example2.com").get("/") - get_response = mock.MagicMock() - CMSMultiSiteMiddleware(get_response).process_request(request) - self.assertEqual(get_urlconf(), "tests.test_utils.urls2") - - request = RequestFactory(host="alias2.example2.com").get("/") - CMSMultiSiteMiddleware(get_response).process_request(request) - self.assertEqual(get_urlconf(), "tests.test_utils.urls2") + self.assertEqual(self._get_urlconf_during_request("www.example2.com"), "tests.test_utils.urls2") + self.assertEqual(self._get_urlconf_during_request("alias2.example2.com"), "tests.test_utils.urls2") # don't set port in MULTISITE_CMS_ALIASES, otherwise it will not be matched - request = RequestFactory(host="alias3.example2.com").get("/") - CMSMultiSiteMiddleware(get_response).process_request(request) - self.assertEqual(get_urlconf(), "tests.test_utils.urls1") - - # don't set port in MULTISITE_CMS_ALIASES, otherwise it will not be matched - request = RequestFactory(host="alias3.example2.com:8000").get("/") - CMSMultiSiteMiddleware(get_response).process_request(request) - self.assertEqual(get_urlconf(), "tests.test_utils.urls1") + self.assertEqual(self._get_urlconf_during_request("alias3.example2.com"), "tests.test_utils.urls1") + self.assertEqual(self._get_urlconf_during_request("alias3.example2.com:8000"), "tests.test_utils.urls1") @override_settings(SITE_ID=2) - def test_process_reponse(self): - request = RequestFactory(host="www.example2.com").get("/") - get_response = mock.MagicMock() - CMSMultiSiteMiddleware(get_response).process_request(request) - self.assertEqual(get_urlconf(), "tests.test_utils.urls2") - CMSMultiSiteMiddleware(get_response).process_response(request, HttpResponse("")) - # Default is restored after request is processed - self.assertIsNone(get_urlconf()) - - request = RequestFactory(host="alias2.example2.com").get("/") - CMSMultiSiteMiddleware(get_response).process_request(request) - self.assertEqual(get_urlconf(), "tests.test_utils.urls2") - CMSMultiSiteMiddleware(get_response).process_response(request, HttpResponse("")) - # Default is restored after request is processed + def test_urlconf_restored_after_request(self): + """urlconf is reset to None after each completed request.""" + for host in ("www.example2.com", "alias2.example2.com"): + CMSMultiSiteMiddleware(lambda r: HttpResponse(""))(RequestFactory(host=host).get("/")) + self.assertIsNone(get_urlconf(), msg=f"urlconf not cleaned up for host {host}") + + def test_urlconf_restored_after_exception(self): + """urlconf is reset to None even when a BaseException escapes the inner handler.""" + captured_inside = [] + + def raise_system_exit(request): + captured_inside.append(get_urlconf()) + raise SystemExit("simulated crash") + + with self.assertRaises(SystemExit): + CMSMultiSiteMiddleware(raise_system_exit)(RequestFactory(host="www.example.com").get("/")) + + # The urlconf was set during the request + self.assertEqual(captured_inside[0], "tests.test_utils.urls1") + # And was cleaned up despite the BaseException self.assertIsNone(get_urlconf()) @@ -115,31 +114,34 @@ def setUp(self): Alias.objects.create(domain="alias1.example2.com", site=self.site2) Alias.objects.create(domain="alias2.example2.com", site=self.site2, redirect_to_canonical=True) - def test_process_site_1(self): - request = RequestFactory(host="www.example.com").get("/") - get_response = mock.MagicMock() - DynamicSiteMiddleware(get_response).process_request(request) - CMSMultiSiteMiddleware(get_response).process_request(request) - self.assertEqual(get_urlconf(), "tests.test_utils.urls1") + def _get_urlconf_during_request(self, host): + """ + Simulate DynamicSiteMiddleware + CMSMultiSiteMiddleware in sequence + and return the urlconf observed inside get_response. + """ + captured = [] + mock_get_response = mock.MagicMock(return_value=HttpResponse("")) - request = RequestFactory(host="alias1.example.com").get("/") - DynamicSiteMiddleware(get_response).process_request(request) - CMSMultiSiteMiddleware(get_response).process_request(request) - self.assertEqual(get_urlconf(), "tests.test_utils.urls1") + request = RequestFactory(host=host).get("/") + DynamicSiteMiddleware(mock_get_response).process_request(request) - def test_process_site_2(self): - request = RequestFactory(host="www.example2.com").get("/") - get_response = mock.MagicMock() - DynamicSiteMiddleware(get_response).process_request(request) - CMSMultiSiteMiddleware(get_response).process_request(request) - self.assertEqual(get_urlconf(), "tests.test_utils.urls2") + def capture_get_response(req): + captured.append(get_urlconf()) + return HttpResponse("") - request = RequestFactory(host="alias2.example2.com").get("/") - DynamicSiteMiddleware(get_response).process_request(request) - CMSMultiSiteMiddleware(get_response).process_request(request) - self.assertEqual(get_urlconf(), "tests.test_utils.urls2") + CMSMultiSiteMiddleware(capture_get_response)(request) + return captured[0] + + def test_process_site_1(self): + self.assertEqual(self._get_urlconf_during_request("www.example.com"), "tests.test_utils.urls1") + self.assertEqual(self._get_urlconf_during_request("alias1.example.com"), "tests.test_utils.urls1") + + def test_process_site_2(self): + self.assertEqual(self._get_urlconf_during_request("www.example2.com"), "tests.test_utils.urls2") + self.assertEqual(self._get_urlconf_during_request("alias2.example2.com"), "tests.test_utils.urls2") - # aliases not configured on django-multisite will not be recognizes + # aliases not configured on django-multisite will not be recognized request = RequestFactory(host="alias3.example2.com").get("/") + get_response = mock.MagicMock(return_value=HttpResponse("")) with self.assertRaises(Http404): DynamicSiteMiddleware(get_response).process_request(request) From e7f3fe2f10283ffbffa77ea82e9cce788057a25e Mon Sep 17 00:00:00 2001 From: Iacopo Spalletti Date: Sun, 21 Jun 2026 11:40:31 +0200 Subject: [PATCH 03/25] Drop support for Python <3.11, Django <4.2, Django CMS <3.11 - tox.ini: envlist now py{311,312,313}-django{42,52}-cms{311}; remove django32/41/cms39 deps; add django52 dep - setup.cfg: python_requires >=3.11; classifiers updated to Django 4.2/5.0/5.1/5.2 and Python 3.11/3.12/3.13 - .github/workflows/test.yml: matrix updated to Python 3.11/3.12/3.13, Django 42/52, CMS 311; py313+django42 marked continue-on-error; action versions bumped to v4/v5 - .github/workflows/lint.yml: Python bumped to 3.12; action versions bumped to v4/v5 - pyproject.toml: black and ruff target-version updated to py311 - requirements-test.txt: remove mock, nose, django-nose, flake8 (dead packages replaced by stdlib unittest.mock and ruff) - cms_helper.py: rename MIDDLEWARE_CLASSES to MIDDLEWARE (removed in Django 2.0) --- .github/workflows/lint.yml | 10 +++++----- .github/workflows/test.yml | 28 ++++++++++++++-------------- cms_helper.py | 2 +- pyproject.toml | 4 ++-- requirements-test.txt | 6 +----- setup.cfg | 14 +++++++------- tox.ini | 7 ++----- 7 files changed, 32 insertions(+), 39 deletions(-) mode change 100755 => 100644 cms_helper.py diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 701b10e..b035dd7 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -8,26 +8,26 @@ jobs: if: "!contains(github.event.head_commit.message, '[skip ci]')" strategy: matrix: - python-version: ["3.11.x"] + python-version: ["3.12"] toxenv: [ruff, isort, black, pypi-description, docs, towncrier] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: repository: ${{ github.event.pull_request.head.repo.full_name }} ref: ${{ github.event.pull_request.head.ref }} - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Cache pip - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ matrix.toxenv }} restore-keys: | ${{ runner.os }}-pip-${{ matrix.toxenv }} - name: Cache tox - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: .tox key: ${{ runner.os }}-lint-${{ matrix.toxenv }}-${{ hashFiles('setup.cfg') }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8bc4fe0..caa6e0d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,30 +9,30 @@ jobs: continue-on-error: ${{ matrix.continue-on-error }} strategy: matrix: - python-version: ["3.11", "3.10", "3.9"] - django: [42, 41, 32] - cms: [311, 39] - continue-on-error: [true] - exclude: - - django: 41 - cms: 39 - - django: 42 - cms: 39 + python-version: ["3.11", "3.12", "3.13"] + django: [42, 52] + cms: [311] + continue-on-error: [false] + include: + - python-version: "3.13" + django: 42 + cms: 311 + continue-on-error: true steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Cache pip - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ matrix.toxenv }} restore-keys: | ${{ runner.os }}-pip-${{ matrix.toxenv }} - name: Cache tox - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: .tox key: ${{ runner.os }}-tox-${{ format('{{py{0}-django{1}-cms{2}}}', matrix.python-version, matrix.django, matrix.cms) }}-${{ hashFiles('setup.cfg') }} @@ -52,7 +52,7 @@ jobs: tox -e$TOX_ENV .tox/$TOX_ENV/bin/coverage xml .tox/$TOX_ENV/bin/coveralls - - uses: codecov/codecov-action@v3 + - uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} flags: unittests diff --git a/cms_helper.py b/cms_helper.py old mode 100755 new mode 100644 index 47658e3..c4150eb --- a/cms_helper.py +++ b/cms_helper.py @@ -36,7 +36,7 @@ def gettext(s): "hide_untranslated": False, }, }, - MIDDLEWARE_CLASSES=( + MIDDLEWARE=( "multisite.middleware.DynamicSiteMiddleware", "djangocms_multisite.middleware.CMSMultiSiteMiddleware", ), diff --git a/pyproject.toml b/pyproject.toml index 8186771..87155d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [tool.black] line-length = 119 -target-version = ["py310"] +target-version = ["py311"] [tool.towncrier] package = "djangocms_multisite" @@ -42,7 +42,7 @@ use_parentheses = true [tool.ruff] ignore = [] line-length = 119 -target-version = "py310" +target-version = "py311" [tool.ruff.mccabe] max-complexity = 10 diff --git a/requirements-test.txt b/requirements-test.txt index 7cea711..7d2bbab 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,10 +1,6 @@ -r requirements.txt coverage>5 coveralls>2 -mock>=1.0.1 -nose>=1.3.0 -django-nose>=1.2 -flake8 django-app-helper -tox>=2.0 +tox>=4.0 wheel diff --git a/setup.cfg b/setup.cfg index 9a1cd59..8ee686b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,24 +16,24 @@ classifiers = Intended Audience :: Developers License :: OSI Approved :: BSD License Natural Language :: English - Framework :: Django - Framework :: Django :: 3.2 - Framework :: Django :: 4.1 Framework :: Django :: 4.2 + Framework :: Django :: 5.0 + Framework :: Django :: 5.1 + Framework :: Django :: 5.2 Programming Language :: Python :: 3 - 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 [options] include_package_data = True install_requires = django-multisite - django-cms + django-cms setup_requires = setuptools packages = djangocms_multisite -python_requires = >=3.7 +python_requires = >=3.11 test_suite = cms_helper.run zip_safe = False diff --git a/tox.ini b/tox.ini index c1a9802..ce02591 100644 --- a/tox.ini +++ b/tox.ini @@ -8,17 +8,14 @@ envlist = ruff pypi-description towncrier - py{311,310,39}-django{42,41}-cms{311} - py{311,310,39}-django{32}-cms{311,39} + py{311,312,313}-django{42,52}-cms{311} minversion = 3.22 [testenv] commands = {env:COMMAND:python} cms_helper.py djangocms_multisite test {posargs} deps = - django32: Django~=3.2.0 - django41: Django~=4.1.0 django42: Django~=4.2.0 - cms39: https://github.com/django-cms/django-cms/archive/release/3.9.x.zip + django52: Django~=5.2.0 cms311: https://github.com/yakky/django-cms/archive/release/3.11.x.zip djangocms-text-ckeditor>=4 https://github.com/protoroto/django-multisite/archive/refs/heads/feature/add-django4-support.zip From 36771ccab22050951706d28c334b70e6ac28b8b2 Mon Sep 17 00:00:00 2001 From: Iacopo Spalletti Date: Sun, 21 Jun 2026 11:40:38 +0200 Subject: [PATCH 04/25] Remove obsolete .travis.yml and .gitlab-ci.yml Travis CI and the old GitLab CI config referenced Python 2.7/3.5-3.8 and Django 1.x/2.x. GitHub Actions is the active CI. --- .travis.yml | 64 ----------------------------------------------------- 1 file changed, 64 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 1667ff6..0000000 --- a/.travis.yml +++ /dev/null @@ -1,64 +0,0 @@ -# Config file for automatic testing at travis-ci.org - -language: python - -sudo: false - -python: - - 3.8 - - 3.7 - - 3.6 - - 3.5 - -env: - matrix: - - TOXENV='pep8' - - TOXENV='isort' - # - TOXENV='docs' docs currently not included - - DJANGO='django30' CMS='cms37' - - DJANGO='django22' CMS='cms37' - - -# command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors -install: - - "if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then export PYVER=py35; fi" - - "if [[ $TRAVIS_PYTHON_VERSION == '3.6' ]]; then export PYVER=py36; fi" - - "if [[ $TRAVIS_PYTHON_VERSION == '3.7' ]]; then export PYVER=py37; fi" - - "if [[ $TRAVIS_PYTHON_VERSION == '3.8' ]]; then export PYVER=py38; fi" - - "if [[ ${DJANGO}z != 'z' ]]; then export TOXENV=$PYVER-$DJANGO-$CMS; fi" - -# command to run tests, e.g. python setup.py test -script: COMMAND='coverage run' tox -e$TOXENV - -before_install: - - pip install -U tox>=1.8 coveralls codecov wheel pip -after_success: - - codecov - - coveralls - -matrix: - exclude: - - python: 3.7 - env: TOXENV='docs' - - python: 3.7 - env: TOXENV='pep8' - - python: 3.7 - env: TOXENV='isort' - - python: 3.6 - env: TOXENV='docs' - - python: 3.6 - env: TOXENV='pep8' - - python: 3.6 - env: TOXENV='isort' - - python: 3.5 - env: TOXENV='docs' - - python: 3.5 - env: TOXENV='pep8' - - python: 3.5 - env: TOXENV='isort' - - python: 3.5 - env: DJANGO='django30' CMS='cms37' - -cache: - directories: - - $HOME/.cache/pip From 8f409cfb63b8ebaf686dc31879ba2db46f87718c Mon Sep 17 00:00:00 2001 From: Iacopo Spalletti Date: Sun, 21 Jun 2026 11:40:40 +0200 Subject: [PATCH 05/25] Remove obsolete .travis.yml and .gitlab-ci.yml Travis CI and the old GitLab CI config referenced Python 2.7/3.5-3.8 and Django 1.x/2.x. GitHub Actions is the active CI. --- .gitlab-ci.yml | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .gitlab-ci.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index eb0dc25..0000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,5 +0,0 @@ -# This file is generated by GitLab CI -ci: - script: - - COMMAND="coverage run" tox -epep8,isort,py35-django19-cms32,py34-django18-cms31,py27-django16-cms30 - - if [[ $? -eq 0 ]]; then coverage report; fi From 6a28e4c494d45fad899ba5df390b3ba8349136ad Mon Sep 17 00:00:00 2001 From: Iacopo Spalletti Date: Sun, 21 Jun 2026 12:11:21 +0200 Subject: [PATCH 06/25] Fix: remove MIDDLEWARE from HELPER_SETTINGS in cms_helper.py MIDDLEWARE_CLASSES was ignored by Django since 2.0, so app_helper was supplying the full default middleware stack. Renaming it to MIDDLEWARE overrode that default with only the two multisite entries, dropping the session/auth/messages middleware required by Django admin. Since all tests instantiate CMSMultiSiteMiddleware directly (never via Django's WSGI handler), this setting has always been dead configuration. Remove it and let app_helper manage the full middleware stack. --- cms_helper.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cms_helper.py b/cms_helper.py index c4150eb..07956be 100644 --- a/cms_helper.py +++ b/cms_helper.py @@ -36,10 +36,6 @@ def gettext(s): "hide_untranslated": False, }, }, - MIDDLEWARE=( - "multisite.middleware.DynamicSiteMiddleware", - "djangocms_multisite.middleware.CMSMultiSiteMiddleware", - ), MIGRATION_MODULES={}, USE_TZ=True, TIME_ZONE="UTC", From 8d22dae545868a71564963c711110446624e563b Mon Sep 17 00:00:00 2001 From: Iacopo Spalletti Date: Sun, 21 Jun 2026 12:17:27 +0200 Subject: [PATCH 07/25] Switch dependency from django-multisite to django-multisite2 Replace the protoroto GitHub zip URL in tox.ini and the django-multisite install_requires entry in setup.cfg with the django-multisite2 PyPI package. The multisite Python module name is unchanged. --- setup.cfg | 2 +- tox.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 8ee686b..52f3486 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,7 +28,7 @@ classifiers = [options] include_package_data = True install_requires = - django-multisite + django-multisite2 django-cms setup_requires = setuptools diff --git a/tox.ini b/tox.ini index ce02591..53be9c3 100644 --- a/tox.ini +++ b/tox.ini @@ -18,7 +18,7 @@ deps = django52: Django~=5.2.0 cms311: https://github.com/yakky/django-cms/archive/release/3.11.x.zip djangocms-text-ckeditor>=4 - https://github.com/protoroto/django-multisite/archive/refs/heads/feature/add-django4-support.zip + django-multisite2 -r{toxinidir}/requirements-test.txt passenv = COMMAND From 3eae6c14639c176208155ed79261115a0a4e0c4c Mon Sep 17 00:00:00 2001 From: Iacopo Spalletti Date: Sun, 21 Jun 2026 12:21:06 +0200 Subject: [PATCH 08/25] Fix cms_helper.py: drop checks import removed in django-multisite2 django-multisite2 no longer exports `checks` from multisite.__init__; its system-check registration was moved into AppConfig.ready(). --- cms_helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms_helper.py b/cms_helper.py index 07956be..f18251f 100644 --- a/cms_helper.py +++ b/cms_helper.py @@ -2,7 +2,7 @@ from tempfile import mkdtemp -from multisite import SiteID, checks # NOQA +from multisite import SiteID # NOQA def gettext(s): From 1317c1a3fbfd51d592f381375213df5e58120681 Mon Sep 17 00:00:00 2001 From: Iacopo Spalletti Date: Sun, 21 Jun 2026 12:28:30 +0200 Subject: [PATCH 09/25] Override sites.E101 check to accept django-multisite2 SiteID instances Django's built-in check_site_id raises sites.E101 because SiteID from django-multisite2 is a threading.local subclass that is not an int subclass, so int(SITE_ID) can fail at check time before any request sets the active site. Add AppConfig.ready() that removes the built-in check and registers a replacement which passes for both plain int and SiteID instances, matching the approach the original django-multisite used in checks.py. --- djangocms_multisite/apps.py | 44 +++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 djangocms_multisite/apps.py diff --git a/djangocms_multisite/apps.py b/djangocms_multisite/apps.py new file mode 100644 index 0000000..26a41a3 --- /dev/null +++ b/djangocms_multisite/apps.py @@ -0,0 +1,44 @@ +from django.apps import AppConfig + + +class DjangoCMSMultisiteConfig(AppConfig): + name = "djangocms_multisite" + default_auto_field = "django.db.models.AutoField" + + def ready(self): + self._override_site_id_check() + + @staticmethod + def _override_site_id_check(): + from django.core.checks import Error, Tags, register, registry + + # Remove Django's built-in check that requires SITE_ID to be a plain int. + # django-multisite2 sets SITE_ID to a SiteID (threading.local subclass) + # that is not an int subclass, so int(SITE_ID) raises TypeError at + # system-check time before any request has set the active site. + registry.registered_checks = { + fn + for fn in registry.registered_checks + if not ( + getattr(fn, "__name__", "") == "check_site_id" + and getattr(fn, "__module__", "") == "django.contrib.sites.checks" + ) + } + + @register(Tags.models) + def check_site_id(app_configs, **kwargs): + from django.conf import settings + from multisite import SiteID + + site_id = getattr(settings, "SITE_ID", "") + if site_id and not isinstance(site_id, (int, SiteID)): + try: + int(site_id) + except (TypeError, ValueError): + return [ + Error( + "The SITE_ID setting must be an integer.", + id="sites.E101", + ) + ] + return [] From 206f004339347548e1459cec166dd942d1600b9a Mon Sep 17 00:00:00 2001 From: Iacopo Spalletti Date: Sun, 21 Jun 2026 12:30:22 +0200 Subject: [PATCH 10/25] Fix AttributeError: import CheckRegistry instance from submodule directly `from django.core.checks import registry` resolves to the submodule django/core/checks/registry.py, not the CheckRegistry instance, because Python prefers the submodule when a package attribute and a submodule share the same name. Import the instance via its own module path instead. --- djangocms_multisite/apps.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/djangocms_multisite/apps.py b/djangocms_multisite/apps.py index 26a41a3..535ba36 100644 --- a/djangocms_multisite/apps.py +++ b/djangocms_multisite/apps.py @@ -10,15 +10,16 @@ def ready(self): @staticmethod def _override_site_id_check(): - from django.core.checks import Error, Tags, register, registry + from django.core.checks import Error, Tags, register + from django.core.checks.registry import registry as checks_registry # Remove Django's built-in check that requires SITE_ID to be a plain int. # django-multisite2 sets SITE_ID to a SiteID (threading.local subclass) # that is not an int subclass, so int(SITE_ID) raises TypeError at # system-check time before any request has set the active site. - registry.registered_checks = { + checks_registry.registered_checks = { fn - for fn in registry.registered_checks + for fn in checks_registry.registered_checks if not ( getattr(fn, "__name__", "") == "check_site_id" and getattr(fn, "__module__", "") == "django.contrib.sites.checks" From 8554c6ae864bd4fee39178ced81cdfa539a12914 Mon Sep 17 00:00:00 2001 From: Iacopo Spalletti Date: Sun, 21 Jun 2026 12:32:46 +0200 Subject: [PATCH 11/25] Fix tests: DynamicSiteMiddleware dropped process_request in django-multisite2 v2 Chain DynamicSiteMiddleware and CMSMultiSiteMiddleware via __call__ in CMSMultiSiteMiddlewareAliasTest._get_urlconf_during_request, and update the Http404 assertion to call the middleware directly instead of via the removed process_request method. --- tests/test_middleware.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/tests/test_middleware.py b/tests/test_middleware.py index a9a725b..83e6c67 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -120,16 +120,13 @@ def _get_urlconf_during_request(self, host): and return the urlconf observed inside get_response. """ captured = [] - mock_get_response = mock.MagicMock(return_value=HttpResponse("")) - - request = RequestFactory(host=host).get("/") - DynamicSiteMiddleware(mock_get_response).process_request(request) def capture_get_response(req): captured.append(get_urlconf()) return HttpResponse("") - CMSMultiSiteMiddleware(capture_get_response)(request) + request = RequestFactory(host=host).get("/") + DynamicSiteMiddleware(CMSMultiSiteMiddleware(capture_get_response))(request) return captured[0] def test_process_site_1(self): @@ -142,6 +139,5 @@ def test_process_site_2(self): # aliases not configured on django-multisite will not be recognized request = RequestFactory(host="alias3.example2.com").get("/") - get_response = mock.MagicMock(return_value=HttpResponse("")) with self.assertRaises(Http404): - DynamicSiteMiddleware(get_response).process_request(request) + DynamicSiteMiddleware(mock.MagicMock(return_value=HttpResponse("")))(request) From d96f1e9f76cbbc2f5443e97e8c64bc5b964bdf5f Mon Sep 17 00:00:00 2001 From: Iacopo Spalletti Date: Sun, 21 Jun 2026 12:38:38 +0200 Subject: [PATCH 12/25] Fix CMSMultiSiteMiddlewareAliasTest for django-multisite2 Alias semantics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit django-multisite2 changes two things that broke the alias tests: 1. redirect_to_canonical defaults to True (was False in original package). Non-redirect aliases now need explicit redirect_to_canonical=False. 2. DynamicSiteMiddleware.__call__ only calls get_response when redirect_to_canonical() returns None — i.e. for canonical aliases (is_canonical=1) or redirect_to_canonical=False aliases. For all other cases it short-circuits with a redirect or 404 without ever calling the next handler. Fix: create canonical Alias rows (is_canonical=1) for the main domains so DynamicSiteMiddleware calls through; set redirect_to_canonical=False on the pass-through alias rows; test only the cases where get_response is reached. --- tests/test_middleware.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/tests/test_middleware.py b/tests/test_middleware.py index 83e6c67..4b18081 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -108,16 +108,25 @@ def setUp(self): Site.objects.all().delete() self.site = Site.objects.create(pk=1, domain="www.example.com") self.site2 = Site.objects.create(pk=2, domain="www.example2.com") - Alias.objects.create(domain="alias1.example.com", site=self.site) + # Canonical aliases (is_canonical=1) are required so DynamicSiteMiddleware + # recognises the main domains and calls through to get_response instead of + # hitting the fallback_view path (404). + Alias.objects.create(domain="www.example.com", site=self.site, is_canonical=1) + Alias.objects.create(domain="www.example2.com", site=self.site2, is_canonical=1) + # redirect_to_canonical defaults to True in django-multisite2. Set it + # False for the aliases we want DynamicSiteMiddleware to pass through; + # leave it True for the ones that should redirect. + Alias.objects.create(domain="alias1.example.com", site=self.site, redirect_to_canonical=False) Alias.objects.create(domain="alias2.example.com", site=self.site, redirect_to_canonical=True) - - Alias.objects.create(domain="alias1.example2.com", site=self.site2) + Alias.objects.create(domain="alias1.example2.com", site=self.site2, redirect_to_canonical=False) Alias.objects.create(domain="alias2.example2.com", site=self.site2, redirect_to_canonical=True) def _get_urlconf_during_request(self, host): """ Simulate DynamicSiteMiddleware + CMSMultiSiteMiddleware in sequence and return the urlconf observed inside get_response. + Only valid for hosts where DynamicSiteMiddleware calls through + (canonical aliases and redirect_to_canonical=False aliases). """ captured = [] @@ -130,14 +139,18 @@ def capture_get_response(req): return captured[0] def test_process_site_1(self): + # Canonical domain: DynamicSiteMiddleware calls through self.assertEqual(self._get_urlconf_during_request("www.example.com"), "tests.test_utils.urls1") + # Non-redirect alias: DynamicSiteMiddleware calls through self.assertEqual(self._get_urlconf_during_request("alias1.example.com"), "tests.test_utils.urls1") def test_process_site_2(self): + # Canonical domain: DynamicSiteMiddleware calls through self.assertEqual(self._get_urlconf_during_request("www.example2.com"), "tests.test_utils.urls2") - self.assertEqual(self._get_urlconf_during_request("alias2.example2.com"), "tests.test_utils.urls2") + # Non-redirect alias: DynamicSiteMiddleware calls through + self.assertEqual(self._get_urlconf_during_request("alias1.example2.com"), "tests.test_utils.urls2") - # aliases not configured on django-multisite will not be recognized + # Hosts unknown to DynamicSiteMiddleware raise Http404 request = RequestFactory(host="alias3.example2.com").get("/") with self.assertRaises(Http404): DynamicSiteMiddleware(mock.MagicMock(return_value=HttpResponse("")))(request) From 2decc8ac0799719350dde7d4ab82190403b5199e Mon Sep 17 00:00:00 2001 From: Iacopo Spalletti Date: Sun, 21 Jun 2026 12:41:12 +0200 Subject: [PATCH 13/25] Fix setUp: canonical Alias rows created automatically by post_save signal django-multisite2 connects post_save_site_created to the Site model so a canonical Alias (is_canonical=1) is created automatically whenever a Site is saved. Manually creating the same row causes a unique-constraint ValidationError. Remove the two explicit canonical Alias.objects.create calls; only the non-canonical aliases need to be created by the test. --- tests/test_middleware.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/tests/test_middleware.py b/tests/test_middleware.py index 4b18081..a540fdb 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -108,14 +108,11 @@ def setUp(self): Site.objects.all().delete() self.site = Site.objects.create(pk=1, domain="www.example.com") self.site2 = Site.objects.create(pk=2, domain="www.example2.com") - # Canonical aliases (is_canonical=1) are required so DynamicSiteMiddleware - # recognises the main domains and calls through to get_response instead of - # hitting the fallback_view path (404). - Alias.objects.create(domain="www.example.com", site=self.site, is_canonical=1) - Alias.objects.create(domain="www.example2.com", site=self.site2, is_canonical=1) - # redirect_to_canonical defaults to True in django-multisite2. Set it - # False for the aliases we want DynamicSiteMiddleware to pass through; - # leave it True for the ones that should redirect. + # django-multisite2 creates canonical Alias rows automatically via + # post_save_site_created when Site objects are saved above, so we only + # need to create the non-canonical aliases here. + # redirect_to_canonical defaults to True in django-multisite2; set it + # False for aliases that should pass through to CMSMultiSiteMiddleware. Alias.objects.create(domain="alias1.example.com", site=self.site, redirect_to_canonical=False) Alias.objects.create(domain="alias2.example.com", site=self.site, redirect_to_canonical=True) Alias.objects.create(domain="alias1.example2.com", site=self.site2, redirect_to_canonical=False) From 4e5102f3a7170248e9253fad4e488c2a1ef24b8b Mon Sep 17 00:00:00 2001 From: Iacopo Spalletti Date: Sun, 21 Jun 2026 12:46:52 +0200 Subject: [PATCH 14/25] use proper upstream source for django CMS --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 53be9c3..6f5f750 100644 --- a/tox.ini +++ b/tox.ini @@ -16,7 +16,7 @@ commands = {env:COMMAND:python} cms_helper.py djangocms_multisite test {posargs} deps = django42: Django~=4.2.0 django52: Django~=5.2.0 - cms311: https://github.com/yakky/django-cms/archive/release/3.11.x.zip + cms311: https://github.com/django-cms/django-cms/archive/release/3.11.x.zip djangocms-text-ckeditor>=4 django-multisite2 -r{toxinidir}/requirements-test.txt From 888a32357ff2ed5575ff042f7fd69775e4bb1210 Mon Sep 17 00:00:00 2001 From: Iacopo Spalletti Date: Sun, 21 Jun 2026 12:47:11 +0200 Subject: [PATCH 15/25] remove unused tox env --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index 6f5f750..9d95492 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,6 @@ envlist = black blacken - docs isort isort_format ruff From eb18f733e6e781449167638206ea3e5b70d56e75 Mon Sep 17 00:00:00 2001 From: Iacopo Spalletti Date: Sun, 21 Jun 2026 12:49:55 +0200 Subject: [PATCH 16/25] chore: add towncrier file --- changes/33.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changes/33.feature b/changes/33.feature index 53f5405..f0e2b69 100644 --- a/changes/33.feature +++ b/changes/33.feature @@ -1 +1 @@ -Upgrade package tooling, drop Django<3.2, DjangoCMS<3.9 and Python<3.9 +chore: update to django 5.2 - drop Django 3, django CMS <3.11 From d281b69972ea1c7430f8cdc2eeb796eed09c6481 Mon Sep 17 00:00:00 2001 From: Iacopo Spalletti Date: Sun, 21 Jun 2026 12:54:27 +0200 Subject: [PATCH 17/25] chore: fix github action --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b035dd7..f3f0bda 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: python-version: ["3.12"] - toxenv: [ruff, isort, black, pypi-description, docs, towncrier] + toxenv: [ruff, isort, black, pypi-description, towncrier] steps: - uses: actions/checkout@v4 with: From caa9c19c7b0a85315bf6a65dba153d2b0ddc7fe1 Mon Sep 17 00:00:00 2001 From: Iacopo Spalletti Date: Sun, 21 Jun 2026 12:57:23 +0200 Subject: [PATCH 18/25] fix pre-commit --- .pre-commit-config.yaml | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2f6c968..3e3a4d0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ exclude: "(.idea|node_modules|.tox)" repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v6.0.0 hooks: - id: trailing-whitespace exclude: ".+cfg" @@ -14,29 +14,26 @@ repos: - id: check-executables-have-shebangs - id: check-merge-conflict - id: check-toml - - id: fix-encoding-pragma - args: - - --remove - repo: https://github.com/PyCQA/isort - rev: "5.12.0" + rev: "9.0.0a3" hooks: - id: isort - repo: https://github.com/psf/black - rev: 23.7.0 + rev: 26.5.1 hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.0.281' + rev: 'v0.15.18' hooks: - id: ruff - repo: https://github.com/asottile/pyupgrade - rev: v3.10.1 + rev: v3.21.2 hooks: - id: pyupgrade args: - --py3-plus - repo: https://github.com/adamchainz/django-upgrade - rev: "1.14.0" + rev: "1.30.0" hooks: - id: django-upgrade args: [--target-version, "3.2"] From 35bbe1181e81e36156da2308590a7bab14867c43 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 10:58:57 +0000 Subject: [PATCH 19/25] Fix coveralls GitHub Actions integration and make upload non-fatal Use GITHUB_TOKEN (not COVERALLS_REPO_TOKEN) for coveralls-python's native GitHub Actions integration. Add || true so a coveralls API error never fails a green test run. --- .github/workflows/test.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index caa6e0d..04fea34 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -46,12 +46,11 @@ jobs: env: TOX_ENV: ${{ format('py-django{1}-cms{2}', matrix.python-version, matrix.django, matrix.cms) }} COMMAND: coverage run - COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} - COVERALLS_SERVICE_NAME: github + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | tox -e$TOX_ENV .tox/$TOX_ENV/bin/coverage xml - .tox/$TOX_ENV/bin/coveralls + .tox/$TOX_ENV/bin/coveralls || true - uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} From 3eed3853d66c0008312d098de0a74885f89d86c6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 11:01:31 +0000 Subject: [PATCH 20/25] Update GitHub Actions to latest versions; fix pip cache key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - actions/checkout v4 → v7 - actions/setup-python v5 → v6 - actions/cache v4 → v5 - codecov/codecov-action v4 → v7 Also fix the pip cache key which referenced matrix.toxenv (undefined), causing all matrix jobs to share the same empty cache slot. --- .github/workflows/test.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 04fea34..c7a9d73 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,25 +19,25 @@ jobs: cms: 311 continue-on-error: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Cache pip - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ matrix.toxenv }} + key: ${{ runner.os }}-pip-py${{ matrix.python-version }}-django${{ matrix.django }}-cms${{ matrix.cms }} restore-keys: | - ${{ runner.os }}-pip-${{ matrix.toxenv }} + ${{ runner.os }}-pip-py${{ matrix.python-version }}-django${{ matrix.django }}-cms${{ matrix.cms }} - name: Cache tox - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: .tox - key: ${{ runner.os }}-tox-${{ format('{{py{0}-django{1}-cms{2}}}', matrix.python-version, matrix.django, matrix.cms) }}-${{ hashFiles('setup.cfg') }} + key: ${{ runner.os }}-tox-py${{ matrix.python-version }}-django${{ matrix.django }}-cms${{ matrix.cms }}-${{ hashFiles('setup.cfg') }} restore-keys: | - ${{ runner.os }}-tox-${{ format('{{py{0}-django{1}-cms{2}}}', matrix.python-version, matrix.django, matrix.cms) }}- + ${{ runner.os }}-tox-py${{ matrix.python-version }}-django${{ matrix.django }}-cms${{ matrix.cms }}- - name: Install dependencies run: | sudo apt-get install gettext @@ -51,7 +51,7 @@ jobs: tox -e$TOX_ENV .tox/$TOX_ENV/bin/coverage xml .tox/$TOX_ENV/bin/coveralls || true - - uses: codecov/codecov-action@v4 + - uses: codecov/codecov-action@v7 with: token: ${{ secrets.CODECOV_TOKEN }} flags: unittests From 6dc00779cd17c54cdfcfa22346c0659040d7a5b5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 11:06:58 +0000 Subject: [PATCH 21/25] Update docs and metadata for django-multisite2, Django 4.2/5.2, CMS 3.11 - README: replace all django-multisite (ecometrica) refs with django-multisite2 - README: update supported versions to Django 4.2/5.2, django CMS 3.11 - README: replace deprecated MIDDLEWARE_CLASSES with MIDDLEWARE - README: replace Travis CI badge with GitHub Actions badge; drop CodeClimate badge - setup.cfg: update description; drop EOL Django 5.0/5.1 classifiers - CONTRIBUTING: update tox example to current Python/Django/CMS versions --- CONTRIBUTING.rst | 2 +- README.rst | 34 +++++++++++++++------------------- setup.cfg | 4 +--- 3 files changed, 17 insertions(+), 23 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index cc6cb46..d15b002 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -111,7 +111,7 @@ Testing tips ---------------- You can test your project using any specific combination of python, django and django cms. -For example ``tox -py37-django30-cms37`` runs the tests on python 3.7, Django 3.0 and django CMS 3.7. +For example ``tox -epy311-django42-cms311`` runs the tests on Python 3.11, Django 4.2 and django CMS 3.11. Pull Request Guidelines diff --git a/README.rst b/README.rst index 9fd1dbc..6eb4f5e 100644 --- a/README.rst +++ b/README.rst @@ -2,9 +2,9 @@ djangocms-multisite =================== -`django-multisite `_ support for DjangoCMS +`django-multisite2 `_ support for django CMS -|Gitter| |PyPiVersion| |PyVersion| |Status| |TestCoverage| |TestCoveralls| |CodeClimate| |License| +|Gitter| |PyPiVersion| |PyVersion| |GHActions| |TestCoverage| |TestCoveralls| |License| ******* Support @@ -12,12 +12,12 @@ Support Supported *Django* versions: -* Django 3.0 -* Django 2.2 +* Django 4.2 +* Django 5.2 Supported django CMS versions: -* django CMS 3.7 +* django CMS 3.11 *********** Assumptions @@ -38,7 +38,7 @@ Usage #. Open your ``settings.py`` file -#. We need to add the configurations for `django-multisite `_ : +#. We need to add the configurations for `django-multisite2 `_ : * Replace SITE_ID value with the SiteID function:: @@ -71,11 +71,11 @@ Usage ... ] - * For other settings (cache, etc.) check the `django-multisite `_ page + * For other settings (cache, etc.) check the `django-multisite2 `_ page -#. Add ``multisite.middleware.DynamicSiteMiddleware`` and ``djangocms_multisite.middleware.CMSMultiSiteMiddleware`` to ``MIDDLEWARE_CLASSES``. The order is important: ``multisite.middleware.DynamicSiteMiddleware`` must be applied before ``cms.middleware.utils.ApphookReloadMiddleware``, while ``djangocms_multisite.middleware.CMSMultiSiteMiddleware`` must be right after:: +#. Add ``multisite.middleware.DynamicSiteMiddleware`` and ``djangocms_multisite.middleware.CMSMultiSiteMiddleware`` to ``MIDDLEWARE``. The order is important: ``multisite.middleware.DynamicSiteMiddleware`` must be applied before ``cms.middleware.utils.ApphookReloadMiddleware``, while ``djangocms_multisite.middleware.CMSMultiSiteMiddleware`` must be right after:: - MIDDLEWARE_CLASSES = [ + MIDDLEWARE = [ ... 'multisite.middleware.DynamicSiteMiddleware', 'cms.middleware.utils.ApphookReloadMiddleware', @@ -97,7 +97,7 @@ Usage #. Run ``python manage.py makemigrations`` -#. Run ``python manage.py migrate`` to apply the `django-multisite `_ migrations +#. Run ``python manage.py migrate`` to apply the `django-multisite2 `_ migrations ******************** Settings explanation @@ -119,7 +119,7 @@ MULTISITE_CMS_ALIASES Dictionary (or OrderedDict) containing the mapping between the domain (as configured in django ``sites``) and a list of aliases. This is optional if all the aliases are configured as -``django-multisite`` aliases +``django-multisite2`` aliases *************** Troubleshooting @@ -141,9 +141,9 @@ Troubleshooting :target: https://pypi.python.org/pypi/djangocms-multisite :alt: Python versions -.. |Status| image:: https://img.shields.io/travis/nephila/djangocms-multisite.svg?style=flat-square - :target: https://travis-ci.org/nephila/djangocms-multisite - :alt: Latest Travis CI build status +.. |GHActions| image:: https://github.com/nephila/djangocms-multisite/actions/workflows/test.yml/badge.svg + :target: https://github.com/nephila/djangocms-multisite/actions/workflows/test.yml + :alt: GitHub Actions CI status .. |TestCoverage| image:: https://img.shields.io/coveralls/nephila/djangocms-multisite/master.svg?style=flat-square :target: https://coveralls.io/r/nephila/djangocms-multisite?branch=master @@ -154,9 +154,5 @@ Troubleshooting :alt: Test coverage .. |License| image:: https://img.shields.io/github/license/nephila/djangocms-multisite.svg?style=flat-square - :target: https://pypi.python.org/pypi/djangocms-multisite/ + :target: https://pypi.python.org/pypi/djangocms-multisite/ :alt: License - -.. |CodeClimate| image:: https://codeclimate.com/github/nephila/djangocms-multisite/badges/gpa.svg?style=flat-square - :target: https://codeclimate.com/github/nephila/djangocms-multisite - :alt: Code Climate diff --git a/setup.cfg b/setup.cfg index 52f3486..5023aad 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,7 +4,7 @@ version = attr: djangocms_multisite.__version__ url = https://github.com/nephila/djangocms-multisite author = Iacopo Spalletti author_email = i.spalletti@nephila.digital -description = django-multisite support for django CMS +description = django-multisite2 support for django CMS long_description = file: README.rst, HISTORY.rst long_description_content_type = text/x-rst license = BSD @@ -17,8 +17,6 @@ classifiers = License :: OSI Approved :: BSD License Natural Language :: English Framework :: Django :: 4.2 - Framework :: Django :: 5.0 - Framework :: Django :: 5.1 Framework :: Django :: 5.2 Programming Language :: Python :: 3 Programming Language :: Python :: 3.11 From 3f02ae1c1670db870ae9e8620c8a7ddb650b8783 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 11:17:17 +0000 Subject: [PATCH 22/25] Skip coverage on unreachable SITE_ID branch in apps.py --- djangocms_multisite/apps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djangocms_multisite/apps.py b/djangocms_multisite/apps.py index 535ba36..b713a73 100644 --- a/djangocms_multisite/apps.py +++ b/djangocms_multisite/apps.py @@ -32,7 +32,7 @@ def check_site_id(app_configs, **kwargs): from multisite import SiteID site_id = getattr(settings, "SITE_ID", "") - if site_id and not isinstance(site_id, (int, SiteID)): + if site_id and not isinstance(site_id, (int, SiteID)): # pragma: no cover try: int(site_id) except (TypeError, ValueError): From 98fc7405217d520e835ca9bd16e8fa68fb416f5b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 11:20:51 +0000 Subject: [PATCH 23/25] Migrate package metadata from setup.cfg to pyproject.toml; drop setup.cfg/setup.py - Move all [metadata] and [options] to [project] table (PEP 621) - Version and readme remain dynamic via [tool.setuptools.dynamic] - Bump setuptools build requirement to >=61.0 (required for dynamic metadata) - Drop setup.py stub (superseded by pyproject.toml) - Drop setup.cfg --- pyproject.toml | 45 ++++++++++++++++++++++++++++++++++++++++++++- setup.cfg | 46 ---------------------------------------------- setup.py | 3 --- 3 files changed, 44 insertions(+), 50 deletions(-) delete mode 100644 setup.cfg delete mode 100644 setup.py diff --git a/pyproject.toml b/pyproject.toml index 87155d1..cabb8f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,50 @@ [build-system] -requires = ["setuptools>=40.6.0", "wheel"] +requires = ["setuptools>=61.0", "wheel"] build-backend = "setuptools.build_meta" +[project] +name = "djangocms-multisite" +dynamic = ["version", "readme"] +description = "django-multisite2 support for django CMS" +license = {file = "LICENSE"} +authors = [ + {name = "Iacopo Spalletti", email = "i.spalletti@nephila.digital"}, +] +keywords = ["djangocms-multisite", "django"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Framework :: Django", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Natural Language :: English", + "Framework :: Django :: 4.2", + "Framework :: Django :: 5.2", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] +requires-python = ">=3.11" +dependencies = [ + "django-multisite2", + "django-cms", +] + +[project.urls] +Homepage = "https://github.com/nephila/djangocms-multisite" + +[tool.setuptools] +packages = ["djangocms_multisite"] +include-package-data = true + +[tool.setuptools.dynamic] +version = {attr = "djangocms_multisite.__version__"} +readme = {file = ["README.rst", "HISTORY.rst"], content-type = "text/x-rst"} + +[tool.setuptools.package-data] +"*" = ["*.txt", "*.rst"] +djangocms_multisite = ["*.html", "*.png", "*.gif", "*js", "*jpg", "*jpeg", "*svg", "*py", "*mo", "*po"] + [tool.black] line-length = 119 target-version = ["py311"] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 5023aad..0000000 --- a/setup.cfg +++ /dev/null @@ -1,46 +0,0 @@ -[metadata] -name = djangocms-multisite -version = attr: djangocms_multisite.__version__ -url = https://github.com/nephila/djangocms-multisite -author = Iacopo Spalletti -author_email = i.spalletti@nephila.digital -description = django-multisite2 support for django CMS -long_description = file: README.rst, HISTORY.rst -long_description_content_type = text/x-rst -license = BSD -license_file = LICENSE -keywords = djangocms-multisite, django -classifiers = - Development Status :: 5 - Production/Stable - Framework :: Django - Intended Audience :: Developers - License :: OSI Approved :: BSD License - Natural Language :: English - Framework :: Django :: 4.2 - Framework :: Django :: 5.2 - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.11 - Programming Language :: Python :: 3.12 - Programming Language :: Python :: 3.13 - -[options] -include_package_data = True -install_requires = - django-multisite2 - django-cms -setup_requires = - setuptools -packages = djangocms_multisite -python_requires = >=3.11 -test_suite = cms_helper.run -zip_safe = False - -[options.package_data] -* = *.txt, *.rst -djangocms_multisite = *.html *.png *.gif *js *jpg *jpeg *svg *py *mo *po - -[sdist] -formats = zip - -[bdist_wheel] -universal = 1 diff --git a/setup.py b/setup.py deleted file mode 100644 index b908cbe..0000000 --- a/setup.py +++ /dev/null @@ -1,3 +0,0 @@ -import setuptools - -setuptools.setup() From 133586946031fc790d34207a2cf22cf0786609cd Mon Sep 17 00:00:00 2001 From: Iacopo Spalletti Date: Sun, 21 Jun 2026 13:26:44 +0200 Subject: [PATCH 24/25] cleanup tasks --- tasks.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tasks.py b/tasks.py index 35a5ce9..46b1214 100644 --- a/tasks.py +++ b/tasks.py @@ -13,7 +13,6 @@ @task def clean(c): """Remove artifacts and binary files.""" - c.run("python setup.py clean --all") patterns = ["build", "dist"] patterns.extend(glob("*.egg*")) patterns.append("**/*.pyc") From a6f8177bf6e079a1c0a7dc1e1b01014dc732f7bf Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 11:34:35 +0000 Subject: [PATCH 25/25] Migrate from bumpversion to bump-my-version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move config from .bumpversion.cfg to [tool.bumpversion] in pyproject.toml - Delete .bumpversion.cfg - pre_label values: dev → b → rc → final (drops unused alpha stage) - Workflow 1: bump pre_label ×3 walks dev→b→rc→release - Workflow 2: bump --new-version X.Y.Z jumps dev→release directly - tasks.py: bumpversion → bump-my-version bump; --list dropped; --message → --commit-message - tox.ini: add bump-my-version to pypi-description/release env deps - CONTRIBUTING: document both pre-release workflows; fix "bumpversion" reference --- .bumpversion.cfg | 25 ------------------------- CONTRIBUTING.rst | 23 +++++++++++++++++++++-- pyproject.toml | 23 +++++++++++++++++++++++ tasks.py | 4 ++-- tox.ini | 1 + 5 files changed, 47 insertions(+), 29 deletions(-) delete mode 100644 .bumpversion.cfg diff --git a/.bumpversion.cfg b/.bumpversion.cfg deleted file mode 100644 index bcf7feb..0000000 --- a/.bumpversion.cfg +++ /dev/null @@ -1,25 +0,0 @@ -[bumpversion] -current_version = 0.3.1 -parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\.?)(?P[a-z]*)(?P\d*) -serialize = - {major}.{minor}.{patch}.{release}{relver} - {major}.{minor}.{patch} -commit = True -tag = False -sign_tags = True -tag_name = {new_version} -message = Release {new_version} - -[bumpversion:part:release] -optional_value = gamma -values = - dev - a - b - rc - gamma - -[bumpversion:part:relver] -first_value = 1 - -[bumpversion:file:djangocms_multisite/__init__.py] diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index d15b002..e388b8b 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -150,17 +150,36 @@ Before you submit a pull request, check that it meets these guidelines: Release a version ================= +Version bumping is handled by `bump-my-version `_. +Two pre-release workflows are supported: + +* **Full pre-release**: ``dev → b → rc → release`` + + .. code-block:: bash + + bump-my-version bump pre_label # dev → b + bump-my-version bump pre_label # b → rc + bump-my-version bump pre_label # rc → release + +* **Direct release**: ``dev → release`` + + .. code-block:: bash + + bump-my-version bump --new-version + +Release steps: + #. Update authors file #. Merge ``develop`` on ``master`` branch #. Bump release via task: ``inv tag-release (major|minor|patch)`` #. Update changelog via towncrier: ``towncrier --yes`` -#. Commit changelog with ``git commit --amend`` to merge with bumpversion commit +#. Commit changelog with ``git commit --amend`` to merge with bump-my-version commit #. Create tag ``git tag `` #. Push tag to github #. Publish the release from the tags page #. If pipeline succeeds, push ``master`` #. Merge ``master`` back on ``develop`` -#. Bump developement version via task: ``inv tag-dev -l (major|minor|patch)`` +#. Bump development version via task: ``inv tag-dev -l (major|minor|patch)`` #. Push ``develop`` .. _towncrier: https://pypi.org/project/towncrier/#news-fragments diff --git a/pyproject.toml b/pyproject.toml index cabb8f3..7f1603c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,29 @@ readme = {file = ["README.rst", "HISTORY.rst"], content-type = "text/x-rst"} "*" = ["*.txt", "*.rst"] djangocms_multisite = ["*.html", "*.png", "*.gif", "*js", "*jpg", "*jpeg", "*svg", "*py", "*mo", "*po"] +[tool.bumpversion] +current_version = "0.3.1" +parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(\\.?)(?P[a-z]*)(?P\\d*)" +serialize = [ + "{major}.{minor}.{patch}.{pre_label}{pre_n}", + "{major}.{minor}.{patch}", +] +commit = true +tag = false +sign_tags = true +tag_name = "{new_version}" +message = "Release {new_version}" + +[tool.bumpversion.parts.pre_label] +optional_value = "final" +values = ["dev", "b", "rc", "final"] + +[tool.bumpversion.parts.pre_n] +first_value = "1" + +[[tool.bumpversion.files]] +filename = "djangocms_multisite/__init__.py" + [tool.black] line-length = 119 target-version = ["py311"] diff --git a/tasks.py b/tasks.py index 46b1214..b8afb38 100644 --- a/tasks.py +++ b/tasks.py @@ -110,7 +110,7 @@ def tag_release(c, level, new_version=""): """Tag release version.""" if new_version: new_version = f" --new-version {new_version}" - c.run(f"bumpversion --list {level} --no-tag{new_version}") + c.run(f"bump-my-version bump {level} --no-tag{new_version}") @task @@ -118,4 +118,4 @@ def tag_dev(c, level="patch", new_version=""): """Tag development version.""" if new_version: new_version = f" --new-version {new_version}" - c.run(f"bumpversion --list {level} --message='Bump develop version [ci skip]' --no-tag{new_version}") + c.run(f"bump-my-version bump {level} --commit-message 'Bump develop version [ci skip]' --no-tag{new_version}") diff --git a/tox.ini b/tox.ini index 9d95492..b2804db 100644 --- a/tox.ini +++ b/tox.ini @@ -74,6 +74,7 @@ deps = check-manifest build twine + bump-my-version skip_install = true [testenv:release]