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..f3f0bda
--- /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.12"]
+ toxenv: [ruff, isort, black, pypi-description, towncrier]
+ steps:
+ - 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@v5
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: Cache pip
+ 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@v4
+ 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..c7a9d73
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,59 @@
+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.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@v7
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v6
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: Cache pip
+ uses: actions/cache@v5
+ with:
+ path: ~/.cache/pip
+ key: ${{ runner.os }}-pip-py${{ matrix.python-version }}-django${{ matrix.django }}-cms${{ matrix.cms }}
+ restore-keys: |
+ ${{ runner.os }}-pip-py${{ matrix.python-version }}-django${{ matrix.django }}-cms${{ matrix.cms }}
+ - name: Cache tox
+ uses: actions/cache@v5
+ with:
+ path: .tox
+ key: ${{ runner.os }}-tox-py${{ matrix.python-version }}-django${{ matrix.django }}-cms${{ matrix.cms }}-${{ hashFiles('setup.cfg') }}
+ restore-keys: |
+ ${{ runner.os }}-tox-py${{ matrix.python-version }}-django${{ matrix.django }}-cms${{ 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
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ tox -e$TOX_ENV
+ .tox/$TOX_ENV/bin/coverage xml
+ .tox/$TOX_ENV/bin/coveralls || true
+ - uses: codecov/codecov-action@v7
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ flags: unittests
+ files: ./coverage.xml
+ fail_ci_if_error: false
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
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..3e3a4d0
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,50 @@
+# 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: v6.0.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
+ - repo: https://github.com/PyCQA/isort
+ rev: "9.0.0a3"
+ hooks:
+ - id: isort
+ - repo: https://github.com/psf/black
+ rev: 26.5.1
+ hooks:
+ - id: black
+ - repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: 'v0.15.18'
+ hooks:
+ - id: ruff
+ - repo: https://github.com/asottile/pyupgrade
+ rev: v3.21.2
+ hooks:
+ - id: pyupgrade
+ args:
+ - --py3-plus
+ - repo: https://github.com/adamchainz/django-upgrade
+ rev: "1.30.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
deleted file mode 100644
index d2cadbb..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,66 +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
-
-
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index b616dd9..e388b8b 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,94 @@ To get flake8 and tox, just pip install them into your virtualenv.
7. Submit a pull request through the GitHub website.
+Development tips
+----------------
+
+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.
+
+Testing tips
+----------------
+You can test your project using any specific combination of python, django and django cms.
+
+For example ``tox -epy311-django42-cms311`` runs the tests on Python 3.11, Django 4.2 and django CMS 3.11.
+
+
Pull Request Guidelines
------------------------
+=======================
Before you submit a pull request, check that it meets these guidelines:
-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.
+#. 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
+=================
+
+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
-Tips
-----
+Release steps:
-To run a subset of tests::
+#. 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 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 development version via task: ``inv tag-dev -l (major|minor|patch)``
+#. Push ``develop``
- $ python -m unittest tests.test_djangocms_multisite
+.. _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..6eb4f5e 100644
--- a/README.rst
+++ b/README.rst
@@ -2,39 +2,43 @@
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
-=======
+*******
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
-===========
+***********
#. A virtualenv up and running
#. DjangoCMS working
+************
Installation
-============
+************
``pip install djangocms-multisite``
+*****
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::
@@ -67,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',
@@ -93,32 +97,33 @@ 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
-====================
+********************
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
+``django-multisite2`` aliases
+***************
Troubleshooting
-===============
+***************
* Domains in ``MULTISITE_CMS_URLS`` must be the same created in your database (via the interface in ``Home âē Sites âē Sites``).
@@ -136,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
@@ -149,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/changes/33.feature b/changes/33.feature
new file mode 100644
index 0000000..f0e2b69
--- /dev/null
+++ b/changes/33.feature
@@ -0,0 +1 @@
+chore: update to django 5.2 - drop Django 3, django CMS <3.11
diff --git a/cms_helper.py b/cms_helper.py
old mode 100755
new mode 100644
index e8ae1cf..f18251f
--- a/cms_helper.py
+++ b/cms_helper.py
@@ -1,100 +1,102 @@
#!/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 # 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',
- ),
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/apps.py b/djangocms_multisite/apps.py
new file mode 100644
index 0000000..b713a73
--- /dev/null
+++ b/djangocms_multisite/apps.py
@@ -0,0 +1,45 @@
+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
+ 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.
+ checks_registry.registered_checks = {
+ fn
+ for fn in checks_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)): # pragma: no cover
+ try:
+ int(site_id)
+ except (TypeError, ValueError):
+ return [
+ Error(
+ "The SITE_ID setting must be an integer.",
+ id="sites.E101",
+ )
+ ]
+ return []
diff --git a/djangocms_multisite/middleware.py b/djangocms_multisite/middleware.py
index eac275b..ba5c85e 100644
--- a/djangocms_multisite/middleware.py
+++ b/djangocms_multisite/middleware.py
@@ -4,18 +4,19 @@
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', {})
+ 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 +26,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,31 +45,30 @@ 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
- 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):
- patch_vary_headers(response, ('Host',))
- # set back to default urlconf
- set_urlconf(None)
+ # 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",))
return response
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..7f1603c
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,114 @@
+[build-system]
+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.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"]
+
+[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 = "py311"
+
+[tool.ruff.mccabe]
+max-complexity = 10
diff --git a/requirements-test.txt b/requirements-test.txt
index 2345d03..7d2bbab 100644
--- a/requirements-test.txt
+++ b/requirements-test.txt
@@ -1,10 +1,6 @@
-r requirements.txt
-coverage
-coveralls
-mock>=1.0.1
-nose>=1.3.0
-django-nose>=1.2
-flake8
+coverage>5
+coveralls>2
django-app-helper
-tox>=2.0
+tox>=4.0
wheel
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index a189366..0000000
--- a/setup.cfg
+++ /dev/null
@@ -1,19 +0,0 @@
-[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
-
-[wheel]
-universal = 1
-
-[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
diff --git a/setup.py b/setup.py
deleted file mode 100755
index cc8b619..0000000
--- a/setup.py
+++ /dev/null
@@ -1,52 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-import djangocms_multisite
-
-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',
- ],
-)
diff --git a/tasks.py b/tasks.py
new file mode 100644
index 0000000..b8afb38
--- /dev/null
+++ b/tasks.py
@@ -0,0 +1,121 @@
+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."""
+ 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"bump-my-version bump {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"bump-my-version bump {level} --commit-message 'Bump develop version [ci skip]' --no-tag{new_version}")
diff --git a/tests/test_middleware.py b/tests/test_middleware.py
index 226b9b9..a540fdb 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,125 +16,138 @@
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")
+
+ 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('/')
- CMSMultiSiteMiddleware().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')
+ 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('/')
- CMSMultiSiteMiddleware().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')
+ 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().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')
+ 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('/')
- CMSMultiSiteMiddleware().process_request(request)
- self.assertEqual(get_urlconf(), 'tests.test_utils.urls2')
- CMSMultiSiteMiddleware().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(''))
- # 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())
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)
-
- Alias.objects.create(domain='alias1.example2.com', site=self.site2)
- Alias.objects.create(domain='alias2.example2.com', site=self.site2, 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")
+ # 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)
+ 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 = []
+
+ def capture_get_response(req):
+ captured.append(get_urlconf())
+ return HttpResponse("")
+
+ request = RequestFactory(host=host).get("/")
+ DynamicSiteMiddleware(CMSMultiSiteMiddleware(capture_get_response))(request)
+ return captured[0]
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='alias1.example.com').get('/')
- DynamicSiteMiddleware().process_request(request)
- CMSMultiSiteMiddleware().process_request(request)
- self.assertEqual(get_urlconf(), 'tests.test_utils.urls1')
+ # 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):
- 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='alias2.example2.com').get('/')
- DynamicSiteMiddleware().process_request(request)
- CMSMultiSiteMiddleware().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('/')
+ # Canonical domain: DynamicSiteMiddleware calls through
+ self.assertEqual(self._get_urlconf_during_request("www.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")
+
+ # Hosts unknown to DynamicSiteMiddleware raise Http404
+ request = RequestFactory(host="alias3.example2.com").get("/")
with self.assertRaises(Http404):
- DynamicSiteMiddleware().process_request(request)
+ DynamicSiteMiddleware(mock.MagicMock(return_value=HttpResponse("")))(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..b2804db 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,32 +1,114 @@
[tox]
-envlist = pep8,isort,py{38,37,36,35}-django{30,22}-cms{37}
+envlist =
+ black
+ blacken
+ isort
+ isort_format
+ ruff
+ pypi-description
+ towncrier
+ py{311,312,313}-django{42,52}-cms{311}
+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
+ django42: Django~=4.2.0
+ django52: Django~=5.2.0
+ 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
+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
+ bump-my-version
+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