diff --git a/.github/contributors.json b/.github/contributors.json
index 723c72083..b47a56246 100644
--- a/.github/contributors.json
+++ b/.github/contributors.json
@@ -1115,7 +1115,7 @@
"twitter_username": "Qoyyuum"
},
{
- "name": "mfosterw",
+ "name": "Matthew Foster Walsh",
"github_login": "mfosterw",
"twitter_username": ""
},
@@ -1508,5 +1508,35 @@
"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": ""
+ },
+ {
+ "name": "Mounir",
+ "github_login": "mounirmesselmeni",
+ "twitter_username": ""
+ },
+ {
+ "name": "JAEGYUN JUNG",
+ "github_login": "TGoddessana",
+ "twitter_username": ""
+ },
+ {
+ "name": "Simeon Emanuilov",
+ "github_login": "s-emanuilov",
+ "twitter_username": "s_emanuilov"
}
]
\ No newline at end of file
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index e6590469a..3582a2125 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -3,6 +3,22 @@
version: 2
updates:
+ # Update Python deps for the template (not the generated project)
+ - package-ecosystem: "pip"
+ directory: "/"
+ schedule:
+ interval: "daily"
+ labels:
+ - "project infrastructure"
+
+ # Update Python deps for the documentation
+ - package-ecosystem: "pip"
+ directory: "docs/"
+ schedule:
+ interval: "daily"
+ labels:
+ - "project infrastructure"
+
# Update GitHub actions in workflows
- package-ecosystem: "github-actions"
directory: "/"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 9863b5c0f..adde7139b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -25,7 +25,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
- python-version: "3.11"
+ python-version: "3.12"
cache: pip
- name: Install dependencies
run: pip install -r requirements.txt
@@ -56,7 +56,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
- python-version: "3.11"
+ python-version: "3.12"
cache: pip
- name: Install dependencies
run: pip install -r requirements.txt
@@ -100,7 +100,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
- python-version: "3.11"
+ python-version: "3.12"
cache: pip
cache-dependency-path: |
requirements.txt
diff --git a/.github/workflows/django-issue-checker.yml b/.github/workflows/django-issue-checker.yml
index e1b36f292..2185da81b 100644
--- a/.github/workflows/django-issue-checker.yml
+++ b/.github/workflows/django-issue-checker.yml
@@ -19,7 +19,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
- python-version: "3.11"
+ python-version: "3.12"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
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..c01cd5141 100644
--- a/.github/workflows/pre-commit-autoupdate.yml
+++ b/.github/workflows/pre-commit-autoupdate.yml
@@ -24,7 +24,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
- python-version: "3.11"
+ python-version: "3.12"
- name: Install pre-commit
run: pip install pre-commit
@@ -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/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml
index b0150507d..305a608df 100644
--- a/.github/workflows/update-changelog.yml
+++ b/.github/workflows/update-changelog.yml
@@ -19,7 +19,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
- python-version: "3.11"
+ python-version: "3.12"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
diff --git a/.github/workflows/update-contributors.yml b/.github/workflows/update-contributors.yml
index 0cda836dc..e814039db 100644
--- a/.github/workflows/update-contributors.yml
+++ b/.github/workflows/update-contributors.yml
@@ -22,7 +22,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
- python-version: "3.11"
+ python-version: "3.12"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 8c10de974..20d2f9c9f 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -2,7 +2,7 @@ exclude: "{{cookiecutter.project_slug}}|.github/contributors.json|CHANGELOG.md|C
default_stages: [commit]
default_language_version:
- python: python3.11
+ python: python3.12
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
@@ -26,14 +26,14 @@ repos:
args: ["--tab-width", "2"]
- repo: https://github.com/asottile/pyupgrade
- rev: v3.15.0
+ rev: v3.15.1
hooks:
- id: pyupgrade
- args: [--py311-plus]
+ args: [--py312-plus]
exclude: hooks/
- repo: https://github.com/psf/black
- rev: 23.12.1
+ rev: 24.3.0
hooks:
- id: black
diff --git a/.pyup.yml b/.pyup.yml
index e5d4752e4..13d336d57 100644
--- a/.pyup.yml
+++ b/.pyup.yml
@@ -14,8 +14,6 @@ pin: True
label_prs: update
requirements:
- - "requirements.txt"
- - "docs/requirements.txt"
- "{{cookiecutter.project_slug}}/requirements/base.txt"
- "{{cookiecutter.project_slug}}/requirements/local.txt"
- "{{cookiecutter.project_slug}}/requirements/production.txt"
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index 4598ff77c..872e43a2e 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -8,7 +8,7 @@ version: 2
build:
os: ubuntu-22.04
tools:
- python: "3.11"
+ python: "3.12"
# Build documentation in the docs/ directory with Sphinx
sphinx:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 707bef268..e70117ffe 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,371 @@ All enhancements and patches to Cookiecutter Django will be documented in this f
+## 2024.03.21
+
+
+### Changed
+
+- Add PostgreSQL 16, remove Postgres 10 and 11 ([#4935](https://github.com/cookiecutter/cookiecutter-django/pull/4935))
+
+### Updated
+
+- Update uvicorn to 0.29.0 ([#4933](https://github.com/cookiecutter/cookiecutter-django/pull/4933))
+
+- Update sentry-sdk to 1.43.0 ([#4934](https://github.com/cookiecutter/cookiecutter-django/pull/4934))
+
+## 2024.03.19
+
+
+### Changed
+
+- Add documentation to upgrade Postgres in Docker environment. Fix: #461 ([#4898](https://github.com/cookiecutter/cookiecutter-django/pull/4898))
+
+- Upgrade Python to version 3.12 ([#4930](https://github.com/cookiecutter/cookiecutter-django/pull/4930))
+
+## 2024.03.18
+
+
+### Changed
+
+- Split the docs from local.yml and build the service in CI ([#4909](https://github.com/cookiecutter/cookiecutter-django/pull/4909))
+
+### Updated
+
+- Update django-anymail to 10.3 ([#4919](https://github.com/cookiecutter/cookiecutter-django/pull/4919))
+
+- Update sentry-sdk to 1.42.0 ([#4921](https://github.com/cookiecutter/cookiecutter-django/pull/4921))
+
+- Update coverage to 7.4.4 ([#4926](https://github.com/cookiecutter/cookiecutter-django/pull/4926))
+
+- Update ruff to 0.3.3 ([#4927](https://github.com/cookiecutter/cookiecutter-django/pull/4927))
+
+- Auto-update pre-commit hooks ([#4928](https://github.com/cookiecutter/cookiecutter-django/pull/4928))
+
+## 2024.03.17
+
+
+### Updated
+
+- Update djangorestframework to 3.15.0 ([#4929](https://github.com/cookiecutter/cookiecutter-django/pull/4929))
+
+## 2024.03.10
+
+
+### Updated
+
+- Auto-update pre-commit hooks ([#4912](https://github.com/cookiecutter/cookiecutter-django/pull/4912))
+
+- Update ruff to 0.3.2 ([#4911](https://github.com/cookiecutter/cookiecutter-django/pull/4911))
+
+- Update uvicorn to 0.28.0 ([#4913](https://github.com/cookiecutter/cookiecutter-django/pull/4913))
+
+- Update redis to 5.0.3 ([#4916](https://github.com/cookiecutter/cookiecutter-django/pull/4916))
+
+- Update pytest to 8.1.1 ([#4914](https://github.com/cookiecutter/cookiecutter-django/pull/4914))
+
+## 2024.03.07
+
+
+### Updated
+
+- Auto-update pre-commit hooks ([#4907](https://github.com/cookiecutter/cookiecutter-django/pull/4907))
+
+- Update sentry-sdk to 1.41.0 ([#4908](https://github.com/cookiecutter/cookiecutter-django/pull/4908))
+
+## 2024.03.06
+
+
+### Fixed
+
+- Fix fully qualified docker images ([#4905](https://github.com/cookiecutter/cookiecutter-django/pull/4905))
+
+## 2024.03.04
+
+
+### Updated
+
+- Update pytest to 8.1.0 ([#4900](https://github.com/cookiecutter/cookiecutter-django/pull/4900))
+
+- Update django to 4.2.11 ([#4901](https://github.com/cookiecutter/cookiecutter-django/pull/4901))
+
+## 2024.03.03
+
+
+### Updated
+
+- Update django-celery-beat to 2.6.0 ([#4899](https://github.com/cookiecutter/cookiecutter-django/pull/4899))
+
+## 2024.03.01
+
+
+### Changed
+
+- Add a maintainer guide to the docs ([#4884](https://github.com/cookiecutter/cookiecutter-django/pull/4884))
+
+### Updated
+
+- Auto-update pre-commit hooks ([#4897](https://github.com/cookiecutter/cookiecutter-django/pull/4897))
+
+- Update ruff to 0.3.0 ([#4896](https://github.com/cookiecutter/cookiecutter-django/pull/4896))
+
+## 2024.02.28
+
+
+### Fixed
+
+- Fix invalid HTML in django-allauth field element template ([#4894](https://github.com/cookiecutter/cookiecutter-django/pull/4894))
+
+- Fix permissions for media files when served by nginx ([#4889](https://github.com/cookiecutter/cookiecutter-django/pull/4889))
+
+### Documentation
+
+- Fix broken "Two scoops of django" link in FAQ ([#4892](https://github.com/cookiecutter/cookiecutter-django/pull/4892))
+
+### Updated
+
+- Update redis to 5.0.2 ([#4895](https://github.com/cookiecutter/cookiecutter-django/pull/4895))
+
+- Update sentry-sdk to 1.40.6 ([#4893](https://github.com/cookiecutter/cookiecutter-django/pull/4893))
+
+## 2024.02.26
+
+
+### Changed
+
+- Allauth elements & MFA ([#4843](https://github.com/cookiecutter/cookiecutter-django/pull/4843))
+
+### Updated
+
+- Update pytest to 8.0.2 ([#4890](https://github.com/cookiecutter/cookiecutter-django/pull/4890))
+
+- Update crispy-bootstrap5 to 2024.2 ([#4891](https://github.com/cookiecutter/cookiecutter-django/pull/4891))
+
+## 2024.02.24
+
+
+### Updated
+
+- Update coverage to 7.4.3 ([#4888](https://github.com/cookiecutter/cookiecutter-django/pull/4888))
+
+## 2024.02.23
+
+
+### Changed
+
+- Switch to local imports within app ([#4883](https://github.com/cookiecutter/cookiecutter-django/pull/4883))
+
+- Install ruff extension in `devcontainer.json` ([#4887](https://github.com/cookiecutter/cookiecutter-django/pull/4887))
+
+### Updated
+
+- Bump webpack-dev-server to 5.0.2 ([#4875](https://github.com/cookiecutter/cookiecutter-django/pull/4875))
+
+## 2024.02.21
+
+
+### Changed
+
+- Switch to `celery.shared_task` to define tasks ([#4881](https://github.com/cookiecutter/cookiecutter-django/pull/4881))
+
+- Replace usages of `get_user_model` by importing model directly ([#4879](https://github.com/cookiecutter/cookiecutter-django/pull/4879))
+
+### Updated
+
+- Auto-update pre-commit hooks ([#4873](https://github.com/cookiecutter/cookiecutter-django/pull/4873))
+
+- Update pre-commit to 3.6.2 ([#4874](https://github.com/cookiecutter/cookiecutter-django/pull/4874))
+
+- Update ruff to 0.2.2 ([#4871](https://github.com/cookiecutter/cookiecutter-django/pull/4871))
+
+## 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
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 94ecbdd7d..753e827aa 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -18,20 +18,20 @@ This last step is very important, don't start developing from master, it'll caus
## Testing
-You'll need to run the tests using Python 3.11. We recommend using [tox](https://tox.readthedocs.io/en/latest/) to run the tests. It will automatically create a fresh virtual environment and install our test dependencies, such as [pytest-cookies](https://pypi.python.org/pypi/pytest-cookies/) and [flake8](https://pypi.python.org/pypi/flake8/).
+You'll need to run the tests using Python 3.12. We recommend using [tox](https://tox.readthedocs.io/en/latest/) to run the tests. It will automatically create a fresh virtual environment and install our test dependencies, such as [pytest-cookies](https://pypi.python.org/pypi/pytest-cookies/) and [flake8](https://pypi.python.org/pypi/flake8/).
We'll also run the tests on GitHub actions when you send your pull request, but it's a good idea to run them locally before you send it.
### Installation
-First, make sure that your version of Python is 3.11:
+First, make sure that your version of Python is 3.12:
```bash
$ python --version
-Python 3.11.3
+Python 3.12.2
```
-Any version that starts with 3.11 will do. If you need to install it, you can get it from [python.org](https://www.python.org/downloads/).
+Any version that starts with 3.12 will do. If you need to install it, you can get it from [python.org](https://www.python.org/downloads/).
Then install `tox`, if not already installed:
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 9a384d141..c14c0ea3b 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -964,6 +964,13 @@ Listed in alphabetical order.
Simon Rey
diff --git a/README.md b/README.md
index c8cfe115d..a4e2e10f8 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,7 @@ production-ready Django projects quickly.
## Features
- For Django 4.2
-- Works with Python 3.11
+- Works with Python 3.12
- Renders Django projects with 100% starting test coverage
- Twitter [Bootstrap](https://github.com/twbs/bootstrap) v5
- [12-Factor](https://12factor.net) based settings via [django-environ](https://github.com/joke2k/django-environ)
@@ -51,7 +51,7 @@ _These features can be enabled during initial project setup._
## Constraints
- Only maintained 3rd party libraries are used.
-- Uses PostgreSQL everywhere: 10 - 15 ([MySQL fork](https://github.com/mabdullahadeel/cookiecutter-django-mysql) also available).
+- Uses PostgreSQL everywhere: 12 - 16 ([MySQL fork](https://github.com/mabdullahadeel/cookiecutter-django-mysql) also available).
- Environment variables for configuration (This won't work with Apache/mod_wsgi).
## Support this Project!
@@ -129,12 +129,11 @@ Answer the prompts with your own desired [options](http://cookiecutter-django.re
Choose from 1, 2, 3 [1]: 1
use_docker [n]: n
Select postgresql_version:
- 1 - 15
- 2 - 14
- 3 - 13
- 4 - 12
- 5 - 11
- 6 - 10
+ 1 - 16
+ 2 - 15
+ 3 - 14
+ 4 - 13
+ 5 - 12
Choose from 1, 2, 3, 4, 5 [1]: 1
Select cloud_provider:
1 - AWS
diff --git a/cookiecutter.json b/cookiecutter.json
index ecc22d452..0ff271ff5 100644
--- a/cookiecutter.json
+++ b/cookiecutter.json
@@ -17,7 +17,7 @@
"windows": "n",
"editor": ["None", "PyCharm", "VS Code"],
"use_docker": "n",
- "postgresql_version": ["15", "14", "13", "12", "11", "10"],
+ "postgresql_version": ["16", "15", "14", "13", "12"],
"cloud_provider": ["AWS", "GCP", "Azure", "None"],
"mail_service": [
"Mailgun",
diff --git a/docs/deployment-on-pythonanywhere.rst b/docs/deployment-on-pythonanywhere.rst
index 2fa6a960c..726f325e2 100644
--- a/docs/deployment-on-pythonanywhere.rst
+++ b/docs/deployment-on-pythonanywhere.rst
@@ -37,7 +37,7 @@ Make sure your project is fully committed and pushed up to Bitbucket or Github o
mkvirtualenv --python=/usr/bin/python3.10 my-project-name
pip install -r requirements/production.txt # may take a few minutes
-.. note:: We're creating the virtualenv using Python 3.10 (``--python=/usr/bin/python3.10```), although Cookiecutter Django generates a project for Python 3.11. This is because, at time of writing, PythonAnywhere only supports Python 3.10. It shouldn't be a problem, but if is, you may try changing the Python version to 3.11 and see if it works. If it does, please let us know, or even better, submit a pull request to update this section.
+.. note:: We're creating the virtualenv using Python 3.10 (``--python=/usr/bin/python3.10```), although Cookiecutter Django generates a project for Python 3.12. This is because, at time of writing, PythonAnywhere only supports Python 3.10. It shouldn't be a problem, but if is, you may try changing the Python version to 3.12 and see if it works. If it does, please let us know, or even better, submit a pull request to update this section.
Setting environment variables in the console
--------------------------------------------
diff --git a/docs/developing-locally-docker.rst b/docs/developing-locally-docker.rst
index a8f945adf..01970e469 100644
--- a/docs/developing-locally-docker.rst
+++ b/docs/developing-locally-docker.rst
@@ -65,6 +65,13 @@ To run in a detached (background) mode, just::
$ docker compose up -d
+These commands don't run the docs service. In order to run docs service you can run::
+
+ $ docker compose -f docs.yml up
+
+To run the docs with local services just use::
+
+ $ docker compose -f local.yml -f docs.yml up
The site should start and be accessible at http://localhost:3000 if you selected Webpack or Gulp as frontend pipeline and http://localhost:8000 otherwise.
diff --git a/docs/developing-locally.rst b/docs/developing-locally.rst
index 92379f4fd..f7191d27d 100644
--- a/docs/developing-locally.rst
+++ b/docs/developing-locally.rst
@@ -9,7 +9,7 @@ Setting Up Development Environment
Make sure to have the following on your host:
-* Python 3.11
+* Python 3.12
* PostgreSQL_.
* Redis_, if using Celery
* Cookiecutter_
@@ -18,7 +18,7 @@ First things first.
#. Create a virtualenv: ::
- $ python3.11 -m venv
+ $ python3.12 -m venv
#. Activate the virtualenv you have just created: ::
diff --git a/docs/docker-postgres-backups.rst b/docs/docker-postgres-backups.rst
index fdf446030..302e4c4b8 100644
--- a/docs/docker-postgres-backups.rst
+++ b/docs/docker-postgres-backups.rst
@@ -104,3 +104,24 @@ Remove Backup
To remove backup you can use the ``rmbackup`` command. This will remove the backup from the ``/backups`` directory. ::
$ docker compose -f local.yml exec postgres rmbackup backup_2018_03_13T09_05_07.sql.gz
+
+
+Upgrading PostgreSQL
+----------------------------------
+
+Upgrading PostgreSQL in your project requires a series of carefully executed steps. Start by halting all containers, excluding the postgres container. Following this, create a backup and proceed to remove the outdated data volume. ::
+
+ $ docker compose -f local.yml down
+ $ docker compose -f local.yml up -d postgres
+ $ docker compose -f local.yml run --rm postgres backup
+ $ docker compose -f local.yml down
+ $ docker volume rm my_project_postgres_data
+
+.. note:: Neglecting to remove the old data volume may lead to issues, such as the new postgres container failing to start with errors like ``FATAL: database files are incompatible with server``, and ``could not translate host name "postgres" to address: Name or service not known``.
+
+To complete the upgrade, update the PostgreSQL version in the corresponding Dockerfile (e.g. ``compose/production/postgres/Dockerfile``) and build a new version of PostgreSQL. ::
+
+ $ docker compose -f local.yml build postgres
+ $ docker compose -f local.yml up -d postgres
+ $ docker compose -f local.yml run --rm postgres restore backup_2018_03_13T09_05_07.sql.gz
+ $ docker compose -f local.yml up -d
diff --git a/docs/faq.rst b/docs/faq.rst
index 52a99467c..9f0b52a7d 100644
--- a/docs/faq.rst
+++ b/docs/faq.rst
@@ -22,6 +22,6 @@ TODO
Why doesn't this follow the layout from Two Scoops of Django?
-------------------------------------------------------------
-You may notice that some elements of this project do not exactly match what we describe in chapter 3 of `Two Scoops of Django 1.11`_. The reason for that is this project, amongst other things, serves as a test bed for trying out new ideas and concepts. Sometimes they work, sometimes they don't, but the end result is that it won't necessarily match precisely what is described in the book I co-authored.
+You may notice that some elements of this project do not exactly match what we describe in chapter 3 of `Two Scoops of Django 3.x`_. The reason for that is this project, amongst other things, serves as a test bed for trying out new ideas and concepts. Sometimes they work, sometimes they don't, but the end result is that it won't necessarily match precisely what is described in the book I co-authored.
-.. _Two Scoops of Django 1.11: https://www.feldroy.com/collections/django/products/two-scoops-of-django-1-11
+.. _Two Scoops of Django 3.x: https://www.feldroy.com/books/two-scoops-of-django-3-x
diff --git a/docs/index.rst b/docs/index.rst
index da5186487..70daa1852 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -28,6 +28,7 @@ Contents
faq
troubleshooting
contributing
+ maintainer-guide
Indices and tables
------------------
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/maintainer-guide.md b/docs/maintainer-guide.md
new file mode 100644
index 000000000..9baac688a
--- /dev/null
+++ b/docs/maintainer-guide.md
@@ -0,0 +1,104 @@
+# Maintainer guide
+
+This document is intended for maintainers of the template.
+
+## Automated updates
+
+We use 2 separate services to keep our dependencies up-to-date:
+
+- Dependabot, which manages updates of Python deps of the template, GitHub actions, npm packages and Docker images.
+- PyUp, which manages the Python deps for the generated project.
+
+We don't use Dependabot for the generated project deps because our requirements files are templated, and Dependabot fails to parse them. PyUp is -AFAIK- the only service out there that supports having Jinja tags in the requirements file.
+
+Updates for the template should be labelled as `project infrastructure` while the ones about the generated project should be labelled as `update`. This is use to work in conjunction with our changelog script (see later).
+
+## Automation scripts
+
+We have a few workflows which have been automated over time. They usually run using GitHub actions and might need a few small manual actions to work nicely. Some have a few limitations which we should document here.
+
+### CI
+
+`ci.yml`
+
+The CI workflow tries to cover 2 main aspects of the template:
+
+- Check all combinations to make sure that valid files are generated with no major linting issues. Issues which are fixed by an auto-formatter after generation aren't considered major, and only aim for best effort. This is under the `test` job.
+- Run more in-depth tests on a few combinations, by installing dependencies, running type checker and the test suite of the generated project. We try to cover docker (`docker` job) and non-docker (`bare` job) setups.
+
+We also run the deployment checks, but we don't do much more beyond that for testing the production setup.
+
+### Django issue checker
+
+`django-issue-checker.yml`
+
+This workflow runs daily, on schedule, and checks if there is a new major version of Django (not in the pure SemVer sense) released that we are not running, and list our dependencies compatibility.
+
+For example, at time of writing, we use Django 4.2, but the latest version of Django is 5.0, so the workflow created a ["Django 5.0" issue](https://github.com/cookiecutter/cookiecutter-django/issues/4724) in GitHub, with a compatibility table and keeps it up to date every day.
+
+#### Limitations
+
+Here are a few current and past limitations of the script
+
+- When a new dependency is added to the template, the script fails to update an existing issue
+- Not sure what happens when a deps is removed
+- ~~Unable to parse classifiers without minor version~~
+- ~~Creates an issue even if we are on the latest version~~
+
+### Issue manager
+
+`issue-manager.yml`
+
+A workflow that uses [Sebastian Ramirez' issue-manager](https://github.com/tiangolo/issue-manager) to help us automate issue management. The tag line from the repo explains it well:
+
+> Automatically close issues or Pull Requests that have a label, after a custom delay, if no one replies back.
+
+It runs on a schedule as well as when some actions are taken on issues and pull requests.
+
+We wait 10 days before closing issues, and we have a few customised reasons, which are configured in the workflow itself. The config should be fairly self-explanatory.
+
+### Pre-commit auto-update
+
+`pre-commit-autoupdate.yml`
+
+Run daily, to do `pre-commit autoupdate` on the template as well as the generated project, and opens a pull request with the changes.
+
+#### Limitations
+
+- The PR is open as GitHub action which means that CI does NOT run. The documentation for create-pull-request action [explains why](https://github.com/peter-evans/create-pull-request/blob/main/docs/concepts-guidelines.md#triggering-further-workflow-runs).
+- Some hooks are also installed as local dependencies (via `requirements/local.txt`), but these are updated separately via PyUP.
+
+### Update changelog
+
+`update-changelog.yml`
+
+Run daily at 2AM to update our changelog and create a GitHub release. This runs a custom script which:
+
+- List all pull requests merged the day before
+- The release name is calendar based, so `YYYY.MM.DD`
+- For each PR:
+ - Get the PR title to summarize the change
+ - Look at the PR labels to classify it in a section of the release notes:
+ - anything labelled `project infrastructure` is excluded
+ - label `update` goes in section "Updated"
+ - label `bug` goes in section "Fixed"
+ - label `docs` goes in section "Documentation"
+ - Default to section "Changed"
+
+With that in mind, when merging changes, it's a good idea to set the labels and rename the PR title to give a good summary of the change, in the context of the changelog.
+
+#### Limitations
+
+- Dependabot updates for npm & Docker have a verbose title, try to rename them to be more readable: `Bump webpack-dev-server from 4.15.1 to 5.0.2 in /{{cookiecutter.project_slug}}` -> `Bump webpack-dev-server to 5.0.2`
+- ~~Dependencies updates for the template repo (tox, cookiecutter, etc...) don't need to appear in changelog, and need to be labelled as `project infrastructure` manually. By default, they come from PyUp labelled as `update`.~~
+
+### Update contributors
+
+`update-contributors.yml`
+
+Runs on each push to master branch. List the 5 most recently merged pull requests and extract their author. If any of the authors is a new one, updates the `.github/contributors.json`, regenerate the `CONTRIBUTORS.md` from it, and push back the changes to master.
+
+#### Limitations
+
+- If you merge a pull request from a new contributor, and merge another one right after, the push to master will fail as the remote will be out of date.
+- If you merge more than 5 pull requests in a row like this, the new contributor might fail to be added.
diff --git a/docs/project-generation-options.rst b/docs/project-generation-options.rst
index 4b21e16eb..d43ceec0c 100644
--- a/docs/project-generation-options.rst
+++ b/docs/project-generation-options.rst
@@ -59,12 +59,11 @@ use_docker:
postgresql_version:
Select a PostgreSQL_ version to use. The choices are:
- 1. 15
- 2. 14
- 3. 13
- 4. 12
- 5. 11
- 6. 10
+ 1. 16
+ 2. 15
+ 3. 14
+ 4. 13
+ 5. 12
cloud_provider:
Select a cloud provider for static & media files. The choices are:
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 bcd0c6533..9b66203b6 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..e58fd3541 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
@@ -38,7 +39,7 @@ if "{{ cookiecutter.use_docker }}".lower() == "n":
if python_major_version == 2:
print(
WARNING + "You're running cookiecutter under Python 2, but the generated "
- "project requires Python 3.11+. Do you want to proceed (y/n)? " + TERMINATOR
+ "project requires Python 3.12+. Do you want to proceed (y/n)? " + TERMINATOR
)
yes_options, no_options = frozenset(["y"]), frozenset(["n"])
while True:
diff --git a/pyproject.toml b/pyproject.toml
index 2a9f00b29..6e68762f7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -15,7 +15,7 @@ norecursedirs = [
# ==== black ====
[tool.black]
line-length = 119
-target-version = ['py311']
+target-version = ['py312']
# ==== isort ====
diff --git a/requirements.txt b/requirements.txt
index aadf4abb1..48cacacc9 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,20 +1,18 @@
-cookiecutter==2.5.0
+cookiecutter==2.6.0
sh==2.0.6; sys_platform != "win32"
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.3.3
+django-upgrade==1.16.0
djlint==1.34.1
-pre-commit==3.6.0
+pre-commit==3.6.2
# Testing
# ------------------------------------------------------------------------------
-tox==4.12.1
-pytest==7.4.4
+tox==4.14.1
+pytest==8.1.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.41
+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/scripts/update_contributors.py b/scripts/update_contributors.py
index 09a7082c0..7f7b48d76 100644
--- a/scripts/update_contributors.py
+++ b/scripts/update_contributors.py
@@ -40,8 +40,8 @@ def iter_recent_authors():
"""
Fetch users who opened recently merged pull requests.
- Use Github API to fetch recent authors rather than
- git CLI to work with Github usernames.
+ Use GitHub API to fetch recent authors rather than
+ git CLI to work with GitHub usernames.
"""
repo = Github(login_or_token=GITHUB_TOKEN, per_page=5).get_repo(GITHUB_REPO)
recent_pulls = repo.get_pulls(state="closed", sort="updated", direction="desc").get_page(0)
diff --git a/setup.py b/setup.py
index 8aa7a43bb..f6e6a0999 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.21"
+version = "2024.03.21"
with open("README.md") as readme_file:
long_description = readme_file.read()
@@ -30,7 +30,7 @@ setup(
"License :: OSI Approved :: BSD License",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
"Programming Language :: Python :: Implementation :: CPython",
"Topic :: Software Development",
],
diff --git a/tests/test_cookiecutter_generation.py b/tests/test_cookiecutter_generation.py
index 8357615a9..598cbbef5 100755
--- a/tests/test_cookiecutter_generation.py
+++ b/tests/test_cookiecutter_generation.py
@@ -56,12 +56,11 @@ SUPPORTED_COMBINATIONS = [
{"editor": "VS Code"},
{"use_docker": "y"},
{"use_docker": "n"},
+ {"postgresql_version": "16"},
{"postgresql_version": "15"},
{"postgresql_version": "14"},
{"postgresql_version": "13"},
{"postgresql_version": "12"},
- {"postgresql_version": "11"},
- {"postgresql_version": "10"},
{"cloud_provider": "AWS", "use_whitenoise": "y"},
{"cloud_provider": "AWS", "use_whitenoise": "n"},
{"cloud_provider": "GCP", "use_whitenoise": "y"},
@@ -179,28 +178,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),
)
@@ -286,7 +282,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..96bf8662d 100755
--- a/tests/test_docker.sh
+++ b/tests/test_docker.sh
@@ -30,10 +30,20 @@ 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
+docker compose -f docs.yml run docs make html
# Run npm build script if package.json is present
if [ -f "package.json" ]
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/tox.ini b/tox.ini
index 903d5a53b..3b7a95088 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,6 @@
[tox]
skipsdist = true
-envlist = py311,black-template
+envlist = py312,black-template
[testenv]
deps = -rrequirements.txt
diff --git a/{{cookiecutter.project_slug}}/.devcontainer/devcontainer.json b/{{cookiecutter.project_slug}}/.devcontainer/devcontainer.json
index c4158dc10..e16d06a20 100644
--- a/{{cookiecutter.project_slug}}/.devcontainer/devcontainer.json
+++ b/{{cookiecutter.project_slug}}/.devcontainer/devcontainer.json
@@ -35,24 +35,13 @@
"analysis.typeCheckingMode": "basic",
"defaultInterpreterPath": "/usr/local/bin/python",
"editor.codeActionsOnSave": {
- "source.organizeImports": true
+ "source.organizeImports": "always"
},
- // 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
@@ -65,8 +54,7 @@
// python
"ms-python.python",
"ms-python.vscode-pylance",
- "ms-python.isort",
- "ms-python.black-formatter",
+ "charliermarsh.ruff",
// django
"batisteo.vscode-django"
]
diff --git a/{{cookiecutter.project_slug}}/.drone.yml b/{{cookiecutter.project_slug}}/.drone.yml
index dc08bfbab..2753fa4e0 100644
--- a/{{cookiecutter.project_slug}}/.drone.yml
+++ b/{{cookiecutter.project_slug}}/.drone.yml
@@ -13,7 +13,7 @@ environment:
steps:
- name: lint
pull: if-not-exists
- image: python:3.11
+ image: python:3.12
environment:
PRE_COMMIT_HOME: ${CI_PROJECT_DIR}/.cache/pre-commit
volumes:
@@ -32,11 +32,12 @@ steps:
DATABASE_URL: pgsql://$POSTGRES_USER:$POSTGRES_PASSWORD@postgres/$POSTGRES_DB
commands:
- docker-compose -f local.yml build
+ - docker-compose -f docs.yml build
- docker-compose -f local.yml run --rm django python manage.py migrate
- docker-compose -f local.yml up -d
- docker-compose -f local.yml run django pytest
{%- else %}
- image: python:3.11
+ image: python:3.12
commands:
- pip install -r requirements/local.txt
- pytest
diff --git a/{{cookiecutter.project_slug}}/.github/workflows/ci.yml b/{{cookiecutter.project_slug}}/.github/workflows/ci.yml
index e39933fe1..4e24af5a9 100644
--- a/{{cookiecutter.project_slug}}/.github/workflows/ci.yml
+++ b/{{cookiecutter.project_slug}}/.github/workflows/ci.yml
@@ -28,7 +28,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v4
with:
- python-version: '3.11'
+ python-version: '3.12'
{%- if cookiecutter.open_source_license != 'Not open source' %}
# Consider using pre-commit.ci for open source project
@@ -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,8 @@ 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
+ run: docker compose -f local.yml build docs
- name: Run DB Migrations
run: docker compose -f local.yml run --rm django python manage.py migrate
@@ -84,7 +85,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v4
with:
- python-version: '3.11'
+ python-version: '3.12'
cache: pip
cache-dependency-path: |
requirements/base.txt
diff --git a/{{cookiecutter.project_slug}}/.gitlab-ci.yml b/{{cookiecutter.project_slug}}/.gitlab-ci.yml
index 350212003..df823db1f 100644
--- a/{{cookiecutter.project_slug}}/.gitlab-ci.yml
+++ b/{{cookiecutter.project_slug}}/.gitlab-ci.yml
@@ -13,7 +13,7 @@ variables:
precommit:
stage: lint
- image: python:3.11
+ image: python:3.12
variables:
PRE_COMMIT_HOME: ${CI_PROJECT_DIR}/.cache/pre-commit
cache:
@@ -34,13 +34,14 @@ pytest:
- docker:dind
before_script:
- docker compose -f local.yml build
+ - docker compose -f docs.yml build
# Ensure celerybeat does not crash due to non-existent tables
- 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
{%- else %}
- image: python:3.11
+ image: python:3.12
tags:
- python
services:
diff --git a/{{cookiecutter.project_slug}}/.pre-commit-config.yaml b/{{cookiecutter.project_slug}}/.pre-commit-config.yaml
index 902274831..5a7bdac52 100644
--- a/{{cookiecutter.project_slug}}/.pre-commit-config.yaml
+++ b/{{cookiecutter.project_slug}}/.pre-commit-config.yaml
@@ -2,7 +2,7 @@ exclude: '^docs/|/migrations/|devcontainer.json'
default_stages: [commit]
default_language_version:
- python: python3.11
+ python: python3.12
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
@@ -32,31 +32,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.3.3
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}}/.readthedocs.yml b/{{cookiecutter.project_slug}}/.readthedocs.yml
index d5a8ef661..556438876 100644
--- a/{{cookiecutter.project_slug}}/.readthedocs.yml
+++ b/{{cookiecutter.project_slug}}/.readthedocs.yml
@@ -8,7 +8,7 @@ version: 2
build:
os: ubuntu-22.04
tools:
- python: '3.11'
+ python: '3.12'
# Build documentation in the docs/ directory with Sphinx
sphinx:
diff --git a/{{cookiecutter.project_slug}}/.travis.yml b/{{cookiecutter.project_slug}}/.travis.yml
index cd703d3ad..abf12f42e 100644
--- a/{{cookiecutter.project_slug}}/.travis.yml
+++ b/{{cookiecutter.project_slug}}/.travis.yml
@@ -2,7 +2,7 @@ dist: focal
language: python
python:
- - "3.11"
+ - "3.12"
services:
- {% if cookiecutter.use_docker == 'y' %}docker{% else %}postgresql{% endif %}
@@ -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' %}
@@ -20,11 +20,12 @@ jobs:
- docker compose -v
- docker -v
- docker compose -f local.yml build
+ - docker compose -f docs.yml build
# Ensure celerybeat does not crash due to non-existent tables
- 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 %}
@@ -37,9 +38,9 @@ jobs:
- sudo apt-get install -qq libsqlite3-dev libxml2 libxml2-dev libssl-dev libbz2-dev wget curl llvm
language: python
python:
- - "3.11"
+ - "3.12"
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..d607550a3 100644
--- a/{{cookiecutter.project_slug}}/compose/local/django/Dockerfile
+++ b/{{cookiecutter.project_slug}}/compose/local/django/Dockerfile
@@ -1,5 +1,5 @@
# 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.12.2-slim-bookworm as python
# Python build stage
FROM python as python-build-stage
diff --git a/{{cookiecutter.project_slug}}/compose/local/docs/Dockerfile b/{{cookiecutter.project_slug}}/compose/local/docs/Dockerfile
index 41ab15b9f..c95fa6663 100644
--- a/{{cookiecutter.project_slug}}/compose/local/docs/Dockerfile
+++ b/{{cookiecutter.project_slug}}/compose/local/docs/Dockerfile
@@ -1,5 +1,5 @@
# 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.12.2-slim-bookworm as python
# Python build stage
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..ea4f1899d 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,7 +25,7 @@ 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.12.2-slim-bookworm as python
# Python build stage
FROM python as python-build-stage
@@ -117,7 +117,7 @@ COPY --chown=django:django . ${APP_HOME}
{%- endif %}
# make django owner of the WORKDIR directory as well.
-RUN chown django:django ${APP_HOME}
+RUN chown -R django:django ${APP_HOME}
USER django
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}}/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 1e77e3f07..ec98689dc 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
@@ -84,6 +84,7 @@ THIRD_PARTY_APPS = [
"crispy_bootstrap5",
"allauth",
"allauth.account",
+ "allauth.mfa",
"allauth.socialaccount",
{%- if cookiecutter.use_celery == 'y' %}
"django_celery_beat",
@@ -139,7 +140,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"},
@@ -212,7 +215,7 @@ TEMPLATES = [
"{{cookiecutter.project_slug}}.users.context_processors.allauth_settings",
],
},
- }
+ },
]
# https://docs.djangoproject.com/en/dev/ref/settings/#form-renderer
@@ -276,7 +279,7 @@ LOGGING = {
"level": "DEBUG",
"class": "logging.StreamHandler",
"formatter": "verbose",
- }
+ },
},
"root": {"level": "INFO", "handlers": ["console"]},
}
@@ -386,7 +389,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 3519a6639..475d985a5 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,7 +53,7 @@ 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
@@ -80,7 +88,7 @@ INSTALLED_APPS = ["whitenoise.runserver_nostatic"] + INSTALLED_APPS # noqa: F40
# 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 4d64c63f2..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,7 +28,7 @@ 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
# ------------------------------------------------------------------------------
@@ -37,7 +38,7 @@ 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.yml b/{{cookiecutter.project_slug}}/docs.yml
new file mode 100644
index 000000000..d9e50b928
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/docs.yml
@@ -0,0 +1,18 @@
+version: '3'
+
+services:
+ docs:
+ image: {{ cookiecutter.project_slug }}_local_docs
+ container_name: {{ cookiecutter.project_slug }}_local_docs
+ build:
+ context: .
+ dockerfile: ./compose/local/docs/Dockerfile
+ env_file:
+ - ./.envs/.local/.django
+ volumes:
+ - ./docs:/docs:z
+ - ./config:/app/config:z
+ - ./{{ cookiecutter.project_slug }}:/app/{{ cookiecutter.project_slug }}:z
+ ports:
+ - '9000:9000'
+ command: /start-docs
diff --git a/{{cookiecutter.project_slug}}/docs/conf.py b/{{cookiecutter.project_slug}}/docs/conf.py
index 69af11fde..797f8c787 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..cd2c7fad0 100644
--- a/{{cookiecutter.project_slug}}/local.yml
+++ b/{{cookiecutter.project_slug}}/local.yml
@@ -40,25 +40,10 @@ services:
env_file:
- ./.envs/.local/.postgres
- docs:
- image: {{ cookiecutter.project_slug }}_local_docs
- container_name: {{ cookiecutter.project_slug }}_local_docs
- build:
- context: .
- dockerfile: ./compose/local/docs/Dockerfile
- env_file:
- - ./.envs/.local/.django
- volumes:
- - ./docs:/docs:z
- - ./config:/app/config:z
- - ./{{ cookiecutter.project_slug }}:/app/{{ cookiecutter.project_slug }}:z
- ports:
- - '9000:9000'
- command: /start-docs
{%- 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 +52,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 958b15ded..9ca728208 100644
--- a/{{cookiecutter.project_slug}}/package.json
+++ b/{{cookiecutter.project_slug}}/package.json
@@ -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",
@@ -31,7 +31,7 @@
"webpack": "^5.65.0",
"webpack-bundle-tracker": "^3.0.1",
"webpack-cli": "^5.0.1",
- "webpack-dev-server": "^4.6.0",
+ "webpack-dev-server": "^5.0.2",
"webpack-merge": "^5.8.0"
},
"engines": {
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 b11b8ea22..4f4a49b95 100644
--- a/{{cookiecutter.project_slug}}/pyproject.toml
+++ b/{{cookiecutter.project_slug}}/pyproject.toml
@@ -16,28 +16,9 @@ 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"
+python_version = "3.12"
check_untyped_defs = true
ignore_missing_imports = true
warn_unused_ignores = true
@@ -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,endblock"
@@ -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 = "py312"
+
+[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 1bfa53eb2..a20b782a9 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' %}
@@ -11,36 +11,36 @@ argon2-cffi==23.1.0 # https://github.com/hynek/argon2_cffi
{%- if cookiecutter.use_whitenoise == 'y' %}
whitenoise==6.6.0 # https://github.com/evansd/whitenoise
{%- endif %}
-redis==5.0.1 # https://github.com/redis/redis-py
+redis==5.0.3 # https://github.com/redis/redis-py
{%- if cookiecutter.use_docker == "y" or cookiecutter.windows == "n" %}
hiredis==2.3.2 # https://github.com/redis/hiredis-py
{%- endif %}
{%- if cookiecutter.use_celery == "y" %}
celery==5.3.6 # pyup: < 6.0 # https://github.com/celery/celery
-django-celery-beat==2.5.0 # https://github.com/celery/django-celery-beat
+django-celery-beat==2.6.0 # https://github.com/celery/django-celery-beat
{%- if cookiecutter.use_docker == 'y' %}
flower==2.0.1 # https://github.com/mher/flower
{%- endif %}
{%- endif %}
{%- if cookiecutter.use_async == 'y' %}
-uvicorn[standard]==0.26.0 # https://github.com/encode/uvicorn
+uvicorn[standard]==0.29.0 # https://github.com/encode/uvicorn
{%- endif %}
# Django
# ------------------------------------------------------------------------------
-django==4.2.9 # pyup: < 5.0 # https://www.djangoproject.com/
+django==4.2.11 # 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.1 # https://github.com/pennersr/django-allauth
+django-model-utils==4.4.0 # https://github.com/jazzband/django-model-utils
+django-allauth[mfa]==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
+crispy-bootstrap5==2024.2 # https://github.com/django-crispy-forms/crispy-bootstrap5
{%- if cookiecutter.frontend_pipeline == 'Django Compressor' %}
django-compressor==4.4 # https://github.com/django-compressor/django-compressor
{%- endif %}
django-redis==5.4.0 # https://github.com/jazzband/django-redis
{%- if cookiecutter.use_drf == 'y' %}
# Django REST Framework
-djangorestframework==3.14.0 # https://github.com/encode/django-rest-framework
+djangorestframework==3.15.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.1 # https://github.com/tfranzel/drf-spectacular
diff --git a/{{cookiecutter.project_slug}}/requirements/local.txt b/{{cookiecutter.project_slug}}/requirements/local.txt
index 6cb9d4434..e52cf10fa 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.1.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,27 +24,21 @@ 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-rtd-theme==1.3.0 # https://pypi.org/project/sphinx-rtd-theme/
+sphinx-autobuild==2024.2.4 # https://github.com/GaretJax/sphinx-autobuild
+sphinx-rtd-theme==2.0.0 # https://pypi.org/project/sphinx-rtd-theme/
# 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.3.3 # https://github.com/astral-sh/ruff
+coverage==7.4.4 # 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.2 # 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..e412bd0cc 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.43.0 # 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
@@ -24,21 +24,21 @@ django-storages[google]==1.14.2 # https://github.com/jschneier/django-storages
django-storages[azure]==1.14.2 # https://github.com/jschneier/django-storages
{%- endif %}
{%- if cookiecutter.mail_service == 'Mailgun' %}
-django-anymail[mailgun]==10.2 # https://github.com/anymail/django-anymail
+django-anymail[mailgun]==10.3 # https://github.com/anymail/django-anymail
{%- elif cookiecutter.mail_service == 'Amazon SES' %}
-django-anymail[amazon-ses]==10.2 # https://github.com/anymail/django-anymail
+django-anymail[amazon-ses]==10.3 # https://github.com/anymail/django-anymail
{%- elif cookiecutter.mail_service == 'Mailjet' %}
-django-anymail[mailjet]==10.2 # https://github.com/anymail/django-anymail
+django-anymail[mailjet]==10.3 # https://github.com/anymail/django-anymail
{%- elif cookiecutter.mail_service == 'Mandrill' %}
-django-anymail[mandrill]==10.2 # https://github.com/anymail/django-anymail
+django-anymail[mandrill]==10.3 # https://github.com/anymail/django-anymail
{%- elif cookiecutter.mail_service == 'Postmark' %}
-django-anymail[postmark]==10.2 # https://github.com/anymail/django-anymail
+django-anymail[postmark]==10.3 # https://github.com/anymail/django-anymail
{%- elif cookiecutter.mail_service == 'Sendgrid' %}
-django-anymail[sendgrid]==10.2 # https://github.com/anymail/django-anymail
+django-anymail[sendgrid]==10.3 # https://github.com/anymail/django-anymail
{%- elif cookiecutter.mail_service == 'SendinBlue' %}
-django-anymail[sendinblue]==10.2 # https://github.com/anymail/django-anymail
+django-anymail[sendinblue]==10.3 # https://github.com/anymail/django-anymail
{%- elif cookiecutter.mail_service == 'SparkPost' %}
-django-anymail[sparkpost]==10.2 # https://github.com/anymail/django-anymail
+django-anymail[sparkpost]==10.3 # https://github.com/anymail/django-anymail
{%- elif cookiecutter.mail_service == 'Other SMTP' %}
-django-anymail==10.2 # https://github.com/anymail/django-anymail
+django-anymail==10.3 # https://github.com/anymail/django-anymail
{%- endif %}
diff --git a/{{cookiecutter.project_slug}}/runtime.txt b/{{cookiecutter.project_slug}}/runtime.txt
index 1f79d441f..6e797d468 100644
--- a/{{cookiecutter.project_slug}}/runtime.txt
+++ b/{{cookiecutter.project_slug}}/runtime.txt
@@ -1 +1 @@
-python-3.11.7
+python-3.12.2
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..7c774185e 100644
--- a/{{cookiecutter.project_slug}}/webpack/dev.config.js
+++ b/{{cookiecutter.project_slug}}/webpack/dev.config.js
@@ -6,12 +6,22 @@ module.exports = merge(commonConfig, {
devtool: 'inline-source-map',
devServer: {
port: 3000,
- proxy: {
- {%- if cookiecutter.use_docker == 'n' %}
- '/': 'http://0.0.0.0:8000',
- {%- else %}
- '/': 'http://django:8000',
- {%- endif %}
+ proxy: [
+ {
+ context: ['/'],
+ {%- if cookiecutter.use_docker == 'n' %}
+ target: 'http://0.0.0.0:8000',
+ {%- else %}
+ target: 'http://django:8000',
+ {%- endif %}
+ },
+ ],
+ client: {
+ overlay: {
+ errors: true,
+ warnings: false,
+ runtimeErrors: true,
+ },
},
// We need hot=false (Disable HMR) to set liveReload=true
hot: false,
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}}/templates/account/account_inactive.html b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/account_inactive.html
deleted file mode 100644
index a9112cf09..000000000
--- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/account_inactive.html
+++ /dev/null
@@ -1,12 +0,0 @@
-{% raw %}{% extends "account/base.html" %}
-
-{% load i18n %}
-
-{% block head_title %}
- {% translate "Account Inactive" %}
-{% endblock head_title %}
-{% block inner %}
- {% translate "Account Inactive" %}
- {% translate "This account is inactive." %}
-{% endblock inner %}
-{%- endraw %}
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/base.html b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/base.html
deleted file mode 100644
index 057618257..000000000
--- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/base.html
+++ /dev/null
@@ -1,14 +0,0 @@
-{% raw %}{% extends "base.html" %}
-
-{% block title %}
- {% block head_title %}
- {% endblock head_title %}
-{% endblock title %}
-{% block content %}
-
-
- {% block inner %}{% endblock inner %}
-
-
-{% endblock content %}
-{%- endraw %}
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/base_manage_password.html b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/base_manage_password.html
new file mode 100644
index 000000000..515f5244a
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/base_manage_password.html
@@ -0,0 +1,11 @@
+{% raw %}{% extends "account/base_manage.html" %}
+
+{% block main %}
+
+
+ {% block content %}
+ {% endblock content %}
+
+
+{% endblock main %}{% endraw %}
+
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/email.html b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/email.html
deleted file mode 100644
index 37770f00c..000000000
--- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/email.html
+++ /dev/null
@@ -1,80 +0,0 @@
-{% raw %}
-{% extends "account/base.html" %}
-
-{% load i18n %}
-{% load crispy_forms_tags %}
-
-{% block head_title %}
- {% translate "Account" %}
-{% endblock head_title %}
-{% block inner %}
- {% translate "E-mail Addresses" %}
- {% if user.emailaddress_set.all %}
- {% translate "The following e-mail addresses are associated with your account:" %}
-
- {% else %}
-
- {% translate "Warning:" %} {% translate "You currently do not have any e-mail address set up. You should really add an e-mail address so you can receive notifications, reset your password, etc." %}
-
- {% endif %}
- {% translate "Add E-mail Address" %}
-
-{% endblock inner %}
-{% block inline_javascript %}
- {{ block.super }}
-
-{% endblock inline_javascript %}
-{%- endraw %}
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/email_confirm.html b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/email_confirm.html
deleted file mode 100644
index 40ca4a47b..000000000
--- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/email_confirm.html
+++ /dev/null
@@ -1,28 +0,0 @@
-{% raw %}{% extends "account/base.html" %}
-
-{% load i18n %}
-{% load account %}
-
-{% block head_title %}
- {% translate "Confirm E-mail Address" %}
-{% endblock head_title %}
-{% block inner %}
- {% translate "Confirm E-mail Address" %}
- {% if confirmation %}
- {% user_display confirmation.email_address.user as user_display %}
-
- {% blocktranslate with confirmation.email_address.email as email %}Please confirm that {{ email }} is an e-mail address for user {{ user_display }}.{% endblocktranslate %}
-
-
- {% else %}
- {% url 'account_email' as email_url %}
-
- {% blocktranslate %}This e-mail confirmation link expired or is invalid. Please issue a new e-mail confirmation request .{% endblocktranslate %}
-
- {% endif %}
-{% endblock inner %}
-{%- endraw %}
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/login.html b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/login.html
deleted file mode 100644
index 5737afc06..000000000
--- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/login.html
+++ /dev/null
@@ -1,53 +0,0 @@
-{% raw %}{% extends "account/base.html" %}
-
-{% load i18n %}
-{% load account socialaccount %}
-{% load crispy_forms_tags %}
-
-{% block head_title %}
- {% translate "Sign In" %}
-{% endblock head_title %}
-{% block inner %}
- {% translate "Sign In" %}
- {% get_providers as socialaccount_providers %}
- {% if socialaccount_providers %}
-
- {% translate "Please sign in with one of your existing third party accounts:" %}
- {% if ACCOUNT_ALLOW_REGISTRATION %}
- {% blocktranslate trimmed %}
- Or, sign up
- for a {{ site_name }} account and sign in below:
- {% endblocktranslate %}
- {% endif %}
-
-
-
- {% include "socialaccount/snippets/provider_list.html" with process="login" %}
-
-
{% translate "or" %}
-
- {% include "socialaccount/snippets/login_extra.html" %}
- {% else %}
- {% if ACCOUNT_ALLOW_REGISTRATION %}
-
- {% blocktranslate trimmed %}
- If you have not created an account yet, then please
- sign up first.
- {% endblocktranslate %}
-
- {% endif %}
- {% endif %}
-
-{% endblock inner %}
-{%- endraw %}
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/logout.html b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/logout.html
deleted file mode 100644
index 43ae9ed38..000000000
--- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/logout.html
+++ /dev/null
@@ -1,21 +0,0 @@
-{% raw %}{% extends "account/base.html" %}
-
-{% load i18n %}
-
-{% block head_title %}
- {% translate "Sign Out" %}
-{% endblock head_title %}
-{% block inner %}
- {% translate "Sign Out" %}
- {% translate "Are you sure you want to sign out?" %}
-
-{% endblock inner %}
-{%- endraw %}
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/password_change.html b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/password_change.html
deleted file mode 100644
index 2e6110d5d..000000000
--- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/password_change.html
+++ /dev/null
@@ -1,19 +0,0 @@
-{% raw %}{% extends "account/base.html" %}
-
-{% load i18n %}
-{% load crispy_forms_tags %}
-
-{% block head_title %}
- {% translate "Change Password" %}
-{% endblock head_title %}
-{% block inner %}
- {% translate "Change Password" %}
-
-{% endblock inner %}
-{%- endraw %}
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/password_reset.html b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/password_reset.html
deleted file mode 100644
index 0c184269a..000000000
--- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/password_reset.html
+++ /dev/null
@@ -1,29 +0,0 @@
-{% raw %}{% extends "account/base.html" %}
-
-{% load i18n %}
-{% load account %}
-{% load crispy_forms_tags %}
-
-{% block head_title %}
- {% translate "Password Reset" %}
-{% endblock head_title %}
-{% block inner %}
- {% translate "Password Reset" %}
- {% if user.is_authenticated %}
- {% include "account/snippets/already_logged_in.html" %}
- {% endif %}
-
- {% translate "Forgotten your password? Enter your e-mail address below, and we'll send you an e-mail allowing you to reset it." %}
-
-
- {% blocktranslate %}Please contact us if you have any trouble resetting your password.{% endblocktranslate %}
-{% endblock inner %}
-{%- endraw %}
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/password_reset_done.html b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/password_reset_done.html
deleted file mode 100644
index a596425bb..000000000
--- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/password_reset_done.html
+++ /dev/null
@@ -1,18 +0,0 @@
-{% raw %}{% extends "account/base.html" %}
-
-{% load i18n %}
-{% load account %}
-
-{% block head_title %}
- {% translate "Password Reset" %}
-{% endblock head_title %}
-{% block inner %}
- {% translate "Password Reset" %}
- {% if user.is_authenticated %}
- {% include "account/snippets/already_logged_in.html" %}
- {% endif %}
-
- {% blocktranslate %}We have sent you an e-mail. Please contact us if you do not receive it within a few minutes.{% endblocktranslate %}
-
-{% endblock inner %}
-{%- endraw %}
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/password_reset_from_key.html b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/password_reset_from_key.html
deleted file mode 100644
index a958ba089..000000000
--- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/password_reset_from_key.html
+++ /dev/null
@@ -1,37 +0,0 @@
-{% raw %}{% extends "account/base.html" %}
-
-{% load i18n %}
-{% load crispy_forms_tags %}
-
-{% block head_title %}
- {% translate "Change Password" %}
-{% endblock head_title %}
-{% block inner %}
-
- {% if token_fail %}
- {% translate "Bad Token" %}
- {% else %}
- {% translate "Change Password" %}
- {% endif %}
-
- {% if token_fail %}
- {% url 'account_reset_password' as passwd_reset_url %}
-
- {% blocktranslate %}The password reset link was invalid, possibly because it has already been used. Please request a new password reset .{% endblocktranslate %}
-
- {% else %}
- {% if form %}
-
- {% else %}
- {% translate "Your password is now changed." %}
- {% endif %}
- {% endif %}
-{% endblock inner %}
-{%- endraw %}
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/password_reset_from_key_done.html b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/password_reset_from_key_done.html
deleted file mode 100644
index ee399b404..000000000
--- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/password_reset_from_key_done.html
+++ /dev/null
@@ -1,12 +0,0 @@
-{% raw %}{% extends "account/base.html" %}
-
-{% load i18n %}
-
-{% block head_title %}
- {% translate "Change Password" %}
-{% endblock head_title %}
-{% block inner %}
- {% translate "Change Password" %}
- {% translate "Your password is now changed." %}
-{% endblock inner %}
-{%- endraw %}
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/password_set.html b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/password_set.html
deleted file mode 100644
index 3efc30874..000000000
--- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/password_set.html
+++ /dev/null
@@ -1,22 +0,0 @@
-{% raw %}{% extends "account/base.html" %}
-
-{% load i18n %}
-{% load crispy_forms_tags %}
-
-{% block head_title %}
- {% translate "Set Password" %}
-{% endblock head_title %}
-{% block inner %}
- {% translate "Set Password" %}
-
-{% endblock inner %}
-{%- endraw %}
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/signup.html b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/signup.html
deleted file mode 100644
index 54150a474..000000000
--- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/signup.html
+++ /dev/null
@@ -1,28 +0,0 @@
-{% raw %}{% extends "account/base.html" %}
-
-{% load i18n %}
-{% load crispy_forms_tags %}
-
-{% block head_title %}
- {% translate "Signup" %}
-{% endblock head_title %}
-{% block inner %}
- {% translate "Sign Up" %}
-
- {% blocktranslate %}Already have an account? Then please sign in .{% endblocktranslate %}
-
-
-{% endblock inner %}
-{%- endraw %}
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/signup_closed.html b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/signup_closed.html
deleted file mode 100644
index b3472ed6d..000000000
--- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/signup_closed.html
+++ /dev/null
@@ -1,12 +0,0 @@
-{% raw %}{% extends "account/base.html" %}
-
-{% load i18n %}
-
-{% block head_title %}
- {% translate "Sign Up Closed" %}
-{% endblock head_title %}
-{% block inner %}
- {% translate "Sign Up Closed" %}
- {% translate "We are sorry, but the sign up is currently closed." %}
-{% endblock inner %}
-{%- endraw %}
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/verification_sent.html b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/verification_sent.html
deleted file mode 100644
index d71bbc41a..000000000
--- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/verification_sent.html
+++ /dev/null
@@ -1,14 +0,0 @@
-{% raw %}{% extends "account/base.html" %}
-
-{% load i18n %}
-
-{% block head_title %}
- {% translate "Verify Your E-mail Address" %}
-{% endblock head_title %}
-{% block inner %}
- {% translate "Verify Your E-mail Address" %}
-
- {% blocktranslate %}We have sent an e-mail to you for verification. Follow the link provided to finalize the signup process. Please contact us if you do not receive it within a few minutes.{% endblocktranslate %}
-
-{% endblock inner %}
-{%- endraw %}
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/verified_email_required.html b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/verified_email_required.html
deleted file mode 100644
index b736581ce..000000000
--- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/account/verified_email_required.html
+++ /dev/null
@@ -1,25 +0,0 @@
-{% raw %}{% extends "account/base.html" %}
-
-{% load i18n %}
-
-{% block head_title %}
- {% translate "Verify Your E-mail Address" %}
-{% endblock head_title %}
-{% block inner %}
- {% translate "Verify Your E-mail Address" %}
- {% url 'account_email' as email_url %}
-
- {% blocktranslate %}This part of the site requires us to verify that
-you are who you claim to be. For this purpose, we require that you
-verify ownership of your e-mail address. {% endblocktranslate %}
-
-
- {% blocktranslate %}We have sent an e-mail to you for
-verification. Please click on the link inside this e-mail. Please
-contact us if you do not receive it within a few minutes.{% endblocktranslate %}
-
-
- {% blocktranslate %}Note: you can still change your e-mail address .{% endblocktranslate %}
-
-{% endblock inner %}
-{%- endraw %}
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/allauth/elements/alert.html b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/allauth/elements/alert.html
new file mode 100644
index 000000000..090c2cbf6
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/allauth/elements/alert.html
@@ -0,0 +1,7 @@
+{% raw %}{% load i18n %}
+{% load allauth %}
+
+
+ {% slot message %}
+{% endslot %}
+
{% endraw %}
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/allauth/elements/badge.html b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/allauth/elements/badge.html
new file mode 100644
index 000000000..7093939da
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/allauth/elements/badge.html
@@ -0,0 +1,6 @@
+{% raw %}{% load allauth %}
+
+
+ {% slot %}
+{% endslot %}
+ {% endraw %}
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/allauth/elements/button.html b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/allauth/elements/button.html
new file mode 100644
index 000000000..1c0ae4b99
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/allauth/elements/button.html
@@ -0,0 +1,20 @@
+{% raw %}{% load allauth %}
+
+{% comment %} djlint:off {% endcomment %}
+<{% if attrs.href %}a href="{{ attrs.href }}"{% else %}button{% endif %}
+ {% if attrs.form %}form="{{ attrs.form }}"{% endif %}
+ {% if attrs.id %}id="{{ attrs.id }}"{% endif %}
+ {% if attrs.name %}name="{{ attrs.name }}"{% endif %}
+ {% if attrs.type %}type="{{ attrs.type }}"{% endif %}
+ class="btn
+{% if 'success' in attrs.tags %}btn-success
+{% elif 'warning' in attrs.tags %}btn-warning
+{% elif 'secondary' in attrs.tags %}btn-secondary
+{% elif 'danger' in attrs.tags %}btn-danger
+{% elif 'primary' in attrs.tags %}btn-primary
+{% else %}btn-primary
+{% endif %}"
+>
+ {% slot %}
+ {% endslot %}
+ {% if attrs.href %}a{% else %}button{% endif %}>{% endraw %}
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/allauth/elements/field.html b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/allauth/elements/field.html
new file mode 100644
index 000000000..8585f7a4d
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/allauth/elements/field.html
@@ -0,0 +1,66 @@
+{% raw %}{% load allauth %}
+{% load crispy_forms_tags %}
+
+{% if attrs.type == "textarea" %}
+
+
+
+ {% slot label %}
+ {% endslot %}
+
+
+
+
+{% elif attrs.type == "radio" %}
+
+
+
+
+
+ {% slot label %}
+ {% endslot %}
+
+
+
+
+{% else %}
+
+
+ {% slot label %}
+ {% endslot %}
+
+
+
+
+
+{% endif %}
+{% if slots.help_text %}
+ {% slot help_text %}{% endslot %}
+{% endif %}{% endraw %}
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/allauth/elements/fields.html b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/allauth/elements/fields.html
new file mode 100644
index 000000000..6a2f3c2cb
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/allauth/elements/fields.html
@@ -0,0 +1,3 @@
+{% raw %}{% load crispy_forms_tags %}
+
+{{ attrs.form|crispy }}{% endraw %}
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/allauth/elements/panel.html b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/allauth/elements/panel.html
new file mode 100644
index 000000000..ce179b587
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/allauth/elements/panel.html
@@ -0,0 +1,19 @@
+{% raw %}{% load allauth %}
+
+
+
+
+
+ {% slot title %}
+ {% endslot %}
+
+ {% slot body %}
+ {% endslot %}
+ {% if slots.actions %}
+
+ {% for action in slots.actions %}{{ action }} {% endfor %}
+
+ {% endif %}
+
+
+ {% endraw %}
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/allauth/elements/table.html b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/allauth/elements/table.html
new file mode 100644
index 000000000..7ceb9234d
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/allauth/elements/table.html
@@ -0,0 +1,6 @@
+{% raw %}{% load allauth %}
+
+
+ {% slot %}
+{% endslot %}
+
{% endraw %}
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/allauth/layouts/entrance.html b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/allauth/layouts/entrance.html
new file mode 100644
index 000000000..91cbaba3f
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/allauth/layouts/entrance.html
@@ -0,0 +1,29 @@
+{% raw %}{% extends "base.html" %}
+{% load i18n %}
+{% block bodyclass %}bg-light{% endblock bodyclass %}
+
+{% block css %}{{ block.super }}{% endblock css %}
+{% block title %}
+ {% block head_title %}
+ {% trans "Sign In" %}
+ {% endblock head_title %}
+{% endblock title %}
+{% block body %}
+
+
+ {% if messages %}
+ {% for message in messages %}
+
+ {{ message }}
+
+
+ {% endfor %}
+ {% endif %}
+ {% block content %}
+ {% endblock content %}
+
+
+{% endblock body %}{% endraw %}
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/allauth/layouts/manage.html b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/allauth/layouts/manage.html
new file mode 100644
index 000000000..b7ff36ba2
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/allauth/layouts/manage.html
@@ -0,0 +1,6 @@
+{% raw %}{% extends "base.html" %}
+
+{% block main %}
+ {% block content %}
+ {% endblock content %}
+{% endblock main %}{% endraw %}
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/base.html b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/base.html
index 74c4b23da..4f65a8ad5 100644
--- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/base.html
+++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/base.html
@@ -111,7 +111,8 @@
{% raw %}
{% endblock javascript %}
-
+
+ {% block body %}
@@ -171,10 +172,14 @@
{% endfor %}
{% endif %}
- {% block content %}
- Use this document as a way to quick start any new project.
- {% endblock content %}
+ {% block main %}
+ {% block content %}
+ Use this document as a way to quick start any new project.
+ {% endblock content %}
+ {% endblock main %}
+
+ {% endblock body %}
{% block modal %}
{% endblock modal %}
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/users/user_detail.html b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/users/user_detail.html
index 21c4c606c..458d180bf 100644
--- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/users/user_detail.html
+++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/users/user_detail.html
@@ -26,6 +26,9 @@
E-Mail
+ MFA
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/adapters.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/adapters.py
index eaa59100b..253a84933 100644
--- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/adapters.py
+++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/adapters.py
@@ -23,7 +23,11 @@ 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 pre_social_login(self, request, sociallogin):
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/admin.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/admin.py
index 43e7683ea..e3c68b891 100644
--- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/admin.py
+++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/admin.py
@@ -1,17 +1,17 @@
from django.conf import settings
from django.contrib import admin
from django.contrib.auth import admin as auth_admin
-from django.contrib.auth import get_user_model, decorators
+from django.contrib.auth.decorators import login_required
from django.utils.translation import gettext_lazy as _
-from {{ cookiecutter.project_slug }}.users.forms import UserAdminChangeForm, UserAdminCreationForm
-
-User = get_user_model()
+from .forms import UserAdminChangeForm
+from .forms import UserAdminCreationForm
+from .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
- admin.site.login = decorators.login_required(admin.site.login) # type: ignore[method-assign]
+ admin.site.login = login_required(admin.site.login) # type: ignore[method-assign]
@admin.register(User)
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/api/serializers.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/api/serializers.py
index c3156254f..30c2c3166 100644
--- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/api/serializers.py
+++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/api/serializers.py
@@ -1,13 +1,9 @@
-from django.contrib.auth import get_user_model
from rest_framework import serializers
-from {{ cookiecutter.project_slug }}.users.models import User as UserType
+from {{ cookiecutter.project_slug }}.users.models import User
-User = get_user_model()
-
-
-class UserSerializer(serializers.ModelSerializer[UserType]):
+class UserSerializer(serializers.ModelSerializer[User]):
class Meta:
model = User
fields = ["email", "uuid", "first_name", "last_name", "url"]
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 f4b29e78a..91b8ea69f 100644
--- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/api/views.py
+++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/api/views.py
@@ -2,13 +2,15 @@ import uuid
from django.contrib.auth import get_user_model
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
-from .serializers import UserSerializer
+from {{ cookiecutter.project_slug }}.users.models import User
-User = get_user_model()
+from .serializers import UserSerializer
class UserViewSet(RetrieveModelMixin, ListModelMixin, UpdateModelMixin, 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..d8beaa48e 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 .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 4a3cc2d4c..f36e1316a 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
@@ -32,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",
),
),
(
@@ -62,7 +62,7 @@ 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",
),
),
(
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/models.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/models.py
index 551c4a153..500792a5e 100644
--- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/models.py
+++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/models.py
@@ -10,7 +10,11 @@ from .managers import UserManager
class User(AbstractUser):
- """Default custom user model for {{cookiecutter.project_name}}."""
+ """
+ Default custom user model for {{cookiecutter.project_name}}.
+ If adding fields that need to be filled at user signup,
+ check forms.SignupForm and forms.SocialSignupForms accordingly.
+ """
email = models.EmailField(_("email address"), unique=True)
username = None
@@ -29,8 +33,8 @@ class User(AbstractUser):
objects = UserManager()
- def get_absolute_url(self):
- """Get url for user's detail view.
+ def get_absolute_url(self) -> str:
+ """Get URL for user's detail view.
Returns:
str: URL for user detail.
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tasks.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tasks.py
index c99341c5f..ca51cd740 100644
--- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tasks.py
+++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tasks.py
@@ -1,11 +1,9 @@
-from django.contrib.auth import get_user_model
+from celery import shared_task
-from config import celery_app
-
-User = get_user_model()
+from .models import User
-@celery_app.task()
+@shared_task()
def get_users_count():
"""A pointless Celery task to demonstrate usage."""
return User.objects.count()
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 730178d55..019bffffb 100644
--- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/factories.py
+++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/factories.py
@@ -12,7 +12,7 @@ class UserFactory(DjangoModelFactory):
last_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 c1a234116..3aec25eaa 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,3 +1,5 @@
+import contextlib
+from http import HTTPStatus
from importlib import reload
import pytest
@@ -13,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,
@@ -35,28 +37,26 @@ class TestUserAdmin:
"password2": "My_R@ndom-P@ssw0rd",
},
)
- assert response.status_code == 302
+ assert response.status_code == HTTPStatus.FOUND
assert User.objects.filter(email="new-admin@example.com").exists()
def test_view_user(self, admin_client):
user = User.objects.get(email="admin@example.com")
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):
+ @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 # pylint: disable=import-outside-toplevel
+ import {{ cookiecutter.project_slug }}.users.admin as users_admin
- try:
+ with contextlib.suppress(admin.sites.AlreadyRegistered):
reload(users_admin)
- except admin.sites.AlreadyRegistered:
- pass
- @pytest.mark.django_db
- @pytest.mark.usefixtures("force_allauth")
+ @pytest.mark.django_db()
+ @pytest.mark.usefixtures("_force_allauth")
def test_allauth_login(self, rf, settings):
request = rf.get("/fake-url")
request.user = AnonymousUser()
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 f27d3ef3b..cb8c9bfff 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,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
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 0f2a5d913..cc491f8a1 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 == {
"email": user.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 d92bcc3ad..b1d30fa2f 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
@@ -29,7 +28,7 @@ class TestUserAdminCreationForm:
"last_name": user.last_name,
"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 d937879f3..963ac758c 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,14 +6,14 @@ 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(
first_name="John",
last_name="Doe",
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
@@ -26,7 +26,7 @@ class TestUserManager:
first_name="Admin",
last_name="User",
email="admin@example.com",
- password="something-r@nd0m!",
+ password="something-r@nd0m!", # noqa: S106
)
assert user.email == "admin@example.com"
assert user.is_staff
@@ -38,12 +38,12 @@ class TestUserManager:
first_name="Test",
last_name="User",
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 3130f2060..97e96d2a7 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
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 264a53658..3e034f327 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
@@ -90,7 +91,7 @@ class TestUserDetailView:
response = user_detail_view(request, uuid=user.uuid)
- 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/")
@@ -100,5 +101,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 d9c638e13..4d04232e5 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 .views import user_detail_view
+from .views import user_redirect_view
+from .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 2962731bd..36be53fe3 100644
--- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/views.py
+++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/views.py
@@ -1,11 +1,12 @@
-from django.contrib.auth import get_user_model
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
-User = get_user_model()
+from {{ cookiecutter.project_slug }}.users.models import User
class UserDetailView(LoginRequiredMixin, DetailView):
@@ -23,7 +24,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 %}