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.
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 }}
[](https://github.com/cookiecutter/cookiecutter-django/)
-[](https://github.com/ambv/black)
+[](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 %}
|