diff --git a/.github/contributors.json b/.github/contributors.json index a426bb551..057309a52 100644 --- a/.github/contributors.json +++ b/.github/contributors.json @@ -1508,5 +1508,20 @@ "name": "Nix Siow", "github_login": "nixsiow", "twitter_username": "nixsiow" + }, + { + "name": "Jens Kaeske", + "github_login": "jkaeske", + "twitter_username": "" + }, + { + "name": "henningbra", + "github_login": "henningbra", + "twitter_username": "" + }, + { + "name": "Paul Wulff", + "github_login": "mtmpaulwulff", + "twitter_username": "" } ] \ No newline at end of file diff --git a/.github/workflows/issue-manager.yml b/.github/workflows/issue-manager.yml index e1bb614b1..103612cfe 100644 --- a/.github/workflows/issue-manager.yml +++ b/.github/workflows/issue-manager.yml @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: tiangolo/issue-manager@0.4.1 + - uses: tiangolo/issue-manager@0.5.0 with: token: ${{ secrets.GITHUB_TOKEN }} config: > @@ -39,5 +39,9 @@ jobs: "waiting": { "delay": 864000, "message": "Automatically closing after waiting for additional info. To re-open, please provide the additional information requested." + }, + "wontfix": { + "delay": 864000, + "message": "As discussed, we won't be implementing this. Automatically closing." } } diff --git a/.github/workflows/pre-commit-autoupdate.yml b/.github/workflows/pre-commit-autoupdate.yml index bfd906cea..0ad414398 100644 --- a/.github/workflows/pre-commit-autoupdate.yml +++ b/.github/workflows/pre-commit-autoupdate.yml @@ -37,7 +37,7 @@ jobs: run: pre-commit autoupdate - name: Create Pull Request - uses: peter-evans/create-pull-request@v5 + uses: peter-evans/create-pull-request@v6 with: token: ${{ secrets.GITHUB_TOKEN }} branch: update/pre-commit-autoupdate diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8c10de974..7daf6ac71 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -33,7 +33,7 @@ repos: exclude: hooks/ - repo: https://github.com/psf/black - rev: 23.12.1 + rev: 24.2.0 hooks: - id: black diff --git a/CHANGELOG.md b/CHANGELOG.md index b44f8f5d6..1f9b07857 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,247 @@ All enhancements and patches to Cookiecutter Django will be documented in this f +## 2024.02.19 + + +### Updated + +- Update sentry-sdk to 1.40.5 ([#4876](https://github.com/cookiecutter/cookiecutter-django/pull/4876)) + +## 2024.02.17 + + +### Updated + +- Update pytest to 8.0.1 ([#4870](https://github.com/cookiecutter/cookiecutter-django/pull/4870)) + +## 2024.02.16 + + +### Changed + +- Speed up GitHub CI for Docker setup ([#4863](https://github.com/cookiecutter/cookiecutter-django/pull/4863)) + +### Documentation + +- Add link to the ruff repository in requirements ([#4866](https://github.com/cookiecutter/cookiecutter-django/pull/4866)) + +## 2024.02.13 + + +### Changed + +- Ruff linting & formatting ([#4834](https://github.com/cookiecutter/cookiecutter-django/pull/4834)) + +### Updated + +- Update uvicorn to 0.27.1 ([#4848](https://github.com/cookiecutter/cookiecutter-django/pull/4848)) + +- Update sentry-sdk to 1.40.4 ([#4858](https://github.com/cookiecutter/cookiecutter-django/pull/4858)) + +- Bump traefik to 2.11.0 ([#4857](https://github.com/cookiecutter/cookiecutter-django/pull/4857)) + +- Auto-update pre-commit hooks ([#4855](https://github.com/cookiecutter/cookiecutter-django/pull/4855)) + +- Update black to 24.2.0 ([#4853](https://github.com/cookiecutter/cookiecutter-django/pull/4853)) + +## 2024.02.12 + + +### Updated + +- Update django-model-utils to 4.4.0 ([#4850](https://github.com/cookiecutter/cookiecutter-django/pull/4850)) + +- Update pre-commit to 3.6.1 ([#4849](https://github.com/cookiecutter/cookiecutter-django/pull/4849)) + +- Update django-upgrade to 1.16.0 ([#4851](https://github.com/cookiecutter/cookiecutter-django/pull/4851)) + +- Auto-update pre-commit hooks ([#4852](https://github.com/cookiecutter/cookiecutter-django/pull/4852)) + +## 2024.02.09 + + +### Updated + +- Update sentry-sdk to 1.40.3 ([#4847](https://github.com/cookiecutter/cookiecutter-django/pull/4847)) + +- Update django-allauth to 0.61.1 ([#4846](https://github.com/cookiecutter/cookiecutter-django/pull/4846)) + +- Update python-slugify to 8.0.4 ([#4844](https://github.com/cookiecutter/cookiecutter-django/pull/4844)) + +## 2024.02.08 + + +### Updated + +- Bump python to 3.11.8 in compose/local/docs ([#4840](https://github.com/cookiecutter/cookiecutter-django/pull/4840)) + +- Bump python to 3.11.8 in compose/local/django ([#4841](https://github.com/cookiecutter/cookiecutter-django/pull/4841)) + +- Bump python to 3.11.8 in compose/production/django ([#4842](https://github.com/cookiecutter/cookiecutter-django/pull/4842)) + +## 2024.02.07 + + +### Changed + +- Extend docker test with deploy check ([#4838](https://github.com/cookiecutter/cookiecutter-django/pull/4838)) + +- Generic UserManager ([#4836](https://github.com/cookiecutter/cookiecutter-django/pull/4836)) + +### Updated + +- Update django-allauth to 0.61.0 ([#4839](https://github.com/cookiecutter/cookiecutter-django/pull/4839)) + +- Bump gulp-postcss to 10.0.0 ([#4835](https://github.com/cookiecutter/cookiecutter-django/pull/4835)) + +- Update sentry-sdk to 1.40.2 ([#4837](https://github.com/cookiecutter/cookiecutter-django/pull/4837)) + +- Update django to 4.2.10 ([#4833](https://github.com/cookiecutter/cookiecutter-django/pull/4833)) + +## 2024.02.05 + + +### Updated + +- Update pytest to 8.0.0 ([#4813](https://github.com/cookiecutter/cookiecutter-django/pull/4813)) + +- Update pytest-sugar to 1.0.0 ([#4828](https://github.com/cookiecutter/cookiecutter-django/pull/4828)) + +- Update sphinx-autobuild to 2024.2.4 ([#4830](https://github.com/cookiecutter/cookiecutter-django/pull/4830)) + +- Update django-debug-toolbar to 4.3.0 ([#4829](https://github.com/cookiecutter/cookiecutter-django/pull/4829)) + +- Update psycopg to 3.1.18 ([#4831](https://github.com/cookiecutter/cookiecutter-django/pull/4831)) + +## 2024.01.31 + + +### Updated + +- Update python-slugify to 8.0.3 ([#4826](https://github.com/cookiecutter/cookiecutter-django/pull/4826)) + +## 2024.01.30 + + +### Updated + +- Update pytest-django to 4.8.0 ([#4823](https://github.com/cookiecutter/cookiecutter-django/pull/4823)) + +- Update sentry-sdk to 1.40.0 ([#4822](https://github.com/cookiecutter/cookiecutter-django/pull/4822)) + +- Update uvicorn to 0.27.0.post1 ([#4818](https://github.com/cookiecutter/cookiecutter-django/pull/4818)) + +## 2024.01.29 + + +### Changed + +- Fix deprecation warning about renaming of `webpack_loader.loader` to `webpack_loader.loaders` ([#4815](https://github.com/cookiecutter/cookiecutter-django/pull/4815)) + +### Documentation + +- Update mention of coverage config file to `pyproject.toml` in documentation ([#4816](https://github.com/cookiecutter/cookiecutter-django/pull/4816)) + +### Updated + +- Update black to 24.1.1 ([#4814](https://github.com/cookiecutter/cookiecutter-django/pull/4814)) + +- Auto-update pre-commit hooks ([#4817](https://github.com/cookiecutter/cookiecutter-django/pull/4817)) + +## 2024.01.27 + + +### Changed + +- Do not show webpack devserver overlay for warnings ([#4809](https://github.com/cookiecutter/cookiecutter-django/pull/4809)) + +### Updated + +- Update python-slugify to 8.0.2 ([#4805](https://github.com/cookiecutter/cookiecutter-django/pull/4805)) + +- Update coverage to 7.4.1 ([#4807](https://github.com/cookiecutter/cookiecutter-django/pull/4807)) + +## 2024.01.26 + + +### Updated + +- Update black to 24.1.0 ([#4806](https://github.com/cookiecutter/cookiecutter-django/pull/4806)) + +## 2024.01.25 + + +### Changed + +- Replace custom static & media storage classes by passing options in the `STORAGES` setting ([#4803](https://github.com/cookiecutter/cookiecutter-django/pull/4803)) + +- Add registry to Docker images names ([#4804](https://github.com/cookiecutter/cookiecutter-django/pull/4804)) + +## 2024.01.24 + + +### Changed + +- Migrate to the unified `STORAGES` setting added in Django 4.2 ([#4477](https://github.com/cookiecutter/cookiecutter-django/pull/4477)) + +### Updated + +- Update uvicorn to 0.27.0 ([#4800](https://github.com/cookiecutter/cookiecutter-django/pull/4800)) + +## 2024.01.21 + + +### Documentation + +- Update traefik doc links ([#4798](https://github.com/cookiecutter/cookiecutter-django/pull/4798)) + +### Updated + +- Bump browser-sync from 2.29.3 to 3.0.2 in /{{cookiecutter.project_slug}} ([#4765](https://github.com/cookiecutter/cookiecutter-django/pull/4765)) + +## 2024.01.19 + + +### Updated + +- Update drf-spectacular to 0.27.1 ([#4797](https://github.com/cookiecutter/cookiecutter-django/pull/4797)) + +## 2024.01.17 + + +### Changed + +- Add a test to cover `DJANGO_ADMIN_FORCE_ALLAUTH` ([#4790](https://github.com/cookiecutter/cookiecutter-django/pull/4790)) + +### Updated + +- Bump webpack-bundle-tracker to 3.0.1 ([#4781](https://github.com/cookiecutter/cookiecutter-django/pull/4781)) + +- Update django-webpack-loader to 3.0.1 ([#4793](https://github.com/cookiecutter/cookiecutter-django/pull/4793)) + +- Bump postcss-loader to 8.0.0 ([#4795](https://github.com/cookiecutter/cookiecutter-django/pull/4795)) + +- Update uvicorn to 0.26.0 ([#4794](https://github.com/cookiecutter/cookiecutter-django/pull/4794)) + +## 2024.01.16 + + +### Updated + +- Bump sass-loader from 13.3.3 to 14.0.0 in /{{cookiecutter.project_slug}} ([#4791](https://github.com/cookiecutter/cookiecutter-django/pull/4791)) + +## 2024.01.15 + + +### Documentation + +- Update allauth documentation links ([#4786](https://github.com/cookiecutter/cookiecutter-django/pull/4786)) + +### Updated + +- Update django-allauth to 0.60.1 ([#4787](https://github.com/cookiecutter/cookiecutter-django/pull/4787)) + ## 2024.01.11 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 9a384d141..9a59b6572 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -964,6 +964,13 @@ Listed in alphabetical order. + + henningbra + + henningbra + + + Henrique G. G. Pereira @@ -1076,6 +1083,13 @@ Listed in alphabetical order. + + Jens Kaeske + + jkaeske + + + Jens Nilsson @@ -1636,6 +1650,13 @@ Listed in alphabetical order. + + Paul Wulff + + mtmpaulwulff + + + Pawan Chaurasia diff --git a/docs/linters.rst b/docs/linters.rst index a4f60cc8d..1fc44f30b 100644 --- a/docs/linters.rst +++ b/docs/linters.rst @@ -4,40 +4,30 @@ Linters .. index:: linters -flake8 +ruff ------ -To run flake8: :: +Ruff is a Python linter and code formatter, written in Rust. +It is a aggregation of flake8, pylint, pyupgrade and many more. - $ flake8 +Ruff comes with a linter (``ruff check``) and a formatter (``ruff format``). +The linter is a wrapper around flake8, pylint, and other linters, +and the formatter is a wrapper around black, isort, and other formatters. -The config for flake8 is located in setup.cfg. It specifies: +To run ruff without modifying your files: :: -* Set max line length to 120 chars -* Exclude ``.tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules`` + $ ruff format --diff . + $ ruff check . -pylint ------- +Ruff is capable of fixing most of the problems it encounters. +Be sure you commit first before running `ruff` so you can restore to a savepoint (and amend afterwards to prevent a double commit. : :: -To run pylint: :: + $ ruff format . + $ ruff check --fix . + # be careful with the --unsafe-fixes option, it can break your code + $ ruff check --fix --unsafe-fixes . - $ pylint - -The config for pylint is located in .pylintrc. It specifies: - -* Use the pylint_django plugin. If using Celery, also use pylint_celery. -* Set max line length to 120 chars -* Disable linting messages for missing docstring and invalid name -* max-parents=13 - -pycodestyle ------------ - -This is included in flake8's checks, but you can also run it separately to see a more detailed report: :: - - $ pycodestyle - -The config for pycodestyle is located in setup.cfg. It specifies: - -* Set max line length to 120 chars -* Exclude ``.tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules`` +The config for ruff is located in pyproject.toml. +On of the most important option is `tool.ruff.lint.select`. +`select` determines which linters are run. In example, `DJ `_ refers to flake8-django. +For a full list of available linters, see `https://docs.astral.sh/ruff/rules/ `_ diff --git a/docs/testing.rst b/docs/testing.rst index 6387a6e1e..d403a30eb 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -43,7 +43,7 @@ If you're running the project locally with Docker, use these commands instead: : At the root of the project folder, you will find the `pytest.ini` file. You can use this to customize_ the ``pytest`` to your liking. - There is also the `.coveragerc`. This is the configuration file for the ``coverage`` tool. You can find out more about `configuring`_ ``coverage``. + The configuration for ``coverage`` can be found in ``pyproject.toml``. You can find out more about `configuring`_ ``coverage``. .. seealso:: diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py index 37f96efc0..1ddab0636 100644 --- a/hooks/post_gen_project.py +++ b/hooks/post_gen_project.py @@ -8,6 +8,7 @@ NOTE: TODO: restrict Cookiecutter Django project initialization to Python 3.x environments only """ + from __future__ import print_function import json @@ -429,10 +430,6 @@ def remove_drf_starter_files(): os.remove(os.path.join("{{cookiecutter.project_slug}}", "users", "tests", "test_swagger.py")) -def remove_storages_module(): - os.remove(os.path.join("{{cookiecutter.project_slug}}", "utils", "storages.py")) - - def main(): debug = "{{ cookiecutter.debug }}".lower() == "y" @@ -499,7 +496,6 @@ def main(): WARNING + "You chose to not use any cloud providers nor Docker, " "media files won't be served in production." + TERMINATOR ) - remove_storages_module() if "{{ cookiecutter.use_celery }}".lower() == "n": remove_celery_files() diff --git a/hooks/pre_gen_project.py b/hooks/pre_gen_project.py index 33dc2e834..2956b9ab4 100644 --- a/hooks/pre_gen_project.py +++ b/hooks/pre_gen_project.py @@ -7,6 +7,7 @@ NOTE: TODO: restrict Cookiecutter Django project initialization to Python 3.x environments only """ + from __future__ import print_function import sys diff --git a/requirements.txt b/requirements.txt index 5bec5d36a..077ce3f75 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,17 +4,15 @@ binaryornot==0.4.4 # Code quality # ------------------------------------------------------------------------------ -black==23.12.1 -isort==5.13.2 -flake8==7.0.0 -django-upgrade==1.15.0 +ruff==0.2.1 +django-upgrade==1.16.0 djlint==1.34.1 -pre-commit==3.6.0 +pre-commit==3.6.1 # Testing # ------------------------------------------------------------------------------ -tox==4.12.0 -pytest==7.4.4 +tox==4.13.0 +pytest==8.0.1 pytest-xdist==3.5.0 pytest-cookies==0.7.0 pytest-instafail==0.5.0 @@ -22,7 +20,7 @@ pyyaml==6.0.1 # Scripting # ------------------------------------------------------------------------------ -PyGithub==2.1.1 -gitpython==3.1.40 +PyGithub==2.2.0 +gitpython==3.1.42 jinja2==3.1.3 requests==2.31.0 diff --git a/scripts/create_django_issue.py b/scripts/create_django_issue.py index f9ff76545..2e59f18b0 100644 --- a/scripts/create_django_issue.py +++ b/scripts/create_django_issue.py @@ -6,6 +6,7 @@ patches, only comparing major and minor version numbers. This script handles when there are multiple Django versions that need to keep up to date. """ + from __future__ import annotations import os diff --git a/setup.py b/setup.py index 3a3d5f9cf..b29fc417a 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ except ImportError: from distutils.core import setup # We use calendar versioning -version = "2024.01.11" +version = "2024.02.19" with open("README.md") as readme_file: long_description = readme_file.read() diff --git a/tests/test_cookiecutter_generation.py b/tests/test_cookiecutter_generation.py index 31d006bed..b744a986c 100755 --- a/tests/test_cookiecutter_generation.py +++ b/tests/test_cookiecutter_generation.py @@ -180,28 +180,25 @@ def test_project_generation(cookies, context, context_override): @pytest.mark.parametrize("context_override", SUPPORTED_COMBINATIONS, ids=_fixture_id) -def test_flake8_passes(cookies, context_override): - """Generated project should pass flake8.""" +def test_ruff_check_passes(cookies, context_override): + """Generated project should pass ruff check.""" result = cookies.bake(extra_context=context_override) try: - sh.flake8(_cwd=str(result.project_path)) + sh.ruff("check", ".", _cwd=str(result.project_path)) except sh.ErrorReturnCode as e: pytest.fail(e.stdout.decode()) @auto_fixable @pytest.mark.parametrize("context_override", SUPPORTED_COMBINATIONS, ids=_fixture_id) -def test_black_passes(cookies, context_override): - """Check whether generated project passes black style.""" +def test_ruff_format_passes(cookies, context_override): + """Check whether generated project passes ruff format.""" result = cookies.bake(extra_context=context_override) try: - sh.black( - "--check", - "--diff", - "--exclude", - "migrations", + sh.ruff( + "format", ".", _cwd=str(result.project_path), ) @@ -287,7 +284,7 @@ def test_travis_invokes_pytest(cookies, context, use_docker, expected_test_scrip with open(f"{result.project_path}/.travis.yml") as travis_yml: try: yml = yaml.safe_load(travis_yml)["jobs"]["include"] - assert yml[0]["script"] == ["flake8"] + assert yml[0]["script"] == ["ruff check ."] assert yml[1]["script"] == [expected_test_script] except yaml.YAMLError as e: pytest.fail(str(e)) diff --git a/tests/test_docker.sh b/tests/test_docker.sh index 8e4055e20..c3cad3b37 100755 --- a/tests/test_docker.sh +++ b/tests/test_docker.sh @@ -30,7 +30,17 @@ docker compose -f local.yml run django python manage.py makemigrations --dry-run docker compose -f local.yml run django python manage.py makemessages --all # Make sure the check doesn't raise any warnings -docker compose -f local.yml run django python manage.py check --fail-level WARNING +docker compose -f local.yml run \ + -e DJANGO_SECRET_KEY="$(openssl rand -base64 64)" \ + -e REDIS_URL=redis://redis:6379/0 \ + -e CELERY_BROKER_URL=redis://redis:6379/0 \ + -e DJANGO_AWS_ACCESS_KEY_ID=x \ + -e DJANGO_AWS_SECRET_ACCESS_KEY=x \ + -e DJANGO_AWS_STORAGE_BUCKET_NAME=x \ + -e DJANGO_ADMIN_URL=x \ + -e MAILGUN_API_KEY=x \ + -e MAILGUN_DOMAIN=x \ + django python manage.py check --settings=config.settings.production --deploy --database default --fail-level WARNING # Generate the HTML for the documentation docker compose -f local.yml run docs make html diff --git a/tests/test_hooks.py b/tests/test_hooks.py index 6afdc400b..2ccac84b2 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -1,4 +1,5 @@ """Unit tests for the hooks""" + import os from pathlib import Path diff --git a/{{cookiecutter.project_slug}}/.devcontainer/devcontainer.json b/{{cookiecutter.project_slug}}/.devcontainer/devcontainer.json index c4158dc10..7fcd62872 100644 --- a/{{cookiecutter.project_slug}}/.devcontainer/devcontainer.json +++ b/{{cookiecutter.project_slug}}/.devcontainer/devcontainer.json @@ -37,22 +37,11 @@ "editor.codeActionsOnSave": { "source.organizeImports": true }, - // Uncomment when fixed - // https://github.com/microsoft/vscode-remote-release/issues/8474 - // "editor.defaultFormatter": "ms-python.black-formatter", - "formatting.blackPath": "/usr/local/bin/black", - "formatting.provider": "black", + "editor.defaultFormatter": "charliermarsh.ruff", "languageServer": "Pylance", - // "linting.banditPath": "/usr/local/py-utils/bin/bandit", "linting.enabled": true, - "linting.flake8Enabled": true, - "linting.flake8Path": "/usr/local/bin/flake8", "linting.mypyEnabled": true, "linting.mypyPath": "/usr/local/bin/mypy", - "linting.pycodestylePath": "/usr/local/bin/pycodestyle", - // "linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle", - "linting.pylintEnabled": true, - "linting.pylintPath": "/usr/local/bin/pylint" } }, // https://code.visualstudio.com/docs/remote/devcontainerjson-reference#_vs-code-specific-properties diff --git a/{{cookiecutter.project_slug}}/.github/workflows/ci.yml b/{{cookiecutter.project_slug}}/.github/workflows/ci.yml index e39933fe1..414ee1e60 100644 --- a/{{cookiecutter.project_slug}}/.github/workflows/ci.yml +++ b/{{cookiecutter.project_slug}}/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: - name: Run pre-commit uses: pre-commit/action@v3.0.0 - # With no caching at all the entire ci process takes 4m 30s to complete! + # With no caching at all the entire ci process takes 3m to complete! pytest: runs-on: ubuntu-latest {%- if cookiecutter.use_docker == 'n' %} @@ -69,7 +69,7 @@ jobs: {%- if cookiecutter.use_docker == 'y' %} - name: Build the Stack - run: docker compose -f local.yml build + run: docker compose -f local.yml build django - name: Run DB Migrations run: docker compose -f local.yml run --rm django python manage.py migrate diff --git a/{{cookiecutter.project_slug}}/.pre-commit-config.yaml b/{{cookiecutter.project_slug}}/.pre-commit-config.yaml index c0e1db7ca..7c9565e19 100644 --- a/{{cookiecutter.project_slug}}/.pre-commit-config.yaml +++ b/{{cookiecutter.project_slug}}/.pre-commit-config.yaml @@ -28,31 +28,20 @@ repos: exclude: '{{cookiecutter.project_slug}}/templates/' - repo: https://github.com/adamchainz/django-upgrade - rev: '1.15.0' + rev: '1.16.0' hooks: - id: django-upgrade args: ['--target-version', '4.2'] - - repo: https://github.com/asottile/pyupgrade - rev: v3.15.0 + # Run the Ruff linter. + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.2.1 hooks: - - id: pyupgrade - args: [--py311-plus] - - - repo: https://github.com/psf/black - rev: 23.12.1 - hooks: - - id: black - - - repo: https://github.com/PyCQA/isort - rev: 5.13.2 - hooks: - - id: isort - - - repo: https://github.com/PyCQA/flake8 - rev: 7.0.0 - hooks: - - id: flake8 + # Linter + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + # Formatter + - id: ruff-format - repo: https://github.com/Riverside-Healthcare/djLint rev: v1.34.1 diff --git a/{{cookiecutter.project_slug}}/.travis.yml b/{{cookiecutter.project_slug}}/.travis.yml index cd703d3ad..78709191a 100644 --- a/{{cookiecutter.project_slug}}/.travis.yml +++ b/{{cookiecutter.project_slug}}/.travis.yml @@ -10,9 +10,9 @@ jobs: include: - name: "Linter" before_script: - - pip install -q flake8 + - pip install -q ruff script: - - "flake8" + - ruff check . - name: "Django Test" {%- if cookiecutter.use_docker == 'y' %} @@ -24,7 +24,7 @@ jobs: - docker compose -f local.yml run --rm django python manage.py migrate - docker compose -f local.yml up -d script: - - "docker compose -f local.yml run django pytest" + - docker compose -f local.yml run django pytest after_failure: - docker compose -f local.yml logs {%- else %} @@ -41,5 +41,5 @@ jobs: install: - pip install -r requirements/local.txt script: - - "pytest" + - pytest {%- endif %} diff --git a/{{cookiecutter.project_slug}}/README.md b/{{cookiecutter.project_slug}}/README.md index ccf245a2f..cb7576892 100644 --- a/{{cookiecutter.project_slug}}/README.md +++ b/{{cookiecutter.project_slug}}/README.md @@ -3,7 +3,7 @@ {{ cookiecutter.description }} [![Built with Cookiecutter Django](https://img.shields.io/badge/built%20with-Cookiecutter%20Django-ff69b4.svg?logo=cookiecutter)](https://github.com/cookiecutter/cookiecutter-django/) -[![Black code style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) +[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) {%- if cookiecutter.open_source_license != "Not open source" %} diff --git a/{{cookiecutter.project_slug}}/compose/local/django/Dockerfile b/{{cookiecutter.project_slug}}/compose/local/django/Dockerfile index 703474a6f..75d5cbb9b 100644 --- a/{{cookiecutter.project_slug}}/compose/local/django/Dockerfile +++ b/{{cookiecutter.project_slug}}/compose/local/django/Dockerfile @@ -1,8 +1,8 @@ # define an alias for the specific python version used in this file. -FROM python:3.11.7-slim-bookworm as python +FROM docker.io/python:3.11.8-slim-bookworm as python # Python build stage -FROM python as python-build-stage +FROM docker.io/python as python-build-stage ARG BUILD_ENVIRONMENT=local @@ -22,7 +22,7 @@ RUN pip wheel --wheel-dir /usr/src/app/wheels \ # Python 'run' stage -FROM python as python-run-stage +FROM docker.io/python as python-run-stage ARG BUILD_ENVIRONMENT=local ARG APP_HOME=/app diff --git a/{{cookiecutter.project_slug}}/compose/local/docs/Dockerfile b/{{cookiecutter.project_slug}}/compose/local/docs/Dockerfile index 41ab15b9f..87a1b2465 100644 --- a/{{cookiecutter.project_slug}}/compose/local/docs/Dockerfile +++ b/{{cookiecutter.project_slug}}/compose/local/docs/Dockerfile @@ -1,9 +1,9 @@ # define an alias for the specific python version used in this file. -FROM python:3.11.7-slim-bookworm as python +FROM docker.io/python:3.11.8-slim-bookworm as python # Python build stage -FROM python as python-build-stage +FROM docker.io/python as python-build-stage ENV PYTHONDONTWRITEBYTECODE 1 @@ -26,7 +26,7 @@ RUN pip wheel --no-cache-dir --wheel-dir /usr/src/app/wheels \ # Python 'run' stage -FROM python as python-run-stage +FROM docker.io/python as python-run-stage ARG BUILD_ENVIRONMENT ENV PYTHONUNBUFFERED 1 diff --git a/{{cookiecutter.project_slug}}/compose/local/node/Dockerfile b/{{cookiecutter.project_slug}}/compose/local/node/Dockerfile index 41f42b625..0848ecaf8 100644 --- a/{{cookiecutter.project_slug}}/compose/local/node/Dockerfile +++ b/{{cookiecutter.project_slug}}/compose/local/node/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20-bookworm-slim +FROM docker.io/node:20-bookworm-slim WORKDIR /app diff --git a/{{cookiecutter.project_slug}}/compose/production/aws/Dockerfile b/{{cookiecutter.project_slug}}/compose/production/aws/Dockerfile index 4d1ecbb20..36eea7f8c 100644 --- a/{{cookiecutter.project_slug}}/compose/production/aws/Dockerfile +++ b/{{cookiecutter.project_slug}}/compose/production/aws/Dockerfile @@ -1,4 +1,4 @@ -FROM garland/aws-cli-docker:1.16.140 +FROM docker.io/garland/aws-cli-docker:1.16.140 COPY ./compose/production/aws/maintenance /usr/local/bin/maintenance COPY ./compose/production/postgres/maintenance/_sourced /usr/local/bin/maintenance/_sourced diff --git a/{{cookiecutter.project_slug}}/compose/production/django/Dockerfile b/{{cookiecutter.project_slug}}/compose/production/django/Dockerfile index e0da6063c..fb7fec50f 100644 --- a/{{cookiecutter.project_slug}}/compose/production/django/Dockerfile +++ b/{{cookiecutter.project_slug}}/compose/production/django/Dockerfile @@ -1,5 +1,5 @@ {% if cookiecutter.frontend_pipeline in ['Gulp', 'Webpack'] -%} -FROM node:20-bookworm-slim as client-builder +FROM docker.io/node:20-bookworm-slim as client-builder ARG APP_HOME=/app WORKDIR ${APP_HOME} @@ -25,10 +25,10 @@ RUN npm run build {%- endif %} # define an alias for the specific python version used in this file. -FROM python:3.11.7-slim-bookworm as python +FROM docker.io/python:3.11.8-slim-bookworm as python # Python build stage -FROM python as python-build-stage +FROM docker.io/python as python-build-stage ARG BUILD_ENVIRONMENT=production @@ -48,7 +48,7 @@ RUN pip wheel --wheel-dir /usr/src/app/wheels \ # Python 'run' stage -FROM python as python-run-stage +FROM docker.io/python as python-run-stage ARG BUILD_ENVIRONMENT=production ARG APP_HOME=/app diff --git a/{{cookiecutter.project_slug}}/compose/production/nginx/Dockerfile b/{{cookiecutter.project_slug}}/compose/production/nginx/Dockerfile index 911b16f71..ec2ad35cb 100644 --- a/{{cookiecutter.project_slug}}/compose/production/nginx/Dockerfile +++ b/{{cookiecutter.project_slug}}/compose/production/nginx/Dockerfile @@ -1,2 +1,2 @@ -FROM nginx:1.17.8-alpine +FROM docker.io/nginx:1.17.8-alpine COPY ./compose/production/nginx/default.conf /etc/nginx/conf.d/default.conf diff --git a/{{cookiecutter.project_slug}}/compose/production/postgres/Dockerfile b/{{cookiecutter.project_slug}}/compose/production/postgres/Dockerfile index eca29bada..5da8982f4 100644 --- a/{{cookiecutter.project_slug}}/compose/production/postgres/Dockerfile +++ b/{{cookiecutter.project_slug}}/compose/production/postgres/Dockerfile @@ -1,4 +1,4 @@ -FROM postgres:{{ cookiecutter.postgresql_version }} +FROM docker.io/postgres:{{ cookiecutter.postgresql_version }} COPY ./compose/production/postgres/maintenance /usr/local/bin/maintenance RUN chmod +x /usr/local/bin/maintenance/* diff --git a/{{cookiecutter.project_slug}}/compose/production/traefik/Dockerfile b/{{cookiecutter.project_slug}}/compose/production/traefik/Dockerfile index 321551ead..ea918e911 100644 --- a/{{cookiecutter.project_slug}}/compose/production/traefik/Dockerfile +++ b/{{cookiecutter.project_slug}}/compose/production/traefik/Dockerfile @@ -1,4 +1,4 @@ -FROM traefik:2.10.7 +FROM docker.io/traefik:2.11.0 RUN mkdir -p /etc/traefik/acme \ && touch /etc/traefik/acme/acme.json \ && chmod 600 /etc/traefik/acme/acme.json diff --git a/{{cookiecutter.project_slug}}/compose/production/traefik/traefik.yml b/{{cookiecutter.project_slug}}/compose/production/traefik/traefik.yml index 724c95cdf..f5d9e52fc 100644 --- a/{{cookiecutter.project_slug}}/compose/production/traefik/traefik.yml +++ b/{{cookiecutter.project_slug}}/compose/production/traefik/traefik.yml @@ -6,7 +6,7 @@ entryPoints: # http address: ':80' http: - # https://docs.traefik.io/routing/entrypoints/#entrypoint + # https://doc.traefik.io/traefik/routing/entrypoints/#entrypoint redirections: entryPoint: to: web-secure @@ -22,11 +22,11 @@ entryPoints: certificatesResolvers: letsencrypt: - # https://docs.traefik.io/master/https/acme/#lets-encrypt + # https://doc.traefik.io/traefik/https/acme/#lets-encrypt acme: email: '{{ cookiecutter.email }}' storage: /etc/traefik/acme/acme.json - # https://docs.traefik.io/master/https/acme/#httpchallenge + # https://doc.traefik.io/traefik/https/acme/#httpchallenge httpChallenge: entryPoint: web @@ -44,7 +44,7 @@ http: - csrf service: django tls: - # https://docs.traefik.io/master/routing/routers/#certresolver + # https://doc.traefik.io/traefik/routing/routers/#certresolver certResolver: letsencrypt {%- if cookiecutter.use_celery == 'y' %} @@ -54,7 +54,7 @@ http: - flower service: flower tls: - # https://docs.traefik.io/master/routing/routers/#certresolver + # https://doc.traefik.io/traefik/master/routing/routers/#certresolver certResolver: letsencrypt {%- endif %} {%- if cookiecutter.cloud_provider == 'None' %} @@ -76,7 +76,7 @@ http: middlewares: csrf: - # https://docs.traefik.io/master/middlewares/headers/#hostsproxyheaders + # https://doc.traefik.io/traefik/master/middlewares/http/headers/#hostsproxyheaders # https://docs.djangoproject.com/en/dev/ref/csrf/#ajax headers: hostsProxyHeaders: ['X-CSRFToken'] @@ -102,7 +102,7 @@ http: {%- endif %} providers: - # https://docs.traefik.io/master/providers/file/ + # https://doc.traefik.io/traefik/master/providers/file/ file: filename: /etc/traefik/traefik.yml watch: true diff --git a/{{cookiecutter.project_slug}}/config/api_router.py b/{{cookiecutter.project_slug}}/config/api_router.py index 743069b2c..d4de098fc 100644 --- a/{{cookiecutter.project_slug}}/config/api_router.py +++ b/{{cookiecutter.project_slug}}/config/api_router.py @@ -1,12 +1,10 @@ from django.conf import settings -from rest_framework.routers import DefaultRouter, SimpleRouter +from rest_framework.routers import DefaultRouter +from rest_framework.routers import SimpleRouter from {{ cookiecutter.project_slug }}.users.api.views import UserViewSet -if settings.DEBUG: - router = DefaultRouter() -else: - router = SimpleRouter() +router = DefaultRouter() if settings.DEBUG else SimpleRouter() router.register("users", UserViewSet) diff --git a/{{cookiecutter.project_slug}}/config/asgi.py b/{{cookiecutter.project_slug}}/config/asgi.py index 65e76ca0a..edfffbbc5 100644 --- a/{{cookiecutter.project_slug}}/config/asgi.py +++ b/{{cookiecutter.project_slug}}/config/asgi.py @@ -1,3 +1,4 @@ +# ruff: noqa """ ASGI config for {{ cookiecutter.project_name }} project. @@ -7,6 +8,7 @@ For more information on this file, see https://docs.djangoproject.com/en/dev/howto/deployment/asgi/ """ + import os import sys from pathlib import Path @@ -28,7 +30,7 @@ django_application = get_asgi_application() # application = HelloWorldApplication(application) # Import websocket application here, so apps from django_application are loaded first -from config.websocket import websocket_application # noqa isort:skip +from config.websocket import websocket_application async def application(scope, receive, send): @@ -37,4 +39,5 @@ async def application(scope, receive, send): elif scope["type"] == "websocket": await websocket_application(scope, receive, send) else: - raise NotImplementedError(f"Unknown scope type {scope['type']}") + msg = f"Unknown scope type {scope['type']}" + raise NotImplementedError(msg) diff --git a/{{cookiecutter.project_slug}}/config/settings/base.py b/{{cookiecutter.project_slug}}/config/settings/base.py index ecc5a540d..55a064e74 100644 --- a/{{cookiecutter.project_slug}}/config/settings/base.py +++ b/{{cookiecutter.project_slug}}/config/settings/base.py @@ -1,6 +1,6 @@ -""" -Base settings to build other settings files upon. -""" +# ruff: noqa: ERA001, E501 +"""Base settings to build other settings files upon.""" + from pathlib import Path import environ @@ -137,7 +137,9 @@ PASSWORD_HASHERS = [ ] # https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ - {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"}, + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, @@ -210,7 +212,7 @@ TEMPLATES = [ "{{cookiecutter.project_slug}}.users.context_processors.allauth_settings", ], }, - } + }, ] # https://docs.djangoproject.com/en/dev/ref/settings/#form-renderer @@ -274,7 +276,7 @@ LOGGING = { "level": "DEBUG", "class": "logging.StreamHandler", "formatter": "verbose", - } + }, }, "root": {"level": "INFO", "handlers": ["console"]}, } @@ -380,7 +382,7 @@ WEBPACK_LOADER = { "STATS_FILE": BASE_DIR / "webpack-stats.json", "POLL_INTERVAL": 0.1, "IGNORE": [r".+\.hot-update.js", r".+\.map"], - } + }, } {%- endif %} diff --git a/{{cookiecutter.project_slug}}/config/settings/local.py b/{{cookiecutter.project_slug}}/config/settings/local.py index 0304d6cd4..f1edb514b 100644 --- a/{{cookiecutter.project_slug}}/config/settings/local.py +++ b/{{cookiecutter.project_slug}}/config/settings/local.py @@ -1,4 +1,10 @@ -from .base import * # noqa +# ruff: noqa: E501 +from .base import * # noqa: F403 +from .base import INSTALLED_APPS +from .base import MIDDLEWARE +{%- if cookiecutter.frontend_pipeline == 'Webpack' %} +from .base import WEBPACK_LOADER +{%- endif %} from .base import env # GENERAL @@ -11,7 +17,7 @@ SECRET_KEY = env( default="!!!SET DJANGO_SECRET_KEY!!!", ) # https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts -ALLOWED_HOSTS = ["localhost", "0.0.0.0", "127.0.0.1"] +ALLOWED_HOSTS = ["localhost", "0.0.0.0", "127.0.0.1"] # noqa: S104 # CACHES # ------------------------------------------------------------------------------ @@ -20,7 +26,7 @@ CACHES = { "default": { "BACKEND": "django.core.cache.backends.locmem.LocMemCache", "LOCATION": "", - } + }, } # EMAIL @@ -37,7 +43,9 @@ EMAIL_HOST = "localhost" EMAIL_PORT = 1025 {%- else -%} # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend -EMAIL_BACKEND = env("DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.console.EmailBackend") +EMAIL_BACKEND = env( + "DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.console.EmailBackend", +) {%- endif %} {%- if cookiecutter.use_whitenoise == 'y' %} @@ -45,15 +53,15 @@ EMAIL_BACKEND = env("DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.c # WhiteNoise # ------------------------------------------------------------------------------ # http://whitenoise.evans.io/en/latest/django.html#using-whitenoise-in-development -INSTALLED_APPS = ["whitenoise.runserver_nostatic"] + INSTALLED_APPS # noqa: F405 +INSTALLED_APPS = ["whitenoise.runserver_nostatic", *INSTALLED_APPS] {% endif %} # django-debug-toolbar # ------------------------------------------------------------------------------ # https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#prerequisites -INSTALLED_APPS += ["debug_toolbar"] # noqa: F405 +INSTALLED_APPS += ["debug_toolbar"] # https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#middleware -MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"] # noqa: F405 +MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"] # https://django-debug-toolbar.readthedocs.io/en/latest/configuration.html#debug-toolbar-config DEBUG_TOOLBAR_CONFIG = { "DISABLE_PANELS": ["debug_toolbar.panels.redirects.RedirectsPanel"], @@ -80,7 +88,7 @@ if env("USE_DOCKER") == "yes": # django-extensions # ------------------------------------------------------------------------------ # https://django-extensions.readthedocs.io/en/latest/installation_instructions.html#configuration -INSTALLED_APPS += ["django_extensions"] # noqa: F405 +INSTALLED_APPS += ["django_extensions"] {% if cookiecutter.use_celery == 'y' -%} # Celery @@ -96,7 +104,7 @@ CELERY_TASK_EAGER_PROPAGATES = True {%- if cookiecutter.frontend_pipeline == 'Webpack' %} # django-webpack-loader # ------------------------------------------------------------------------------ -WEBPACK_LOADER["DEFAULT"]["CACHE"] = not DEBUG # noqa: F405 +WEBPACK_LOADER["DEFAULT"]["CACHE"] = not DEBUG {%- endif %} # Your stuff... diff --git a/{{cookiecutter.project_slug}}/config/settings/production.py b/{{cookiecutter.project_slug}}/config/settings/production.py index 971efa396..0cebe1d96 100644 --- a/{{cookiecutter.project_slug}}/config/settings/production.py +++ b/{{cookiecutter.project_slug}}/config/settings/production.py @@ -1,3 +1,4 @@ +# ruff: noqa: E501 {% if cookiecutter.use_sentry == 'y' -%} import logging @@ -12,7 +13,12 @@ from sentry_sdk.integrations.logging import LoggingIntegration from sentry_sdk.integrations.redis import RedisIntegration {% endif -%} -from .base import * # noqa +from .base import * # noqa: F403 +from .base import DATABASES +from .base import INSTALLED_APPS +{%- if cookiecutter.use_drf == "y" %} +from .base import SPECTACULAR_SETTINGS +{%- endif %} from .base import env # GENERAL @@ -24,7 +30,7 @@ ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["{{ cookiecutter.domai # DATABASES # ------------------------------------------------------------------------------ -DATABASES["default"]["CONN_MAX_AGE"] = env.int("CONN_MAX_AGE", default=60) # noqa: F405 +DATABASES["default"]["CONN_MAX_AGE"] = env.int("CONN_MAX_AGE", default=60) # CACHES # ------------------------------------------------------------------------------ @@ -38,7 +44,7 @@ CACHES = { # https://github.com/jazzband/django-redis#memcached-exceptions-behavior "IGNORE_EXCEPTIONS": True, }, - } + }, } # SECURITY @@ -56,17 +62,23 @@ CSRF_COOKIE_SECURE = True # TODO: set this to 60 seconds first and then to 518400 once you prove the former works SECURE_HSTS_SECONDS = 60 # https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-include-subdomains -SECURE_HSTS_INCLUDE_SUBDOMAINS = env.bool("DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS", default=True) +SECURE_HSTS_INCLUDE_SUBDOMAINS = env.bool( + "DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS", + default=True, +) # https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-preload SECURE_HSTS_PRELOAD = env.bool("DJANGO_SECURE_HSTS_PRELOAD", default=True) # https://docs.djangoproject.com/en/dev/ref/middleware/#x-content-type-options-nosniff -SECURE_CONTENT_TYPE_NOSNIFF = env.bool("DJANGO_SECURE_CONTENT_TYPE_NOSNIFF", default=True) +SECURE_CONTENT_TYPE_NOSNIFF = env.bool( + "DJANGO_SECURE_CONTENT_TYPE_NOSNIFF", + default=True, +) {% if cookiecutter.cloud_provider != 'None' -%} # STORAGES # ------------------------------------------------------------------------------ # https://django-storages.readthedocs.io/en/latest/#installation -INSTALLED_APPS += ["storages"] # noqa: F405 +INSTALLED_APPS += ["storages"] {%- endif -%} {% if cookiecutter.cloud_provider == 'AWS' %} # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings @@ -103,35 +115,75 @@ AZURE_CONTAINER = env("DJANGO_AZURE_CONTAINER_NAME") {% endif -%} {% if cookiecutter.cloud_provider != 'None' or cookiecutter.use_whitenoise == 'y' -%} -# STATIC +# STATIC & MEDIA # ------------------------ -{% endif -%} -{% if cookiecutter.use_whitenoise == 'y' -%} -STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" -{% elif cookiecutter.cloud_provider == 'AWS' -%} -STATICFILES_STORAGE = "{{cookiecutter.project_slug}}.utils.storages.StaticS3Storage" +STORAGES = { +{%- if cookiecutter.use_whitenoise == 'y' %} + "default": { + "BACKEND": "django.core.files.storage.FileSystemStorage", + }, + "staticfiles": { + "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", + }, +{%- elif cookiecutter.cloud_provider == 'AWS' %} + "default": { + "BACKEND": "storages.backends.s3.S3Storage", + "OPTIONS": { + "location": "media", + "file_overwrite": False, + }, + }, + "staticfiles": { + "BACKEND": "storages.backends.s3.S3Storage", + "OPTIONS": { + "location": "static", + "default_acl": "public-read", + }, + }, +{%- elif cookiecutter.cloud_provider == 'GCP' %} + "default": { + "BACKEND": "storages.backends.gcloud.GoogleCloudStorage", + "OPTIONS": { + "location": "media", + "file_overwrite": False, + }, + }, + "staticfiles": { + "BACKEND": "storages.backends.gcloud.GoogleCloudStorage", + "OPTIONS": { + "location": "static", + "default_acl": "publicRead", + }, + }, +{%- elif cookiecutter.cloud_provider == 'Azure' %} + "default": { + "BACKEND": "storages.backends.azure_storage.AzureStorage", + "OPTIONS": { + "location": "media", + "file_overwrite": False, + }, + }, + "staticfiles": { + "BACKEND": "storages.backends.azure_storage.AzureStorage", + "OPTIONS": { + "location": "static", + }, + }, +{%- endif %} +} +{%- endif %} + +{%- if cookiecutter.cloud_provider == 'AWS' %} +MEDIA_URL = f"https://{aws_s3_domain}/media/" COLLECTFAST_STRATEGY = "collectfast.strategies.boto3.Boto3Strategy" STATIC_URL = f"https://{aws_s3_domain}/static/" -{% elif cookiecutter.cloud_provider == 'GCP' -%} -STATICFILES_STORAGE = "{{cookiecutter.project_slug}}.utils.storages.StaticGoogleCloudStorage" +{%- elif cookiecutter.cloud_provider == 'GCP' %} +MEDIA_URL = f"https://storage.googleapis.com/{GS_BUCKET_NAME}/media/" COLLECTFAST_STRATEGY = "collectfast.strategies.gcloud.GoogleCloudStrategy" STATIC_URL = f"https://storage.googleapis.com/{GS_BUCKET_NAME}/static/" -{% elif cookiecutter.cloud_provider == 'Azure' -%} -STATICFILES_STORAGE = "{{cookiecutter.project_slug}}.utils.storages.StaticAzureStorage" -STATIC_URL = f"https://{AZURE_ACCOUNT_NAME}.blob.core.windows.net/static/" -{% endif -%} - -# MEDIA -# ------------------------------------------------------------------------------ -{%- if cookiecutter.cloud_provider == 'AWS' %} -DEFAULT_FILE_STORAGE = "{{cookiecutter.project_slug}}.utils.storages.MediaS3Storage" -MEDIA_URL = f"https://{aws_s3_domain}/media/" -{%- elif cookiecutter.cloud_provider == 'GCP' %} -DEFAULT_FILE_STORAGE = "{{cookiecutter.project_slug}}.utils.storages.MediaGoogleCloudStorage" -MEDIA_URL = f"https://storage.googleapis.com/{GS_BUCKET_NAME}/media/" {%- elif cookiecutter.cloud_provider == 'Azure' %} -DEFAULT_FILE_STORAGE = "{{cookiecutter.project_slug}}.utils.storages.MediaAzureStorage" MEDIA_URL = f"https://{AZURE_ACCOUNT_NAME}.blob.core.windows.net/media/" +STATIC_URL = f"https://{AZURE_ACCOUNT_NAME}.blob.core.windows.net/static/" {%- endif %} # EMAIL @@ -157,7 +209,7 @@ ADMIN_URL = env("DJANGO_ADMIN_URL") # Anymail # ------------------------------------------------------------------------------ # https://anymail.readthedocs.io/en/stable/installation/#installing-anymail -INSTALLED_APPS += ["anymail"] # noqa: F405 +INSTALLED_APPS += ["anymail"] # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend # https://anymail.readthedocs.io/en/stable/installation/#anymail-settings-reference {%- if cookiecutter.mail_service == 'Mailgun' %} @@ -230,10 +282,10 @@ COMPRESS_ENABLED = env.bool("COMPRESS_ENABLED", default=True) COMPRESS_STORAGE = "compressor.storage.GzipCompressorFileStorage" {%- elif cookiecutter.cloud_provider in ('AWS', 'GCP', 'Azure') and cookiecutter.use_whitenoise == 'n' %} # https://django-compressor.readthedocs.io/en/latest/settings/#django.conf.settings.COMPRESS_STORAGE -COMPRESS_STORAGE = STATICFILES_STORAGE +COMPRESS_STORAGE = STORAGES["staticfiles"]["BACKEND"] {%- endif %} # https://django-compressor.readthedocs.io/en/latest/settings/#django.conf.settings.COMPRESS_URL -COMPRESS_URL = STATIC_URL{% if cookiecutter.use_whitenoise == 'y' or cookiecutter.cloud_provider == 'None' %} # noqa: F405{% endif %} +COMPRESS_URL = STATIC_URL{% if cookiecutter.use_whitenoise == 'y' or cookiecutter.cloud_provider == 'None' %}{% endif %} {%- if cookiecutter.use_whitenoise == 'y' %} # https://django-compressor.readthedocs.io/en/latest/settings/#django.conf.settings.COMPRESS_OFFLINE COMPRESS_OFFLINE = True # Offline compression is required when using Whitenoise @@ -251,7 +303,7 @@ COMPRESS_FILTERS = { # Collectfast # ------------------------------------------------------------------------------ # https://github.com/antonagestam/collectfast#installation -INSTALLED_APPS = ["collectfast"] + INSTALLED_APPS # noqa: F405 +INSTALLED_APPS = ["collectfast", *INSTALLED_APPS] {% endif %} # LOGGING # ------------------------------------------------------------------------------ @@ -311,7 +363,7 @@ LOGGING = { "level": "DEBUG", "class": "logging.StreamHandler", "formatter": "verbose", - } + }, }, "root": {"level": "INFO", "handlers": ["console"]}, "loggers": { @@ -363,7 +415,7 @@ sentry_sdk.init( # django-rest-framework # ------------------------------------------------------------------------------- # Tools that generate code samples can use SERVERS to point to the correct domain -SPECTACULAR_SETTINGS["SERVERS"] = [ # noqa: F405 +SPECTACULAR_SETTINGS["SERVERS"] = [ {"url": "https://{{ cookiecutter.domain_name }}", "description": "Production server"}, ] diff --git a/{{cookiecutter.project_slug}}/config/settings/test.py b/{{cookiecutter.project_slug}}/config/settings/test.py index 68126182e..696b48710 100644 --- a/{{cookiecutter.project_slug}}/config/settings/test.py +++ b/{{cookiecutter.project_slug}}/config/settings/test.py @@ -2,7 +2,8 @@ With these settings, tests run faster. """ -from .base import * # noqa +from .base import * # noqa: F403 +from .base import TEMPLATES from .base import env # GENERAL @@ -27,17 +28,17 @@ EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" # DEBUGGING FOR TEMPLATES # ------------------------------------------------------------------------------ -TEMPLATES[0]["OPTIONS"]["debug"] = True # type: ignore # noqa: F405 +TEMPLATES[0]["OPTIONS"]["debug"] = True # type: ignore[index] # MEDIA # ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/dev/ref/settings/#media-url -MEDIA_URL = 'http://media.testserver' +MEDIA_URL = "http://media.testserver" {%- if cookiecutter.frontend_pipeline == 'Webpack' %} # django-webpack-loader # ------------------------------------------------------------------------------ -WEBPACK_LOADER["DEFAULT"]["LOADER_CLASS"] = "webpack_loader.loader.FakeWebpackLoader" # noqa: F405 +WEBPACK_LOADER["DEFAULT"]["LOADER_CLASS"] = "webpack_loader.loaders.FakeWebpackLoader" # noqa: F405 {%- endif %} # Your stuff... diff --git a/{{cookiecutter.project_slug}}/config/urls.py b/{{cookiecutter.project_slug}}/config/urls.py index 7c5ad1a7e..5d9301b67 100644 --- a/{{cookiecutter.project_slug}}/config/urls.py +++ b/{{cookiecutter.project_slug}}/config/urls.py @@ -1,27 +1,37 @@ +# ruff: noqa from django.conf import settings from django.conf.urls.static import static from django.contrib import admin {%- if cookiecutter.use_async == 'y' %} from django.contrib.staticfiles.urls import staticfiles_urlpatterns {%- endif %} -from django.urls import include, path +from django.urls import include +from django.urls import path from django.views import defaults as default_views from django.views.generic import TemplateView {%- if cookiecutter.use_drf == 'y' %} -from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView +from drf_spectacular.views import SpectacularAPIView +from drf_spectacular.views import SpectacularSwaggerView from rest_framework.authtoken.views import obtain_auth_token {%- endif %} urlpatterns = [ path("", TemplateView.as_view(template_name="pages/home.html"), name="home"), - path("about/", TemplateView.as_view(template_name="pages/about.html"), name="about"), + path( + "about/", + TemplateView.as_view(template_name="pages/about.html"), + name="about", + ), # Django Admin, use {% raw %}{% url 'admin:index' %}{% endraw %} path(settings.ADMIN_URL, admin.site.urls), # User management path("users/", include("{{ cookiecutter.project_slug }}.users.urls", namespace="users")), path("accounts/", include("allauth.urls")), # Your stuff: custom urls includes go here -] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + # ... + # Media files + *static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT), +] {%- if cookiecutter.use_async == 'y' %} if settings.DEBUG: # Static file serving when using Gunicorn + Uvicorn for local web socket development diff --git a/{{cookiecutter.project_slug}}/config/wsgi.py b/{{cookiecutter.project_slug}}/config/wsgi.py index 3fd809ef3..73a6cddcb 100644 --- a/{{cookiecutter.project_slug}}/config/wsgi.py +++ b/{{cookiecutter.project_slug}}/config/wsgi.py @@ -1,3 +1,4 @@ +# ruff: noqa """ WSGI config for {{ cookiecutter.project_name }} project. @@ -13,6 +14,7 @@ middleware here, or combine a Django application with an application of another framework. """ + import os import sys from pathlib import Path diff --git a/{{cookiecutter.project_slug}}/docs/conf.py b/{{cookiecutter.project_slug}}/docs/conf.py index c640e1c63..40d59dbbb 100644 --- a/{{cookiecutter.project_slug}}/docs/conf.py +++ b/{{cookiecutter.project_slug}}/docs/conf.py @@ -1,3 +1,4 @@ +# ruff: noqa # Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full diff --git a/{{cookiecutter.project_slug}}/local.yml b/{{cookiecutter.project_slug}}/local.yml index 6609f8053..d924b739f 100644 --- a/{{cookiecutter.project_slug}}/local.yml +++ b/{{cookiecutter.project_slug}}/local.yml @@ -58,7 +58,7 @@ services: {%- if cookiecutter.use_mailpit == 'y' %} mailpit: - image: axllent/mailpit:latest + image: docker.io/axllent/mailpit:latest container_name: {{ cookiecutter.project_slug }}_local_mailpit ports: - "8025:8025" @@ -67,7 +67,7 @@ services: {%- if cookiecutter.use_celery == 'y' %} redis: - image: redis:6 + image: docker.io/redis:6 container_name: {{ cookiecutter.project_slug }}_local_redis celeryworker: diff --git a/{{cookiecutter.project_slug}}/manage.py b/{{cookiecutter.project_slug}}/manage.py index c44cc826d..a39871814 100755 --- a/{{cookiecutter.project_slug}}/manage.py +++ b/{{cookiecutter.project_slug}}/manage.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# ruff: noqa import os import sys from pathlib import Path @@ -13,7 +14,7 @@ if __name__ == "__main__": # issue is really that Django is missing to avoid masking other # exceptions on Python 2. try: - import django # noqa + import django except ImportError: raise ImportError( "Couldn't import Django. Are you sure it's installed and " diff --git a/{{cookiecutter.project_slug}}/merge_production_dotenvs_in_dotenv.py b/{{cookiecutter.project_slug}}/merge_production_dotenvs_in_dotenv.py index 35139fb2e..c83ed7166 100644 --- a/{{cookiecutter.project_slug}}/merge_production_dotenvs_in_dotenv.py +++ b/{{cookiecutter.project_slug}}/merge_production_dotenvs_in_dotenv.py @@ -1,3 +1,4 @@ +# ruff: noqa import os from collections.abc import Sequence from pathlib import Path diff --git a/{{cookiecutter.project_slug}}/package.json b/{{cookiecutter.project_slug}}/package.json index 6b8371edf..efa2136e5 100644 --- a/{{cookiecutter.project_slug}}/package.json +++ b/{{cookiecutter.project_slug}}/package.json @@ -8,7 +8,7 @@ "autoprefixer": "^10.4.0", "babel-loader": "^9.1.2", "bootstrap": "^5.2.3", - "browser-sync": "^2.27.7", + "browser-sync": "^3.0.2", "css-loader": "^6.5.1", "gulp-concat": "^2.6.1", "concurrently": "^8.0.1", @@ -16,7 +16,7 @@ "gulp": "^4.0.2", "gulp-imagemin": "^7.1.0", "gulp-plumber": "^1.2.1", - "gulp-postcss": "^9.0.1", + "gulp-postcss": "^10.0.0", "gulp-rename": "^2.0.0", "gulp-sass": "^5.0.0", "gulp-uglify-es": "^3.0.0", @@ -24,12 +24,12 @@ "node-sass-tilde-importer": "^1.0.2", "pixrem": "^5.0.0", "postcss": "^8.3.11", - "postcss-loader": "^7.0.2", + "postcss-loader": "^8.0.0", "postcss-preset-env": "^9.0.0", "sass": "^1.43.4", - "sass-loader": "^13.2.0", + "sass-loader": "^14.0.0", "webpack": "^5.65.0", - "webpack-bundle-tracker": "^2.0.0", + "webpack-bundle-tracker": "^3.0.1", "webpack-cli": "^5.0.1", "webpack-dev-server": "^4.6.0", "webpack-merge": "^5.8.0" diff --git a/{{cookiecutter.project_slug}}/production.yml b/{{cookiecutter.project_slug}}/production.yml index 30d72d61e..f7bf5284f 100644 --- a/{{cookiecutter.project_slug}}/production.yml +++ b/{{cookiecutter.project_slug}}/production.yml @@ -67,7 +67,7 @@ services: {%- endif %} redis: - image: redis:6 + image: docker.io/redis:6 {%- if cookiecutter.use_celery == 'y' %} celeryworker: diff --git a/{{cookiecutter.project_slug}}/pyproject.toml b/{{cookiecutter.project_slug}}/pyproject.toml index 7e4c9aa9c..a056c71c3 100644 --- a/{{cookiecutter.project_slug}}/pyproject.toml +++ b/{{cookiecutter.project_slug}}/pyproject.toml @@ -16,25 +16,6 @@ include = ["{{cookiecutter.project_slug}}/**"] omit = ["*/migrations/*", "*/tests/*"] plugins = ["django_coverage_plugin"] - -# ==== black ==== -[tool.black] -line-length = 119 -target-version = ['py311'] - - -# ==== isort ==== -[tool.isort] -profile = "black" -line_length = 119 -known_first_party = [ - "{{cookiecutter.project_slug}}", - "config", -] -skip = ["venv/"] -skip_glob = ["**/migrations/*.py"] - - # ==== mypy ==== [tool.mypy] python_version = "3.11" @@ -58,40 +39,6 @@ ignore_errors = true [tool.django-stubs] django_settings_module = "config.settings.test" - -# ==== PyLint ==== -[tool.pylint.MASTER] -load-plugins = [ - "pylint_django", -{%- if cookiecutter.use_celery == "y" %} - "pylint_celery", -{%- endif %} -] -django-settings-module = "config.settings.local" - -[tool.pylint.FORMAT] -max-line-length = 119 - -[tool.pylint."MESSAGES CONTROL"] -disable = [ - "missing-docstring", - "invalid-name", -] - -[tool.pylint.DESIGN] -max-parents = 13 - -[tool.pylint.TYPECHECK] -generated-members = [ - "REQUEST", - "acl_users", - "aq_parent", - "[a-zA-Z]+_set{1,2}", - "save", - "delete", -] - - # ==== djLint ==== [tool.djlint] blank_line_after_tag = "load,extends" @@ -110,3 +57,112 @@ indent_size = 2 [tool.djlint.js] indent_size = 2 + +[tool.ruff] +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "venv", + "*/migrations/*.py", + "staticfiles/*" +] +# Same as Django: https://github.com/cookiecutter/cookiecutter-django/issues/4792. +line-length = 88 +indent-width = 4 +target-version = "py311" + +[tool.ruff.lint] +select = [ + "F", + "E", + "W", + "C90", + "I", + "N", + "UP", + "YTT", + # "ANN", # flake8-annotations: we should support this in the future but 100+ errors atm + "ASYNC", + "S", + "BLE", + "FBT", + "B", + "A", + "COM", + "C4", + "DTZ", + "T10", + "DJ", + "EM", + "EXE", + "FA", + 'ISC', + "ICN", + "G", + 'INP', + 'PIE', + "T20", + 'PYI', + 'PT', + "Q", + "RSE", + "RET", + "SLF", + "SLOT", + "SIM", + "TID", + "TCH", + "INT", + # "ARG", # Unused function argument + "PTH", + "ERA", + "PD", + "PGH", + "PL", + "TRY", + "FLY", + # "NPY", + # "AIR", + "PERF", + # "FURB", + # "LOG", + "RUF" +] +ignore = [ + "S101", # Use of assert detected https://docs.astral.sh/ruff/rules/assert/ + "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar` + "SIM102" # sometimes it's better to nest +] +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = [] +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" + +[tool.ruff.lint.isort] +force-single-line = true diff --git a/{{cookiecutter.project_slug}}/requirements/base.txt b/{{cookiecutter.project_slug}}/requirements/base.txt index 87b9ce956..f090018ef 100644 --- a/{{cookiecutter.project_slug}}/requirements/base.txt +++ b/{{cookiecutter.project_slug}}/requirements/base.txt @@ -1,4 +1,4 @@ -python-slugify==8.0.1 # https://github.com/un33k/python-slugify +python-slugify==8.0.4 # https://github.com/un33k/python-slugify Pillow==10.2.0 # https://github.com/python-pillow/Pillow {%- if cookiecutter.frontend_pipeline == 'Django Compressor' %} {%- if cookiecutter.windows == 'y' and cookiecutter.use_docker == 'n' %} @@ -23,15 +23,15 @@ flower==2.0.1 # https://github.com/mher/flower {%- endif %} {%- endif %} {%- if cookiecutter.use_async == 'y' %} -uvicorn[standard]==0.25.0 # https://github.com/encode/uvicorn +uvicorn[standard]==0.27.1 # https://github.com/encode/uvicorn {%- endif %} # Django # ------------------------------------------------------------------------------ -django==4.2.9 # pyup: < 5.0 # https://www.djangoproject.com/ +django==4.2.10 # pyup: < 5.0 # https://www.djangoproject.com/ django-environ==0.11.2 # https://github.com/joke2k/django-environ -django-model-utils==4.3.1 # https://github.com/jazzband/django-model-utils -django-allauth==0.60.0 # https://github.com/pennersr/django-allauth +django-model-utils==4.4.0 # https://github.com/jazzband/django-model-utils +django-allauth==0.61.1 # https://github.com/pennersr/django-allauth django-crispy-forms==2.1 # https://github.com/django-crispy-forms/django-crispy-forms crispy-bootstrap5==2023.10 # https://github.com/django-crispy-forms/crispy-bootstrap5 {%- if cookiecutter.frontend_pipeline == 'Django Compressor' %} @@ -43,8 +43,8 @@ django-redis==5.4.0 # https://github.com/jazzband/django-redis djangorestframework==3.14.0 # https://github.com/encode/django-rest-framework django-cors-headers==4.3.1 # https://github.com/adamchainz/django-cors-headers # DRF-spectacular for api documentation -drf-spectacular==0.27.0 # https://github.com/tfranzel/drf-spectacular +drf-spectacular==0.27.1 # https://github.com/tfranzel/drf-spectacular {%- endif %} {%- if cookiecutter.frontend_pipeline == 'Webpack' %} -django-webpack-loader==2.0.1 # https://github.com/django-webpack/django-webpack-loader +django-webpack-loader==3.0.1 # https://github.com/django-webpack/django-webpack-loader {%- endif %} diff --git a/{{cookiecutter.project_slug}}/requirements/local.txt b/{{cookiecutter.project_slug}}/requirements/local.txt index 2a80055c0..4c142ff66 100644 --- a/{{cookiecutter.project_slug}}/requirements/local.txt +++ b/{{cookiecutter.project_slug}}/requirements/local.txt @@ -1,11 +1,11 @@ --r base.txt +-r production.txt Werkzeug[watchdog]==3.0.1 # https://github.com/pallets/werkzeug ipdb==0.13.13 # https://github.com/gotcha/ipdb {%- if cookiecutter.use_docker == 'y' %} -psycopg[c]==3.1.17 # https://github.com/psycopg/psycopg +psycopg[c]==3.1.18 # https://github.com/psycopg/psycopg {%- else %} -psycopg[binary]==3.1.17 # https://github.com/psycopg/psycopg +psycopg[binary]==3.1.18 # https://github.com/psycopg/psycopg {%- endif %} {%- if cookiecutter.use_async == 'y' or cookiecutter.use_celery == 'y' %} watchfiles==0.21.0 # https://github.com/samuelcolvin/watchfiles @@ -15,8 +15,8 @@ watchfiles==0.21.0 # https://github.com/samuelcolvin/watchfiles # ------------------------------------------------------------------------------ mypy==1.7.1 # https://github.com/python/mypy django-stubs[compatible-mypy]==4.2.7 # https://github.com/typeddjango/django-stubs -pytest==7.4.4 # https://github.com/pytest-dev/pytest -pytest-sugar==0.9.7 # https://github.com/Frozenball/pytest-sugar +pytest==8.0.1 # https://github.com/pytest-dev/pytest +pytest-sugar==1.0.0 # https://github.com/Frozenball/pytest-sugar {%- if cookiecutter.use_drf == "y" %} djangorestframework-stubs[compatible-mypy]==3.14.5 # https://github.com/typeddjango/djangorestframework-stubs {%- endif %} @@ -24,26 +24,20 @@ djangorestframework-stubs[compatible-mypy]==3.14.5 # https://github.com/typeddj # Documentation # ------------------------------------------------------------------------------ sphinx==7.2.6 # https://github.com/sphinx-doc/sphinx -sphinx-autobuild==2021.3.14 # https://github.com/GaretJax/sphinx-autobuild +sphinx-autobuild==2024.2.4 # https://github.com/GaretJax/sphinx-autobuild # Code quality # ------------------------------------------------------------------------------ -flake8==7.0.0 # https://github.com/PyCQA/flake8 -flake8-isort==6.1.1 # https://github.com/gforcada/flake8-isort -coverage==7.4.0 # https://github.com/nedbat/coveragepy -black==23.12.1 # https://github.com/psf/black +ruff==0.2.1 # https://github.com/astral-sh/ruff +coverage==7.4.1 # https://github.com/nedbat/coveragepy djlint==1.34.1 # https://github.com/Riverside-Healthcare/djLint -pylint-django==2.5.5 # https://github.com/PyCQA/pylint-django -{%- if cookiecutter.use_celery == 'y' %} -pylint-celery==0.3 # https://github.com/PyCQA/pylint-celery -{%- endif %} -pre-commit==3.6.0 # https://github.com/pre-commit/pre-commit +pre-commit==3.6.1 # https://github.com/pre-commit/pre-commit # Django # ------------------------------------------------------------------------------ factory-boy==3.3.0 # https://github.com/FactoryBoy/factory_boy -django-debug-toolbar==4.2.0 # https://github.com/jazzband/django-debug-toolbar +django-debug-toolbar==4.3.0 # https://github.com/jazzband/django-debug-toolbar django-extensions==3.2.3 # https://github.com/django-extensions/django-extensions django-coverage-plugin==3.1.0 # https://github.com/nedbat/django_coverage_plugin -pytest-django==4.7.0 # https://github.com/pytest-dev/pytest-django +pytest-django==4.8.0 # https://github.com/pytest-dev/pytest-django diff --git a/{{cookiecutter.project_slug}}/requirements/production.txt b/{{cookiecutter.project_slug}}/requirements/production.txt index 80afd9e18..4d96e86e1 100644 --- a/{{cookiecutter.project_slug}}/requirements/production.txt +++ b/{{cookiecutter.project_slug}}/requirements/production.txt @@ -3,12 +3,12 @@ -r base.txt gunicorn==21.2.0 # https://github.com/benoitc/gunicorn -psycopg[c]==3.1.17 # https://github.com/psycopg/psycopg +psycopg[c]==3.1.18 # https://github.com/psycopg/psycopg {%- if cookiecutter.use_whitenoise == 'n' %} Collectfast==2.2.0 # https://github.com/antonagestam/collectfast {%- endif %} {%- if cookiecutter.use_sentry == "y" %} -sentry-sdk==1.39.2 # https://github.com/getsentry/sentry-python +sentry-sdk==1.40.5 # https://github.com/getsentry/sentry-python {%- endif %} {%- if cookiecutter.use_docker == "n" and cookiecutter.windows == "y" %} hiredis==2.3.2 # https://github.com/redis/hiredis-py diff --git a/{{cookiecutter.project_slug}}/setup.cfg b/{{cookiecutter.project_slug}}/setup.cfg deleted file mode 100644 index 2412f1746..000000000 --- a/{{cookiecutter.project_slug}}/setup.cfg +++ /dev/null @@ -1,11 +0,0 @@ -# flake8 and pycodestyle don't support pyproject.toml -# https://github.com/PyCQA/flake8/issues/234 -# https://github.com/PyCQA/pycodestyle/issues/813 -[flake8] -max-line-length = 119 -exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv,.venv -extend-ignore = E203 - -[pycodestyle] -max-line-length = 119 -exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv,.venv diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/utils/__init__.py b/{{cookiecutter.project_slug}}/tests/__init__.py similarity index 100% rename from {{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/utils/__init__.py rename to {{cookiecutter.project_slug}}/tests/__init__.py diff --git a/{{cookiecutter.project_slug}}/webpack/dev.config.js b/{{cookiecutter.project_slug}}/webpack/dev.config.js index c2f14abb1..8276c3489 100644 --- a/{{cookiecutter.project_slug}}/webpack/dev.config.js +++ b/{{cookiecutter.project_slug}}/webpack/dev.config.js @@ -13,6 +13,13 @@ module.exports = merge(commonConfig, { '/': 'http://django:8000', {%- endif %} }, + client: { + overlay: { + errors: true, + warnings: false, + runtimeErrors: true, + }, + }, // We need hot=false (Disable HMR) to set liveReload=true hot: false, liveReload: true, diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/__init__.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/__init__.py index 150a914ee..fb6532709 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/__init__.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/__init__.py @@ -1,2 +1,5 @@ __version__ = "{{ cookiecutter.version }}" -__version_info__ = tuple(int(num) if num.isdigit() else num for num in __version__.replace("-", ".", 1).split(".")) +__version_info__ = tuple( + int(num) if num.isdigit() else num + for num in __version__.replace("-", ".", 1).split(".") +) diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/conftest.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/conftest.py index 7095a4714..98efcd75e 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/conftest.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/conftest.py @@ -5,10 +5,10 @@ from {{ cookiecutter.project_slug }}.users.tests.factories import UserFactory @pytest.fixture(autouse=True) -def media_storage(settings, tmpdir): +def _media_storage(settings, tmpdir) -> None: settings.MEDIA_ROOT = tmpdir.strpath -@pytest.fixture +@pytest.fixture() def user(db) -> User: return UserFactory() diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/contrib/sites/migrations/0001_initial.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/contrib/sites/migrations/0001_initial.py index 304cd6d7c..fd76afb25 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/contrib/sites/migrations/0001_initial.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/contrib/sites/migrations/0001_initial.py @@ -1,6 +1,7 @@ import django.contrib.sites.models from django.contrib.sites.models import _simple_domain_name_validator -from django.db import migrations, models +from django.db import migrations +from django.db import models class Migration(migrations.Migration): @@ -38,5 +39,5 @@ class Migration(migrations.Migration): }, bases=(models.Model,), managers=[("objects", django.contrib.sites.models.SiteManager())], - ) + ), ] diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/contrib/sites/migrations/0002_alter_domain_unique.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/contrib/sites/migrations/0002_alter_domain_unique.py index 2c8d6dac0..4a44a6a92 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/contrib/sites/migrations/0002_alter_domain_unique.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/contrib/sites/migrations/0002_alter_domain_unique.py @@ -1,5 +1,6 @@ import django.contrib.sites.models -from django.db import migrations, models +from django.db import migrations +from django.db import models class Migration(migrations.Migration): diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/contrib/sites/migrations/0003_set_site_domain_and_name.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/contrib/sites/migrations/0003_set_site_domain_and_name.py index e1822375b..85ee2d9c1 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/contrib/sites/migrations/0003_set_site_domain_and_name.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/contrib/sites/migrations/0003_set_site_domain_and_name.py @@ -23,7 +23,7 @@ def _update_or_create_site_with_sequence(site_model, connection, domain, name): # site is created. # To avoid this, we need to manually update DB sequence and make sure it's # greater than the maximum value. - max_id = site_model.objects.order_by('-id').first().id + max_id = site_model.objects.order_by("-id").first().id with connection.cursor() as cursor: cursor.execute("SELECT last_value from django_site_id_seq") (current_id,) = cursor.fetchone() diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/adapters.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/adapters.py index f9ae43a8e..484f686ad 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/adapters.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/adapters.py @@ -5,10 +5,11 @@ import typing from allauth.account.adapter import DefaultAccountAdapter from allauth.socialaccount.adapter import DefaultSocialAccountAdapter from django.conf import settings -from django.http import HttpRequest if typing.TYPE_CHECKING: from allauth.socialaccount.models import SocialLogin + from django.http import HttpRequest + from {{cookiecutter.project_slug}}.users.models import User @@ -18,10 +19,19 @@ class AccountAdapter(DefaultAccountAdapter): class SocialAccountAdapter(DefaultSocialAccountAdapter): - def is_open_for_signup(self, request: HttpRequest, sociallogin: SocialLogin) -> bool: + def is_open_for_signup( + self, + request: HttpRequest, + sociallogin: SocialLogin, + ) -> bool: return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True) - def populate_user(self, request: HttpRequest, sociallogin: SocialLogin, data: dict[str, typing.Any]) -> User: + def populate_user( + self, + request: HttpRequest, + sociallogin: SocialLogin, + data: dict[str, typing.Any], + ) -> User: """ Populates user information from social provider info. diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/admin.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/admin.py index 8154a198e..0e7d8471b 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/admin.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/admin.py @@ -7,6 +7,7 @@ from django.utils.translation import gettext_lazy as _ from {{ cookiecutter.project_slug }}.users.forms import UserAdminChangeForm, UserAdminCreationForm from {{ cookiecutter.project_slug }}.users.models import User + if settings.DJANGO_ADMIN_FORCE_ALLAUTH: # Force the `admin` sign in process to go through the `django-allauth` workflow: # https://docs.allauth.org/en/latest/common/admin.html#admin diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/api/views.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/api/views.py index 96f2b26aa..7a521cdfe 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/api/views.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/api/views.py @@ -1,6 +1,8 @@ from rest_framework import status from rest_framework.decorators import action -from rest_framework.mixins import ListModelMixin, RetrieveModelMixin, UpdateModelMixin +from rest_framework.mixins import ListModelMixin +from rest_framework.mixins import RetrieveModelMixin +from rest_framework.mixins import UpdateModelMixin from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/apps.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/apps.py index 92e7a74ec..5c3d4fe08 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/apps.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/apps.py @@ -1,3 +1,5 @@ +import contextlib + from django.apps import AppConfig from django.utils.translation import gettext_lazy as _ @@ -7,7 +9,5 @@ class UsersConfig(AppConfig): verbose_name = _("Users") def ready(self): - try: + with contextlib.suppress(ImportError): import {{ cookiecutter.project_slug }}.users.signals # noqa: F401 - except ImportError: - pass diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/managers.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/managers.py index 017ab14e7..c75c0e970 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/managers.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/managers.py @@ -1,8 +1,13 @@ +from typing import TYPE_CHECKING + from django.contrib.auth.hashers import make_password from django.contrib.auth.models import UserManager as DjangoUserManager +if TYPE_CHECKING: + from {{ cookiecutter.project_slug }}.users.models import User # noqa: F401 -class UserManager(DjangoUserManager): + +class UserManager(DjangoUserManager["User"]): """Custom manager for the User model.""" def _create_user(self, email: str, password: str | None, **extra_fields): @@ -10,25 +15,28 @@ class UserManager(DjangoUserManager): Create and save a user with the given email and password. """ if not email: - raise ValueError("The given email must be set") + msg = "The given email must be set" + raise ValueError(msg) email = self.normalize_email(email) user = self.model(email=email, **extra_fields) user.password = make_password(password) user.save(using=self._db) return user - def create_user(self, email: str, password: str | None = None, **extra_fields): + def create_user(self, email: str, password: str | None = None, **extra_fields): # type: ignore[override] extra_fields.setdefault("is_staff", False) extra_fields.setdefault("is_superuser", False) return self._create_user(email, password, **extra_fields) - def create_superuser(self, email: str, password: str | None = None, **extra_fields): + def create_superuser(self, email: str, password: str | None = None, **extra_fields): # type: ignore[override] extra_fields.setdefault("is_staff", True) extra_fields.setdefault("is_superuser", True) if extra_fields.get("is_staff") is not True: - raise ValueError("Superuser must have is_staff=True.") + msg = "Superuser must have is_staff=True." + raise ValueError(msg) if extra_fields.get("is_superuser") is not True: - raise ValueError("Superuser must have is_superuser=True.") + msg = "Superuser must have is_superuser=True." + raise ValueError(msg) return self._create_user(email, password, **extra_fields) diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/migrations/0001_initial.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/migrations/0001_initial.py index 58a439c5d..cee6676bc 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/migrations/0001_initial.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/migrations/0001_initial.py @@ -1,7 +1,8 @@ import django.contrib.auth.models import django.contrib.auth.validators -from django.db import migrations, models import django.utils.timezone +from django.db import migrations +from django.db import models import {{cookiecutter.project_slug}}.users.models @@ -31,7 +32,7 @@ class Migration(migrations.Migration): ( "last_login", models.DateTimeField( - blank=True, null=True, verbose_name="last login" + blank=True, null=True, verbose_name="last login", ), ), ( @@ -61,14 +62,14 @@ class Migration(migrations.Migration): ( "email", models.EmailField( - blank=True, max_length=254, verbose_name="email address" + blank=True, max_length=254, verbose_name="email address", ), ), {%- else %} ( "email", models.EmailField( - unique=True, max_length=254, verbose_name="email address" + unique=True, max_length=254, verbose_name="email address", ), ), {%- endif %} @@ -91,13 +92,13 @@ class Migration(migrations.Migration): ( "date_joined", models.DateTimeField( - default=django.utils.timezone.now, verbose_name="date joined" + default=django.utils.timezone.now, verbose_name="date joined", ), ), ( "name", models.CharField( - blank=True, max_length=255, verbose_name="Name of User" + blank=True, max_length=255, verbose_name="Name of User", ), ), ( diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/models.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/models.py index 1e4807510..fd78c26a8 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/models.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/models.py @@ -1,5 +1,12 @@ +{%- if cookiecutter.username_type == "email" %} +from typing import ClassVar + +{% endif -%} from django.contrib.auth.models import AbstractUser -from django.db.models import CharField{% if cookiecutter.username_type == "email" %}, EmailField{% endif %} +from django.db.models import CharField +{%- if cookiecutter.username_type == "email" %} +from django.db.models import EmailField +{%- endif %} from django.urls import reverse from django.utils.translation import gettext_lazy as _ {%- if cookiecutter.username_type == "email" %} @@ -17,16 +24,16 @@ class User(AbstractUser): # First and last name do not cover name patterns around the globe name = CharField(_("Name of User"), blank=True, max_length=255) - first_name = None # type: ignore - last_name = None # type: ignore + first_name = None # type: ignore[assignment] + last_name = None # type: ignore[assignment] {%- if cookiecutter.username_type == "email" %} email = EmailField(_("email address"), unique=True) - username = None # type: ignore + username = None # type: ignore[assignment] USERNAME_FIELD = "email" REQUIRED_FIELDS = [] - objects = UserManager() + objects: ClassVar[UserManager] = UserManager() {%- endif %} def get_absolute_url(self) -> str: diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/factories.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/factories.py index ef03ce07e..136d0b1d5 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/factories.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/factories.py @@ -1,7 +1,8 @@ from collections.abc import Sequence from typing import Any -from factory import Faker, post_generation +from factory import Faker +from factory import post_generation from factory.django import DjangoModelFactory from {{ cookiecutter.project_slug }}.users.models import User @@ -15,7 +16,7 @@ class UserFactory(DjangoModelFactory): name = Faker("name") @post_generation - def password(self, create: bool, extracted: Sequence[Any], **kwargs): + def password(self, create: bool, extracted: Sequence[Any], **kwargs): # noqa: FBT001 password = ( extracted if extracted diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_admin.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_admin.py index 2991d18a9..f802b8ba1 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_admin.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_admin.py @@ -1,4 +1,12 @@ +import contextlib +from http import HTTPStatus +from importlib import reload + +import pytest +from django.contrib import admin +from django.contrib.auth.models import AnonymousUser from django.urls import reverse +from pytest_django.asserts import assertRedirects from {{ cookiecutter.project_slug }}.users.models import User @@ -7,17 +15,17 @@ class TestUserAdmin: def test_changelist(self, admin_client): url = reverse("admin:users_user_changelist") response = admin_client.get(url) - assert response.status_code == 200 + assert response.status_code == HTTPStatus.OK def test_search(self, admin_client): url = reverse("admin:users_user_changelist") response = admin_client.get(url, data={"q": "test"}) - assert response.status_code == 200 + assert response.status_code == HTTPStatus.OK def test_add(self, admin_client): url = reverse("admin:users_user_add") response = admin_client.get(url) - assert response.status_code == 200 + assert response.status_code == HTTPStatus.OK response = admin_client.post( url, @@ -31,7 +39,7 @@ class TestUserAdmin: "password2": "My_R@ndom-P@ssw0rd", }, ) - assert response.status_code == 302 + assert response.status_code == HTTPStatus.FOUND {%- if cookiecutter.username_type == "email" %} assert User.objects.filter(email="new-admin@example.com").exists() {%- else %} @@ -46,4 +54,24 @@ class TestUserAdmin: {%- endif %} url = reverse("admin:users_user_change", kwargs={"object_id": user.pk}) response = admin_client.get(url) - assert response.status_code == 200 + assert response.status_code == HTTPStatus.OK + + @pytest.fixture() + def _force_allauth(self, settings): + settings.DJANGO_ADMIN_FORCE_ALLAUTH = True + # Reload the admin module to apply the setting change + import {{ cookiecutter.project_slug }}.users.admin as users_admin + + with contextlib.suppress(admin.sites.AlreadyRegistered): + reload(users_admin) + + @pytest.mark.django_db() + @pytest.mark.usefixtures("_force_allauth") + def test_allauth_login(self, rf, settings): + request = rf.get("/fake-url") + request.user = AnonymousUser() + response = admin.site.login(request) + + # The `admin` login view should redirect to the `allauth` login view + target_url = reverse(settings.LOGIN_URL) + "?next=" + request.path + assertRedirects(response, target_url, fetch_redirect_response=False) diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_drf_urls.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_drf_urls.py index 334ab1185..b445b611d 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_drf_urls.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_drf_urls.py @@ -1,14 +1,20 @@ -from django.urls import resolve, reverse +from django.urls import resolve +from django.urls import reverse from {{ cookiecutter.project_slug }}.users.models import User def test_user_detail(user: User): {%- if cookiecutter.username_type == "email" %} - assert reverse("api:user-detail", kwargs={"pk": user.pk}) == f"/api/users/{user.pk}/" + assert ( + reverse("api:user-detail", kwargs={"pk": user.pk}) == f"/api/users/{user.pk}/" + ) assert resolve(f"/api/users/{user.pk}/").view_name == "api:user-detail" {%- else %} - assert reverse("api:user-detail", kwargs={"username": user.username}) == f"/api/users/{user.username}/" + assert ( + reverse("api:user-detail", kwargs={"username": user.username}) + == f"/api/users/{user.username}/" + ) assert resolve(f"/api/users/{user.username}/").view_name == "api:user-detail" {%- endif %} diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_drf_views.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_drf_views.py index 90e84dc7d..955ebe4eb 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_drf_views.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_drf_views.py @@ -6,7 +6,7 @@ from {{ cookiecutter.project_slug }}.users.models import User class TestUserViewSet: - @pytest.fixture + @pytest.fixture() def api_rf(self) -> APIRequestFactory: return APIRequestFactory() @@ -26,7 +26,7 @@ class TestUserViewSet: view.request = request - response = view.me(request) # type: ignore + response = view.me(request) # type: ignore[call-arg, arg-type, misc] assert response.data == { {%- if cookiecutter.username_type == "email" %} diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_forms.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_forms.py index 023aad056..17d0d72a1 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_forms.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_forms.py @@ -1,6 +1,5 @@ -""" -Module for all Form Tests. -""" +"""Module for all Form Tests.""" + from django.utils.translation import gettext_lazy as _ from {{ cookiecutter.project_slug }}.users.forms import UserAdminCreationForm @@ -31,7 +30,7 @@ class TestUserAdminCreationForm: {%- endif %} "password1": user.password, "password2": user.password, - } + }, ) assert not form.is_valid() diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_managers.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_managers.py index f25af4ee2..e5e5f5a4b 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_managers.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_managers.py @@ -6,12 +6,12 @@ from django.core.management import call_command from {{ cookiecutter.project_slug }}.users.models import User -@pytest.mark.django_db +@pytest.mark.django_db() class TestUserManager: def test_create_user(self): user = User.objects.create_user( email="john@example.com", - password="something-r@nd0m!", + password="something-r@nd0m!", # noqa: S106 ) assert user.email == "john@example.com" assert not user.is_staff @@ -22,7 +22,7 @@ class TestUserManager: def test_create_superuser(self): user = User.objects.create_superuser( email="admin@example.com", - password="something-r@nd0m!", + password="something-r@nd0m!", # noqa: S106 ) assert user.email == "admin@example.com" assert user.is_staff @@ -32,12 +32,12 @@ class TestUserManager: def test_create_superuser_username_ignored(self): user = User.objects.create_superuser( email="test@example.com", - password="something-r@nd0m!", + password="something-r@nd0m!", # noqa: S106 ) assert user.username is None -@pytest.mark.django_db +@pytest.mark.django_db() def test_createsuperuser_command(): """Ensure createsuperuser command works with our custom manager.""" out = StringIO() diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_swagger.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_swagger.py index f97658b55..3081d1f65 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_swagger.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_swagger.py @@ -1,3 +1,5 @@ +from http import HTTPStatus + import pytest from django.urls import reverse @@ -5,17 +7,17 @@ from django.urls import reverse def test_swagger_accessible_by_admin(admin_client): url = reverse("api-docs") response = admin_client.get(url) - assert response.status_code == 200 + assert response.status_code == HTTPStatus.OK -@pytest.mark.django_db +@pytest.mark.django_db() def test_swagger_ui_not_accessible_by_normal_user(client): url = reverse("api-docs") response = client.get(url) - assert response.status_code == 403 + assert response.status_code == HTTPStatus.FORBIDDEN def test_api_schema_generated_successfully(admin_client): url = reverse("api-schema") response = admin_client.get(url) - assert response.status_code == 200 + assert response.status_code == HTTPStatus.OK diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_tasks.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_tasks.py index 41d5af292..d3f610139 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_tasks.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_tasks.py @@ -9,8 +9,9 @@ pytestmark = pytest.mark.django_db def test_user_count(settings): """A basic test to execute the get_users_count Celery task.""" - UserFactory.create_batch(3) + batch_size = 3 + UserFactory.create_batch(batch_size) settings.CELERY_TASK_ALWAYS_EAGER = True task_result = get_users_count.delay() assert isinstance(task_result, EagerResult) - assert task_result.result == 3 + assert task_result.result == batch_size diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_urls.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_urls.py index a0d068890..aaacb05a1 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_urls.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_urls.py @@ -1,4 +1,5 @@ -from django.urls import resolve, reverse +from django.urls import resolve +from django.urls import reverse from {{ cookiecutter.project_slug }}.users.models import User @@ -8,7 +9,10 @@ def test_detail(user: User): assert reverse("users:detail", kwargs={"pk": user.pk}) == f"/users/{user.pk}/" assert resolve(f"/users/{user.pk}/").view_name == "users:detail" {%- else %} - assert reverse("users:detail", kwargs={"username": user.username}) == f"/users/{user.username}/" + assert ( + reverse("users:detail", kwargs={"username": user.username}) + == f"/users/{user.username}/" + ) assert resolve(f"/users/{user.username}/").view_name == "users:detail" {%- endif %} diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_views.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_views.py index 2c1027038..136daa402 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_views.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_views.py @@ -1,10 +1,13 @@ +from http import HTTPStatus + import pytest from django.conf import settings from django.contrib import messages from django.contrib.auth.models import AnonymousUser from django.contrib.messages.middleware import MessageMiddleware from django.contrib.sessions.middleware import SessionMiddleware -from django.http import HttpRequest, HttpResponseRedirect +from django.http import HttpRequest +from django.http import HttpResponseRedirect from django.test import RequestFactory from django.urls import reverse from django.utils.translation import gettext_lazy as _ @@ -12,11 +15,9 @@ from django.utils.translation import gettext_lazy as _ from {{ cookiecutter.project_slug }}.users.forms import UserAdminChangeForm from {{ cookiecutter.project_slug }}.users.models import User from {{ cookiecutter.project_slug }}.users.tests.factories import UserFactory -from {{ cookiecutter.project_slug }}.users.views import ( - UserRedirectView, - UserUpdateView, - user_detail_view, -) +from {{ cookiecutter.project_slug }}.users.views import UserRedirectView +from {{ cookiecutter.project_slug }}.users.views import UserUpdateView +from {{ cookiecutter.project_slug }}.users.views import user_detail_view pytestmark = pytest.mark.django_db @@ -102,7 +103,7 @@ class TestUserDetailView: response = user_detail_view(request, username=user.username) {%- endif %} - assert response.status_code == 200 + assert response.status_code == HTTPStatus.OK def test_not_authenticated(self, user: User, rf: RequestFactory): request = rf.get("/fake-url/") @@ -116,5 +117,5 @@ class TestUserDetailView: login_url = reverse(settings.LOGIN_URL) assert isinstance(response, HttpResponseRedirect) - assert response.status_code == 302 + assert response.status_code == HTTPStatus.FOUND assert response.url == f"{login_url}?next=/fake-url/" diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/urls.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/urls.py index 0ffca17aa..40719ed21 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/urls.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/urls.py @@ -1,10 +1,8 @@ from django.urls import path -from {{ cookiecutter.project_slug }}.users.views import ( - user_detail_view, - user_redirect_view, - user_update_view, -) +from {{ cookiecutter.project_slug }}.users.views import user_detail_view +from {{ cookiecutter.project_slug }}.users.views import user_redirect_view +from {{ cookiecutter.project_slug }}.users.views import user_update_view app_name = "users" urlpatterns = [ diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/views.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/views.py index 0d9847ef0..3f20f2686 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/views.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/views.py @@ -2,7 +2,9 @@ from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.messages.views import SuccessMessageMixin from django.urls import reverse from django.utils.translation import gettext_lazy as _ -from django.views.generic import DetailView, RedirectView, UpdateView +from django.views.generic import DetailView +from django.views.generic import RedirectView +from django.views.generic import UpdateView from {{ cookiecutter.project_slug }}.users.models import User @@ -27,7 +29,8 @@ class UserUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): success_message = _("Information successfully updated") def get_success_url(self): - assert self.request.user.is_authenticated # for mypy to know that the user is authenticated + # for mypy to know that the user is authenticated + assert self.request.user.is_authenticated return self.request.user.get_absolute_url() def get_object(self): diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/utils/storages.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/utils/storages.py deleted file mode 100644 index cc055378a..000000000 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/utils/storages.py +++ /dev/null @@ -1,36 +0,0 @@ -{% if cookiecutter.cloud_provider == 'AWS' -%} -from storages.backends.s3 import S3Storage - - -class StaticS3Storage(S3Storage): - location = "static" - default_acl = "public-read" - - -class MediaS3Storage(S3Storage): - location = "media" - file_overwrite = False -{%- elif cookiecutter.cloud_provider == 'GCP' -%} -from storages.backends.gcloud import GoogleCloudStorage - - -class StaticGoogleCloudStorage(GoogleCloudStorage): - location = "static" - default_acl = "publicRead" - - -class MediaGoogleCloudStorage(GoogleCloudStorage): - location = "media" - file_overwrite = False -{%- elif cookiecutter.cloud_provider == 'Azure' -%} -from storages.backends.azure_storage import AzureStorage - - -class StaticAzureStorage(AzureStorage): - location = "static" - - -class MediaAzureStorage(AzureStorage): - location = "media" - file_overwrite = False -{%- endif %}