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