Merge branch 'master' into mfosterw/issue4872

This commit is contained in:
Matthew Foster Walsh 2024-02-20 14:44:08 -05:00 committed by GitHub
commit f2a6d5aaec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
76 changed files with 815 additions and 396 deletions

View File

@ -1508,5 +1508,20 @@
"name": "Nix Siow", "name": "Nix Siow",
"github_login": "nixsiow", "github_login": "nixsiow",
"twitter_username": "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": ""
} }
] ]

View File

@ -23,7 +23,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: tiangolo/issue-manager@0.4.1 - uses: tiangolo/issue-manager@0.5.0
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
config: > config: >
@ -39,5 +39,9 @@ jobs:
"waiting": { "waiting": {
"delay": 864000, "delay": 864000,
"message": "Automatically closing after waiting for additional info. To re-open, please provide the additional information requested." "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."
} }
} }

View File

@ -37,7 +37,7 @@ jobs:
run: pre-commit autoupdate run: pre-commit autoupdate
- name: Create Pull Request - name: Create Pull Request
uses: peter-evans/create-pull-request@v5 uses: peter-evans/create-pull-request@v6
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
branch: update/pre-commit-autoupdate branch: update/pre-commit-autoupdate

View File

@ -33,7 +33,7 @@ repos:
exclude: hooks/ exclude: hooks/
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 23.12.1 rev: 24.2.0
hooks: hooks:
- id: black - id: black

View File

@ -3,6 +3,247 @@ All enhancements and patches to Cookiecutter Django will be documented in this f
<!-- GENERATOR_PLACEHOLDER --> <!-- GENERATOR_PLACEHOLDER -->
## 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 &amp; 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 &amp; media storage classes by passing options in the `STORAGES` setting ([#4803](https://github.com/cookiecutter/cookiecutter-django/pull/4803))
- Add registry to Docker images names ([#4804](https://github.com/cookiecutter/cookiecutter-django/pull/4804))
## 2024.01.24
### Changed
- Migrate to the unified `STORAGES` setting added in Django 4.2 ([#4477](https://github.com/cookiecutter/cookiecutter-django/pull/4477))
### Updated
- Update uvicorn to 0.27.0 ([#4800](https://github.com/cookiecutter/cookiecutter-django/pull/4800))
## 2024.01.21
### Documentation
- Update traefik doc links ([#4798](https://github.com/cookiecutter/cookiecutter-django/pull/4798))
### Updated
- Bump browser-sync from 2.29.3 to 3.0.2 in /{{cookiecutter.project_slug}} ([#4765](https://github.com/cookiecutter/cookiecutter-django/pull/4765))
## 2024.01.19
### Updated
- Update drf-spectacular to 0.27.1 ([#4797](https://github.com/cookiecutter/cookiecutter-django/pull/4797))
## 2024.01.17
### Changed
- Add a test to cover `DJANGO_ADMIN_FORCE_ALLAUTH` ([#4790](https://github.com/cookiecutter/cookiecutter-django/pull/4790))
### Updated
- Bump webpack-bundle-tracker to 3.0.1 ([#4781](https://github.com/cookiecutter/cookiecutter-django/pull/4781))
- Update django-webpack-loader to 3.0.1 ([#4793](https://github.com/cookiecutter/cookiecutter-django/pull/4793))
- Bump postcss-loader to 8.0.0 ([#4795](https://github.com/cookiecutter/cookiecutter-django/pull/4795))
- Update uvicorn to 0.26.0 ([#4794](https://github.com/cookiecutter/cookiecutter-django/pull/4794))
## 2024.01.16
### Updated
- Bump sass-loader from 13.3.3 to 14.0.0 in /{{cookiecutter.project_slug}} ([#4791](https://github.com/cookiecutter/cookiecutter-django/pull/4791))
## 2024.01.15
### Documentation
- Update allauth documentation links ([#4786](https://github.com/cookiecutter/cookiecutter-django/pull/4786))
### Updated
- Update django-allauth to 0.60.1 ([#4787](https://github.com/cookiecutter/cookiecutter-django/pull/4787))
## 2024.01.11 ## 2024.01.11

View File

@ -964,6 +964,13 @@ Listed in alphabetical order.
</td> </td>
<td></td> <td></td>
</tr> </tr>
<tr>
<td>henningbra</td>
<td>
<a href="https://github.com/henningbra">henningbra</a>
</td>
<td></td>
</tr>
<tr> <tr>
<td>Henrique G. G. Pereira</td> <td>Henrique G. G. Pereira</td>
<td> <td>
@ -1076,6 +1083,13 @@ Listed in alphabetical order.
</td> </td>
<td></td> <td></td>
</tr> </tr>
<tr>
<td>Jens Kaeske</td>
<td>
<a href="https://github.com/jkaeske">jkaeske</a>
</td>
<td></td>
</tr>
<tr> <tr>
<td>Jens Nilsson</td> <td>Jens Nilsson</td>
<td> <td>
@ -1636,6 +1650,13 @@ Listed in alphabetical order.
</td> </td>
<td></td> <td></td>
</tr> </tr>
<tr>
<td>Paul Wulff</td>
<td>
<a href="https://github.com/mtmpaulwulff">mtmpaulwulff</a>
</td>
<td></td>
</tr>
<tr> <tr>
<td>Pawan Chaurasia</td> <td>Pawan Chaurasia</td>
<td> <td>

View File

@ -4,40 +4,30 @@ Linters
.. index:: 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 $ ruff format --diff .
* Exclude ``.tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules`` $ 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 <python files that you wish to lint> The config for ruff is located in pyproject.toml.
On of the most important option is `tool.ruff.lint.select`.
The config for pylint is located in .pylintrc. It specifies: `select` determines which linters are run. In example, `DJ <https://docs.astral.sh/ruff/rules/#flake8-django-dj>`_ refers to flake8-django.
For a full list of available linters, see `https://docs.astral.sh/ruff/rules/ <https://docs.astral.sh/ruff/rules/>`_
* 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 <python files that you wish to lint>
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``

View File

@ -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. 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:: .. seealso::

View File

@ -8,6 +8,7 @@ NOTE:
TODO: restrict Cookiecutter Django project initialization to TODO: restrict Cookiecutter Django project initialization to
Python 3.x environments only Python 3.x environments only
""" """
from __future__ import print_function from __future__ import print_function
import json import json
@ -429,10 +430,6 @@ def remove_drf_starter_files():
os.remove(os.path.join("{{cookiecutter.project_slug}}", "users", "tests", "test_swagger.py")) 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(): def main():
debug = "{{ cookiecutter.debug }}".lower() == "y" debug = "{{ cookiecutter.debug }}".lower() == "y"
@ -499,7 +496,6 @@ def main():
WARNING + "You chose to not use any cloud providers nor Docker, " WARNING + "You chose to not use any cloud providers nor Docker, "
"media files won't be served in production." + TERMINATOR "media files won't be served in production." + TERMINATOR
) )
remove_storages_module()
if "{{ cookiecutter.use_celery }}".lower() == "n": if "{{ cookiecutter.use_celery }}".lower() == "n":
remove_celery_files() remove_celery_files()

View File

@ -7,6 +7,7 @@ NOTE:
TODO: restrict Cookiecutter Django project initialization TODO: restrict Cookiecutter Django project initialization
to Python 3.x environments only to Python 3.x environments only
""" """
from __future__ import print_function from __future__ import print_function
import sys import sys

View File

@ -4,17 +4,15 @@ binaryornot==0.4.4
# Code quality # Code quality
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
black==23.12.1 ruff==0.2.1
isort==5.13.2 django-upgrade==1.16.0
flake8==7.0.0
django-upgrade==1.15.0
djlint==1.34.1 djlint==1.34.1
pre-commit==3.6.0 pre-commit==3.6.1
# Testing # Testing
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
tox==4.12.0 tox==4.13.0
pytest==7.4.4 pytest==8.0.1
pytest-xdist==3.5.0 pytest-xdist==3.5.0
pytest-cookies==0.7.0 pytest-cookies==0.7.0
pytest-instafail==0.5.0 pytest-instafail==0.5.0
@ -22,7 +20,7 @@ pyyaml==6.0.1
# Scripting # Scripting
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
PyGithub==2.1.1 PyGithub==2.2.0
gitpython==3.1.40 gitpython==3.1.42
jinja2==3.1.3 jinja2==3.1.3
requests==2.31.0 requests==2.31.0

View File

@ -6,6 +6,7 @@ patches, only comparing major and minor version numbers.
This script handles when there are multiple Django versions that need This script handles when there are multiple Django versions that need
to keep up to date. to keep up to date.
""" """
from __future__ import annotations from __future__ import annotations
import os import os

View File

@ -5,7 +5,7 @@ except ImportError:
from distutils.core import setup from distutils.core import setup
# We use calendar versioning # We use calendar versioning
version = "2024.01.11" version = "2024.02.19"
with open("README.md") as readme_file: with open("README.md") as readme_file:
long_description = readme_file.read() long_description = readme_file.read()

View File

@ -180,28 +180,25 @@ def test_project_generation(cookies, context, context_override):
@pytest.mark.parametrize("context_override", SUPPORTED_COMBINATIONS, ids=_fixture_id) @pytest.mark.parametrize("context_override", SUPPORTED_COMBINATIONS, ids=_fixture_id)
def test_flake8_passes(cookies, context_override): def test_ruff_check_passes(cookies, context_override):
"""Generated project should pass flake8.""" """Generated project should pass ruff check."""
result = cookies.bake(extra_context=context_override) result = cookies.bake(extra_context=context_override)
try: try:
sh.flake8(_cwd=str(result.project_path)) sh.ruff("check", ".", _cwd=str(result.project_path))
except sh.ErrorReturnCode as e: except sh.ErrorReturnCode as e:
pytest.fail(e.stdout.decode()) pytest.fail(e.stdout.decode())
@auto_fixable @auto_fixable
@pytest.mark.parametrize("context_override", SUPPORTED_COMBINATIONS, ids=_fixture_id) @pytest.mark.parametrize("context_override", SUPPORTED_COMBINATIONS, ids=_fixture_id)
def test_black_passes(cookies, context_override): def test_ruff_format_passes(cookies, context_override):
"""Check whether generated project passes black style.""" """Check whether generated project passes ruff format."""
result = cookies.bake(extra_context=context_override) result = cookies.bake(extra_context=context_override)
try: try:
sh.black( sh.ruff(
"--check", "format",
"--diff",
"--exclude",
"migrations",
".", ".",
_cwd=str(result.project_path), _cwd=str(result.project_path),
) )
@ -287,7 +284,7 @@ def test_travis_invokes_pytest(cookies, context, use_docker, expected_test_scrip
with open(f"{result.project_path}/.travis.yml") as travis_yml: with open(f"{result.project_path}/.travis.yml") as travis_yml:
try: try:
yml = yaml.safe_load(travis_yml)["jobs"]["include"] 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] assert yml[1]["script"] == [expected_test_script]
except yaml.YAMLError as e: except yaml.YAMLError as e:
pytest.fail(str(e)) pytest.fail(str(e))

View File

@ -30,7 +30,17 @@ docker compose -f local.yml run django python manage.py makemigrations --dry-run
docker compose -f local.yml run django python manage.py makemessages --all docker compose -f local.yml run django python manage.py makemessages --all
# Make sure the check doesn't raise any warnings # 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 # Generate the HTML for the documentation
docker compose -f local.yml run docs make html docker compose -f local.yml run docs make html

View File

@ -1,4 +1,5 @@
"""Unit tests for the hooks""" """Unit tests for the hooks"""
import os import os
from pathlib import Path from pathlib import Path

View File

@ -37,22 +37,11 @@
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.organizeImports": true "source.organizeImports": true
}, },
// Uncomment when fixed "editor.defaultFormatter": "charliermarsh.ruff",
// https://github.com/microsoft/vscode-remote-release/issues/8474
// "editor.defaultFormatter": "ms-python.black-formatter",
"formatting.blackPath": "/usr/local/bin/black",
"formatting.provider": "black",
"languageServer": "Pylance", "languageServer": "Pylance",
// "linting.banditPath": "/usr/local/py-utils/bin/bandit",
"linting.enabled": true, "linting.enabled": true,
"linting.flake8Enabled": true,
"linting.flake8Path": "/usr/local/bin/flake8",
"linting.mypyEnabled": true, "linting.mypyEnabled": true,
"linting.mypyPath": "/usr/local/bin/mypy", "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 // https://code.visualstudio.com/docs/remote/devcontainerjson-reference#_vs-code-specific-properties

View File

@ -36,7 +36,7 @@ jobs:
- name: Run pre-commit - name: Run pre-commit
uses: pre-commit/action@v3.0.0 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: pytest:
runs-on: ubuntu-latest runs-on: ubuntu-latest
{%- if cookiecutter.use_docker == 'n' %} {%- if cookiecutter.use_docker == 'n' %}
@ -69,7 +69,7 @@ jobs:
{%- if cookiecutter.use_docker == 'y' %} {%- if cookiecutter.use_docker == 'y' %}
- name: Build the Stack - name: Build the Stack
run: docker compose -f local.yml build run: docker compose -f local.yml build django
- name: Run DB Migrations - name: Run DB Migrations
run: docker compose -f local.yml run --rm django python manage.py migrate run: docker compose -f local.yml run --rm django python manage.py migrate

View File

@ -28,31 +28,20 @@ repos:
exclude: '{{cookiecutter.project_slug}}/templates/' exclude: '{{cookiecutter.project_slug}}/templates/'
- repo: https://github.com/adamchainz/django-upgrade - repo: https://github.com/adamchainz/django-upgrade
rev: '1.15.0' rev: '1.16.0'
hooks: hooks:
- id: django-upgrade - id: django-upgrade
args: ['--target-version', '4.2'] args: ['--target-version', '4.2']
- repo: https://github.com/asottile/pyupgrade # Run the Ruff linter.
rev: v3.15.0 - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.2.1
hooks: hooks:
- id: pyupgrade # Linter
args: [--py311-plus] - id: ruff
args: [--fix, --exit-non-zero-on-fix]
- repo: https://github.com/psf/black # Formatter
rev: 23.12.1 - id: ruff-format
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
- repo: https://github.com/Riverside-Healthcare/djLint - repo: https://github.com/Riverside-Healthcare/djLint
rev: v1.34.1 rev: v1.34.1

View File

@ -10,9 +10,9 @@ jobs:
include: include:
- name: "Linter" - name: "Linter"
before_script: before_script:
- pip install -q flake8 - pip install -q ruff
script: script:
- "flake8" - ruff check .
- name: "Django Test" - name: "Django Test"
{%- if cookiecutter.use_docker == 'y' %} {%- if cookiecutter.use_docker == 'y' %}
@ -24,7 +24,7 @@ jobs:
- docker compose -f local.yml run --rm django python manage.py migrate - docker compose -f local.yml run --rm django python manage.py migrate
- docker compose -f local.yml up -d - docker compose -f local.yml up -d
script: script:
- "docker compose -f local.yml run django pytest" - docker compose -f local.yml run django pytest
after_failure: after_failure:
- docker compose -f local.yml logs - docker compose -f local.yml logs
{%- else %} {%- else %}
@ -41,5 +41,5 @@ jobs:
install: install:
- pip install -r requirements/local.txt - pip install -r requirements/local.txt
script: script:
- "pytest" - pytest
{%- endif %} {%- endif %}

View File

@ -3,7 +3,7 @@
{{ cookiecutter.description }} {{ cookiecutter.description }}
[![Built with Cookiecutter Django](https://img.shields.io/badge/built%20with-Cookiecutter%20Django-ff69b4.svg?logo=cookiecutter)](https://github.com/cookiecutter/cookiecutter-django/) [![Built with Cookiecutter Django](https://img.shields.io/badge/built%20with-Cookiecutter%20Django-ff69b4.svg?logo=cookiecutter)](https://github.com/cookiecutter/cookiecutter-django/)
[![Black code style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
{%- if cookiecutter.open_source_license != "Not open source" %} {%- if cookiecutter.open_source_license != "Not open source" %}

View File

@ -1,8 +1,8 @@
# define an alias for the specific python version used in this file. # define an alias for the specific python version used in this file.
FROM python:3.11.7-slim-bookworm as python FROM docker.io/python:3.11.8-slim-bookworm as python
# Python build stage # Python build stage
FROM python as python-build-stage FROM docker.io/python as python-build-stage
ARG BUILD_ENVIRONMENT=local ARG BUILD_ENVIRONMENT=local
@ -22,7 +22,7 @@ RUN pip wheel --wheel-dir /usr/src/app/wheels \
# Python 'run' stage # Python 'run' stage
FROM python as python-run-stage FROM docker.io/python as python-run-stage
ARG BUILD_ENVIRONMENT=local ARG BUILD_ENVIRONMENT=local
ARG APP_HOME=/app ARG APP_HOME=/app

View File

@ -1,9 +1,9 @@
# define an alias for the specific python version used in this file. # define an alias for the specific python version used in this file.
FROM python:3.11.7-slim-bookworm as python FROM docker.io/python:3.11.8-slim-bookworm as python
# Python build stage # Python build stage
FROM python as python-build-stage FROM docker.io/python as python-build-stage
ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONDONTWRITEBYTECODE 1
@ -26,7 +26,7 @@ RUN pip wheel --no-cache-dir --wheel-dir /usr/src/app/wheels \
# Python 'run' stage # Python 'run' stage
FROM python as python-run-stage FROM docker.io/python as python-run-stage
ARG BUILD_ENVIRONMENT ARG BUILD_ENVIRONMENT
ENV PYTHONUNBUFFERED 1 ENV PYTHONUNBUFFERED 1

View File

@ -1,4 +1,4 @@
FROM node:20-bookworm-slim FROM docker.io/node:20-bookworm-slim
WORKDIR /app WORKDIR /app

View File

@ -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/aws/maintenance /usr/local/bin/maintenance
COPY ./compose/production/postgres/maintenance/_sourced /usr/local/bin/maintenance/_sourced COPY ./compose/production/postgres/maintenance/_sourced /usr/local/bin/maintenance/_sourced

View File

@ -1,5 +1,5 @@
{% if cookiecutter.frontend_pipeline in ['Gulp', 'Webpack'] -%} {% 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 ARG APP_HOME=/app
WORKDIR ${APP_HOME} WORKDIR ${APP_HOME}
@ -25,10 +25,10 @@ RUN npm run build
{%- endif %} {%- endif %}
# define an alias for the specific python version used in this file. # define an alias for the specific python version used in this file.
FROM python:3.11.7-slim-bookworm as python FROM docker.io/python:3.11.8-slim-bookworm as python
# Python build stage # Python build stage
FROM python as python-build-stage FROM docker.io/python as python-build-stage
ARG BUILD_ENVIRONMENT=production ARG BUILD_ENVIRONMENT=production
@ -48,7 +48,7 @@ RUN pip wheel --wheel-dir /usr/src/app/wheels \
# Python 'run' stage # Python 'run' stage
FROM python as python-run-stage FROM docker.io/python as python-run-stage
ARG BUILD_ENVIRONMENT=production ARG BUILD_ENVIRONMENT=production
ARG APP_HOME=/app ARG APP_HOME=/app

View File

@ -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 COPY ./compose/production/nginx/default.conf /etc/nginx/conf.d/default.conf

View File

@ -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 COPY ./compose/production/postgres/maintenance /usr/local/bin/maintenance
RUN chmod +x /usr/local/bin/maintenance/* RUN chmod +x /usr/local/bin/maintenance/*

View File

@ -1,4 +1,4 @@
FROM traefik:2.10.7 FROM docker.io/traefik:2.11.0
RUN mkdir -p /etc/traefik/acme \ RUN mkdir -p /etc/traefik/acme \
&& touch /etc/traefik/acme/acme.json \ && touch /etc/traefik/acme/acme.json \
&& chmod 600 /etc/traefik/acme/acme.json && chmod 600 /etc/traefik/acme/acme.json

View File

@ -6,7 +6,7 @@ entryPoints:
# http # http
address: ':80' address: ':80'
http: http:
# https://docs.traefik.io/routing/entrypoints/#entrypoint # https://doc.traefik.io/traefik/routing/entrypoints/#entrypoint
redirections: redirections:
entryPoint: entryPoint:
to: web-secure to: web-secure
@ -22,11 +22,11 @@ entryPoints:
certificatesResolvers: certificatesResolvers:
letsencrypt: letsencrypt:
# https://docs.traefik.io/master/https/acme/#lets-encrypt # https://doc.traefik.io/traefik/https/acme/#lets-encrypt
acme: acme:
email: '{{ cookiecutter.email }}' email: '{{ cookiecutter.email }}'
storage: /etc/traefik/acme/acme.json storage: /etc/traefik/acme/acme.json
# https://docs.traefik.io/master/https/acme/#httpchallenge # https://doc.traefik.io/traefik/https/acme/#httpchallenge
httpChallenge: httpChallenge:
entryPoint: web entryPoint: web
@ -44,7 +44,7 @@ http:
- csrf - csrf
service: django service: django
tls: tls:
# https://docs.traefik.io/master/routing/routers/#certresolver # https://doc.traefik.io/traefik/routing/routers/#certresolver
certResolver: letsencrypt certResolver: letsencrypt
{%- if cookiecutter.use_celery == 'y' %} {%- if cookiecutter.use_celery == 'y' %}
@ -54,7 +54,7 @@ http:
- flower - flower
service: flower service: flower
tls: tls:
# https://docs.traefik.io/master/routing/routers/#certresolver # https://doc.traefik.io/traefik/master/routing/routers/#certresolver
certResolver: letsencrypt certResolver: letsencrypt
{%- endif %} {%- endif %}
{%- if cookiecutter.cloud_provider == 'None' %} {%- if cookiecutter.cloud_provider == 'None' %}
@ -76,7 +76,7 @@ http:
middlewares: middlewares:
csrf: csrf:
# https://docs.traefik.io/master/middlewares/headers/#hostsproxyheaders # https://doc.traefik.io/traefik/master/middlewares/http/headers/#hostsproxyheaders
# https://docs.djangoproject.com/en/dev/ref/csrf/#ajax # https://docs.djangoproject.com/en/dev/ref/csrf/#ajax
headers: headers:
hostsProxyHeaders: ['X-CSRFToken'] hostsProxyHeaders: ['X-CSRFToken']
@ -102,7 +102,7 @@ http:
{%- endif %} {%- endif %}
providers: providers:
# https://docs.traefik.io/master/providers/file/ # https://doc.traefik.io/traefik/master/providers/file/
file: file:
filename: /etc/traefik/traefik.yml filename: /etc/traefik/traefik.yml
watch: true watch: true

View File

@ -1,12 +1,10 @@
from django.conf import settings 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 from {{ cookiecutter.project_slug }}.users.api.views import UserViewSet
if settings.DEBUG: router = DefaultRouter() if settings.DEBUG else SimpleRouter()
router = DefaultRouter()
else:
router = SimpleRouter()
router.register("users", UserViewSet) router.register("users", UserViewSet)

View File

@ -1,3 +1,4 @@
# ruff: noqa
""" """
ASGI config for {{ cookiecutter.project_name }} project. 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/ https://docs.djangoproject.com/en/dev/howto/deployment/asgi/
""" """
import os import os
import sys import sys
from pathlib import Path from pathlib import Path
@ -28,7 +30,7 @@ django_application = get_asgi_application()
# application = HelloWorldApplication(application) # application = HelloWorldApplication(application)
# Import websocket application here, so apps from django_application are loaded first # 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): async def application(scope, receive, send):
@ -37,4 +39,5 @@ async def application(scope, receive, send):
elif scope["type"] == "websocket": elif scope["type"] == "websocket":
await websocket_application(scope, receive, send) await websocket_application(scope, receive, send)
else: else:
raise NotImplementedError(f"Unknown scope type {scope['type']}") msg = f"Unknown scope type {scope['type']}"
raise NotImplementedError(msg)

View File

@ -1,6 +1,6 @@
""" # ruff: noqa: ERA001, E501
Base settings to build other settings files upon. """Base settings to build other settings files upon."""
"""
from pathlib import Path from pathlib import Path
import environ import environ
@ -137,7 +137,9 @@ PASSWORD_HASHERS = [
] ]
# https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators
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.MinimumLengthValidator"},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
@ -210,7 +212,7 @@ TEMPLATES = [
"{{cookiecutter.project_slug}}.users.context_processors.allauth_settings", "{{cookiecutter.project_slug}}.users.context_processors.allauth_settings",
], ],
}, },
} },
] ]
# https://docs.djangoproject.com/en/dev/ref/settings/#form-renderer # https://docs.djangoproject.com/en/dev/ref/settings/#form-renderer
@ -274,7 +276,7 @@ LOGGING = {
"level": "DEBUG", "level": "DEBUG",
"class": "logging.StreamHandler", "class": "logging.StreamHandler",
"formatter": "verbose", "formatter": "verbose",
} },
}, },
"root": {"level": "INFO", "handlers": ["console"]}, "root": {"level": "INFO", "handlers": ["console"]},
} }
@ -380,7 +382,7 @@ WEBPACK_LOADER = {
"STATS_FILE": BASE_DIR / "webpack-stats.json", "STATS_FILE": BASE_DIR / "webpack-stats.json",
"POLL_INTERVAL": 0.1, "POLL_INTERVAL": 0.1,
"IGNORE": [r".+\.hot-update.js", r".+\.map"], "IGNORE": [r".+\.hot-update.js", r".+\.map"],
} },
} }
{%- endif %} {%- endif %}

View File

@ -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 from .base import env
# GENERAL # GENERAL
@ -11,7 +17,7 @@ SECRET_KEY = env(
default="!!!SET DJANGO_SECRET_KEY!!!", default="!!!SET DJANGO_SECRET_KEY!!!",
) )
# https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts # 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 # CACHES
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
@ -20,7 +26,7 @@ CACHES = {
"default": { "default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache", "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
"LOCATION": "", "LOCATION": "",
} },
} }
# EMAIL # EMAIL
@ -37,7 +43,9 @@ EMAIL_HOST = "localhost"
EMAIL_PORT = 1025 EMAIL_PORT = 1025
{%- else -%} {%- else -%}
# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend # 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 %} {%- endif %}
{%- if cookiecutter.use_whitenoise == 'y' %} {%- if cookiecutter.use_whitenoise == 'y' %}
@ -45,15 +53,15 @@ EMAIL_BACKEND = env("DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.c
# WhiteNoise # WhiteNoise
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# http://whitenoise.evans.io/en/latest/django.html#using-whitenoise-in-development # 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 %} {% endif %}
# django-debug-toolbar # django-debug-toolbar
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#prerequisites # https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#prerequisites
INSTALLED_APPS += ["debug_toolbar"] # noqa: F405 INSTALLED_APPS += ["debug_toolbar"]
# https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#middleware # https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#middleware
MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"] # noqa: F405 MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"]
# https://django-debug-toolbar.readthedocs.io/en/latest/configuration.html#debug-toolbar-config # https://django-debug-toolbar.readthedocs.io/en/latest/configuration.html#debug-toolbar-config
DEBUG_TOOLBAR_CONFIG = { DEBUG_TOOLBAR_CONFIG = {
"DISABLE_PANELS": ["debug_toolbar.panels.redirects.RedirectsPanel"], "DISABLE_PANELS": ["debug_toolbar.panels.redirects.RedirectsPanel"],
@ -80,7 +88,7 @@ if env("USE_DOCKER") == "yes":
# django-extensions # django-extensions
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# https://django-extensions.readthedocs.io/en/latest/installation_instructions.html#configuration # 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' -%} {% if cookiecutter.use_celery == 'y' -%}
# Celery # Celery
@ -96,7 +104,7 @@ CELERY_TASK_EAGER_PROPAGATES = True
{%- if cookiecutter.frontend_pipeline == 'Webpack' %} {%- if cookiecutter.frontend_pipeline == 'Webpack' %}
# django-webpack-loader # django-webpack-loader
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
WEBPACK_LOADER["DEFAULT"]["CACHE"] = not DEBUG # noqa: F405 WEBPACK_LOADER["DEFAULT"]["CACHE"] = not DEBUG
{%- endif %} {%- endif %}
# Your stuff... # Your stuff...

View File

@ -1,3 +1,4 @@
# ruff: noqa: E501
{% if cookiecutter.use_sentry == 'y' -%} {% if cookiecutter.use_sentry == 'y' -%}
import logging import logging
@ -12,7 +13,12 @@ from sentry_sdk.integrations.logging import LoggingIntegration
from sentry_sdk.integrations.redis import RedisIntegration from sentry_sdk.integrations.redis import RedisIntegration
{% endif -%} {% 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 from .base import env
# GENERAL # GENERAL
@ -24,7 +30,7 @@ ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["{{ cookiecutter.domai
# DATABASES # 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 # CACHES
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
@ -38,7 +44,7 @@ CACHES = {
# https://github.com/jazzband/django-redis#memcached-exceptions-behavior # https://github.com/jazzband/django-redis#memcached-exceptions-behavior
"IGNORE_EXCEPTIONS": True, "IGNORE_EXCEPTIONS": True,
}, },
} },
} }
# SECURITY # 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 # TODO: set this to 60 seconds first and then to 518400 once you prove the former works
SECURE_HSTS_SECONDS = 60 SECURE_HSTS_SECONDS = 60
# https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-include-subdomains # 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 # https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-preload
SECURE_HSTS_PRELOAD = env.bool("DJANGO_SECURE_HSTS_PRELOAD", default=True) SECURE_HSTS_PRELOAD = env.bool("DJANGO_SECURE_HSTS_PRELOAD", default=True)
# https://docs.djangoproject.com/en/dev/ref/middleware/#x-content-type-options-nosniff # 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' -%} {% if cookiecutter.cloud_provider != 'None' -%}
# STORAGES # STORAGES
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# https://django-storages.readthedocs.io/en/latest/#installation # https://django-storages.readthedocs.io/en/latest/#installation
INSTALLED_APPS += ["storages"] # noqa: F405 INSTALLED_APPS += ["storages"]
{%- endif -%} {%- endif -%}
{% if cookiecutter.cloud_provider == 'AWS' %} {% if cookiecutter.cloud_provider == 'AWS' %}
# https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings
@ -103,35 +115,75 @@ AZURE_CONTAINER = env("DJANGO_AZURE_CONTAINER_NAME")
{% endif -%} {% endif -%}
{% if cookiecutter.cloud_provider != 'None' or cookiecutter.use_whitenoise == 'y' -%} {% if cookiecutter.cloud_provider != 'None' or cookiecutter.use_whitenoise == 'y' -%}
# STATIC # STATIC & MEDIA
# ------------------------ # ------------------------
{% endif -%} STORAGES = {
{% if cookiecutter.use_whitenoise == 'y' -%} {%- if cookiecutter.use_whitenoise == 'y' %}
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" "default": {
{% elif cookiecutter.cloud_provider == 'AWS' -%} "BACKEND": "django.core.files.storage.FileSystemStorage",
STATICFILES_STORAGE = "{{cookiecutter.project_slug}}.utils.storages.StaticS3Storage" },
"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" COLLECTFAST_STRATEGY = "collectfast.strategies.boto3.Boto3Strategy"
STATIC_URL = f"https://{aws_s3_domain}/static/" STATIC_URL = f"https://{aws_s3_domain}/static/"
{% elif cookiecutter.cloud_provider == 'GCP' -%} {%- elif cookiecutter.cloud_provider == 'GCP' %}
STATICFILES_STORAGE = "{{cookiecutter.project_slug}}.utils.storages.StaticGoogleCloudStorage" MEDIA_URL = f"https://storage.googleapis.com/{GS_BUCKET_NAME}/media/"
COLLECTFAST_STRATEGY = "collectfast.strategies.gcloud.GoogleCloudStrategy" COLLECTFAST_STRATEGY = "collectfast.strategies.gcloud.GoogleCloudStrategy"
STATIC_URL = f"https://storage.googleapis.com/{GS_BUCKET_NAME}/static/" 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' %} {%- 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/" 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 %} {%- endif %}
# EMAIL # EMAIL
@ -157,7 +209,7 @@ ADMIN_URL = env("DJANGO_ADMIN_URL")
# Anymail # Anymail
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# https://anymail.readthedocs.io/en/stable/installation/#installing-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://docs.djangoproject.com/en/dev/ref/settings/#email-backend
# https://anymail.readthedocs.io/en/stable/installation/#anymail-settings-reference # https://anymail.readthedocs.io/en/stable/installation/#anymail-settings-reference
{%- if cookiecutter.mail_service == 'Mailgun' %} {%- if cookiecutter.mail_service == 'Mailgun' %}
@ -230,10 +282,10 @@ COMPRESS_ENABLED = env.bool("COMPRESS_ENABLED", default=True)
COMPRESS_STORAGE = "compressor.storage.GzipCompressorFileStorage" COMPRESS_STORAGE = "compressor.storage.GzipCompressorFileStorage"
{%- elif cookiecutter.cloud_provider in ('AWS', 'GCP', 'Azure') and cookiecutter.use_whitenoise == 'n' %} {%- 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 # https://django-compressor.readthedocs.io/en/latest/settings/#django.conf.settings.COMPRESS_STORAGE
COMPRESS_STORAGE = STATICFILES_STORAGE COMPRESS_STORAGE = STORAGES["staticfiles"]["BACKEND"]
{%- endif %} {%- endif %}
# https://django-compressor.readthedocs.io/en/latest/settings/#django.conf.settings.COMPRESS_URL # 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' %} {%- if cookiecutter.use_whitenoise == 'y' %}
# https://django-compressor.readthedocs.io/en/latest/settings/#django.conf.settings.COMPRESS_OFFLINE # https://django-compressor.readthedocs.io/en/latest/settings/#django.conf.settings.COMPRESS_OFFLINE
COMPRESS_OFFLINE = True # Offline compression is required when using Whitenoise COMPRESS_OFFLINE = True # Offline compression is required when using Whitenoise
@ -251,7 +303,7 @@ COMPRESS_FILTERS = {
# Collectfast # Collectfast
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# https://github.com/antonagestam/collectfast#installation # https://github.com/antonagestam/collectfast#installation
INSTALLED_APPS = ["collectfast"] + INSTALLED_APPS # noqa: F405 INSTALLED_APPS = ["collectfast", *INSTALLED_APPS]
{% endif %} {% endif %}
# LOGGING # LOGGING
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
@ -311,7 +363,7 @@ LOGGING = {
"level": "DEBUG", "level": "DEBUG",
"class": "logging.StreamHandler", "class": "logging.StreamHandler",
"formatter": "verbose", "formatter": "verbose",
} },
}, },
"root": {"level": "INFO", "handlers": ["console"]}, "root": {"level": "INFO", "handlers": ["console"]},
"loggers": { "loggers": {
@ -363,7 +415,7 @@ sentry_sdk.init(
# django-rest-framework # django-rest-framework
# ------------------------------------------------------------------------------- # -------------------------------------------------------------------------------
# Tools that generate code samples can use SERVERS to point to the correct domain # 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"}, {"url": "https://{{ cookiecutter.domain_name }}", "description": "Production server"},
] ]

View File

@ -2,7 +2,8 @@
With these settings, tests run faster. With these settings, tests run faster.
""" """
from .base import * # noqa from .base import * # noqa: F403
from .base import TEMPLATES
from .base import env from .base import env
# GENERAL # GENERAL
@ -27,17 +28,17 @@ EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
# DEBUGGING FOR TEMPLATES # DEBUGGING FOR TEMPLATES
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
TEMPLATES[0]["OPTIONS"]["debug"] = True # type: ignore # noqa: F405 TEMPLATES[0]["OPTIONS"]["debug"] = True # type: ignore[index]
# MEDIA # MEDIA
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#media-url # https://docs.djangoproject.com/en/dev/ref/settings/#media-url
MEDIA_URL = 'http://media.testserver' MEDIA_URL = "http://media.testserver"
{%- if cookiecutter.frontend_pipeline == 'Webpack' %} {%- if cookiecutter.frontend_pipeline == 'Webpack' %}
# django-webpack-loader # 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 %} {%- endif %}
# Your stuff... # Your stuff...

View File

@ -1,27 +1,37 @@
# ruff: noqa
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static from django.conf.urls.static import static
from django.contrib import admin from django.contrib import admin
{%- if cookiecutter.use_async == 'y' %} {%- if cookiecutter.use_async == 'y' %}
from django.contrib.staticfiles.urls import staticfiles_urlpatterns from django.contrib.staticfiles.urls import staticfiles_urlpatterns
{%- endif %} {%- 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 import defaults as default_views
from django.views.generic import TemplateView from django.views.generic import TemplateView
{%- if cookiecutter.use_drf == 'y' %} {%- 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 from rest_framework.authtoken.views import obtain_auth_token
{%- endif %} {%- endif %}
urlpatterns = [ urlpatterns = [
path("", TemplateView.as_view(template_name="pages/home.html"), name="home"), 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 %} # Django Admin, use {% raw %}{% url 'admin:index' %}{% endraw %}
path(settings.ADMIN_URL, admin.site.urls), path(settings.ADMIN_URL, admin.site.urls),
# User management # User management
path("users/", include("{{ cookiecutter.project_slug }}.users.urls", namespace="users")), path("users/", include("{{ cookiecutter.project_slug }}.users.urls", namespace="users")),
path("accounts/", include("allauth.urls")), path("accounts/", include("allauth.urls")),
# Your stuff: custom urls includes go here # 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 cookiecutter.use_async == 'y' %}
if settings.DEBUG: if settings.DEBUG:
# Static file serving when using Gunicorn + Uvicorn for local web socket development # Static file serving when using Gunicorn + Uvicorn for local web socket development

View File

@ -1,3 +1,4 @@
# ruff: noqa
""" """
WSGI config for {{ cookiecutter.project_name }} project. WSGI config for {{ cookiecutter.project_name }} project.
@ -13,6 +14,7 @@ middleware here, or combine a Django application with an application of another
framework. framework.
""" """
import os import os
import sys import sys
from pathlib import Path from pathlib import Path

View File

@ -1,3 +1,4 @@
# ruff: noqa
# Configuration file for the Sphinx documentation builder. # Configuration file for the Sphinx documentation builder.
# #
# This file only contains a selection of the most common options. For a full # This file only contains a selection of the most common options. For a full

View File

@ -58,7 +58,7 @@ services:
{%- if cookiecutter.use_mailpit == 'y' %} {%- if cookiecutter.use_mailpit == 'y' %}
mailpit: mailpit:
image: axllent/mailpit:latest image: docker.io/axllent/mailpit:latest
container_name: {{ cookiecutter.project_slug }}_local_mailpit container_name: {{ cookiecutter.project_slug }}_local_mailpit
ports: ports:
- "8025:8025" - "8025:8025"
@ -67,7 +67,7 @@ services:
{%- if cookiecutter.use_celery == 'y' %} {%- if cookiecutter.use_celery == 'y' %}
redis: redis:
image: redis:6 image: docker.io/redis:6
container_name: {{ cookiecutter.project_slug }}_local_redis container_name: {{ cookiecutter.project_slug }}_local_redis
celeryworker: celeryworker:

View File

@ -1,4 +1,5 @@
#!/usr/bin/env python #!/usr/bin/env python
# ruff: noqa
import os import os
import sys import sys
from pathlib import Path from pathlib import Path
@ -13,7 +14,7 @@ if __name__ == "__main__":
# issue is really that Django is missing to avoid masking other # issue is really that Django is missing to avoid masking other
# exceptions on Python 2. # exceptions on Python 2.
try: try:
import django # noqa import django
except ImportError: except ImportError:
raise ImportError( raise ImportError(
"Couldn't import Django. Are you sure it's installed and " "Couldn't import Django. Are you sure it's installed and "

View File

@ -1,3 +1,4 @@
# ruff: noqa
import os import os
from collections.abc import Sequence from collections.abc import Sequence
from pathlib import Path from pathlib import Path

View File

@ -8,7 +8,7 @@
"autoprefixer": "^10.4.0", "autoprefixer": "^10.4.0",
"babel-loader": "^9.1.2", "babel-loader": "^9.1.2",
"bootstrap": "^5.2.3", "bootstrap": "^5.2.3",
"browser-sync": "^2.27.7", "browser-sync": "^3.0.2",
"css-loader": "^6.5.1", "css-loader": "^6.5.1",
"gulp-concat": "^2.6.1", "gulp-concat": "^2.6.1",
"concurrently": "^8.0.1", "concurrently": "^8.0.1",
@ -16,7 +16,7 @@
"gulp": "^4.0.2", "gulp": "^4.0.2",
"gulp-imagemin": "^7.1.0", "gulp-imagemin": "^7.1.0",
"gulp-plumber": "^1.2.1", "gulp-plumber": "^1.2.1",
"gulp-postcss": "^9.0.1", "gulp-postcss": "^10.0.0",
"gulp-rename": "^2.0.0", "gulp-rename": "^2.0.0",
"gulp-sass": "^5.0.0", "gulp-sass": "^5.0.0",
"gulp-uglify-es": "^3.0.0", "gulp-uglify-es": "^3.0.0",
@ -24,12 +24,12 @@
"node-sass-tilde-importer": "^1.0.2", "node-sass-tilde-importer": "^1.0.2",
"pixrem": "^5.0.0", "pixrem": "^5.0.0",
"postcss": "^8.3.11", "postcss": "^8.3.11",
"postcss-loader": "^7.0.2", "postcss-loader": "^8.0.0",
"postcss-preset-env": "^9.0.0", "postcss-preset-env": "^9.0.0",
"sass": "^1.43.4", "sass": "^1.43.4",
"sass-loader": "^13.2.0", "sass-loader": "^14.0.0",
"webpack": "^5.65.0", "webpack": "^5.65.0",
"webpack-bundle-tracker": "^2.0.0", "webpack-bundle-tracker": "^3.0.1",
"webpack-cli": "^5.0.1", "webpack-cli": "^5.0.1",
"webpack-dev-server": "^4.6.0", "webpack-dev-server": "^4.6.0",
"webpack-merge": "^5.8.0" "webpack-merge": "^5.8.0"

View File

@ -67,7 +67,7 @@ services:
{%- endif %} {%- endif %}
redis: redis:
image: redis:6 image: docker.io/redis:6
{%- if cookiecutter.use_celery == 'y' %} {%- if cookiecutter.use_celery == 'y' %}
celeryworker: celeryworker:

View File

@ -16,25 +16,6 @@ include = ["{{cookiecutter.project_slug}}/**"]
omit = ["*/migrations/*", "*/tests/*"] omit = ["*/migrations/*", "*/tests/*"]
plugins = ["django_coverage_plugin"] 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 ==== # ==== mypy ====
[tool.mypy] [tool.mypy]
python_version = "3.11" python_version = "3.11"
@ -58,40 +39,6 @@ ignore_errors = true
[tool.django-stubs] [tool.django-stubs]
django_settings_module = "config.settings.test" 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 ==== # ==== djLint ====
[tool.djlint] [tool.djlint]
blank_line_after_tag = "load,extends" blank_line_after_tag = "load,extends"
@ -110,3 +57,112 @@ indent_size = 2
[tool.djlint.js] [tool.djlint.js]
indent_size = 2 indent_size = 2
[tool.ruff]
# Exclude a variety of commonly ignored directories.
exclude = [
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".mypy_cache",
".nox",
".pants.d",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"venv",
"*/migrations/*.py",
"staticfiles/*"
]
# Same as Django: https://github.com/cookiecutter/cookiecutter-django/issues/4792.
line-length = 88
indent-width = 4
target-version = "py311"
[tool.ruff.lint]
select = [
"F",
"E",
"W",
"C90",
"I",
"N",
"UP",
"YTT",
# "ANN", # flake8-annotations: we should support this in the future but 100+ errors atm
"ASYNC",
"S",
"BLE",
"FBT",
"B",
"A",
"COM",
"C4",
"DTZ",
"T10",
"DJ",
"EM",
"EXE",
"FA",
'ISC',
"ICN",
"G",
'INP',
'PIE',
"T20",
'PYI',
'PT',
"Q",
"RSE",
"RET",
"SLF",
"SLOT",
"SIM",
"TID",
"TCH",
"INT",
# "ARG", # Unused function argument
"PTH",
"ERA",
"PD",
"PGH",
"PL",
"TRY",
"FLY",
# "NPY",
# "AIR",
"PERF",
# "FURB",
# "LOG",
"RUF"
]
ignore = [
"S101", # Use of assert detected https://docs.astral.sh/ruff/rules/assert/
"RUF012", # Mutable class attributes should be annotated with `typing.ClassVar`
"SIM102" # sometimes it's better to nest
]
# Allow fix for all enabled rules (when `--fix`) is provided.
fixable = ["ALL"]
unfixable = []
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
line-ending = "auto"
[tool.ruff.lint.isort]
force-single-line = true

View File

@ -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 Pillow==10.2.0 # https://github.com/python-pillow/Pillow
{%- if cookiecutter.frontend_pipeline == 'Django Compressor' %} {%- if cookiecutter.frontend_pipeline == 'Django Compressor' %}
{%- if cookiecutter.windows == 'y' and cookiecutter.use_docker == 'n' %} {%- if cookiecutter.windows == 'y' and cookiecutter.use_docker == 'n' %}
@ -23,15 +23,15 @@ flower==2.0.1 # https://github.com/mher/flower
{%- endif %} {%- endif %}
{%- endif %} {%- endif %}
{%- if cookiecutter.use_async == 'y' %} {%- if cookiecutter.use_async == 'y' %}
uvicorn[standard]==0.25.0 # https://github.com/encode/uvicorn uvicorn[standard]==0.27.1 # https://github.com/encode/uvicorn
{%- endif %} {%- endif %}
# Django # Django
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
django==4.2.9 # pyup: < 5.0 # https://www.djangoproject.com/ django==4.2.10 # pyup: < 5.0 # https://www.djangoproject.com/
django-environ==0.11.2 # https://github.com/joke2k/django-environ django-environ==0.11.2 # https://github.com/joke2k/django-environ
django-model-utils==4.3.1 # https://github.com/jazzband/django-model-utils django-model-utils==4.4.0 # https://github.com/jazzband/django-model-utils
django-allauth==0.60.0 # https://github.com/pennersr/django-allauth django-allauth==0.61.1 # https://github.com/pennersr/django-allauth
django-crispy-forms==2.1 # https://github.com/django-crispy-forms/django-crispy-forms 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==2023.10 # https://github.com/django-crispy-forms/crispy-bootstrap5
{%- if cookiecutter.frontend_pipeline == 'Django Compressor' %} {%- if cookiecutter.frontend_pipeline == 'Django Compressor' %}
@ -43,8 +43,8 @@ django-redis==5.4.0 # https://github.com/jazzband/django-redis
djangorestframework==3.14.0 # https://github.com/encode/django-rest-framework djangorestframework==3.14.0 # https://github.com/encode/django-rest-framework
django-cors-headers==4.3.1 # https://github.com/adamchainz/django-cors-headers django-cors-headers==4.3.1 # https://github.com/adamchainz/django-cors-headers
# DRF-spectacular for api documentation # DRF-spectacular for api documentation
drf-spectacular==0.27.0 # https://github.com/tfranzel/drf-spectacular drf-spectacular==0.27.1 # https://github.com/tfranzel/drf-spectacular
{%- endif %} {%- endif %}
{%- if cookiecutter.frontend_pipeline == 'Webpack' %} {%- if cookiecutter.frontend_pipeline == 'Webpack' %}
django-webpack-loader==2.0.1 # https://github.com/django-webpack/django-webpack-loader django-webpack-loader==3.0.1 # https://github.com/django-webpack/django-webpack-loader
{%- endif %} {%- endif %}

View File

@ -1,11 +1,11 @@
-r base.txt -r production.txt
Werkzeug[watchdog]==3.0.1 # https://github.com/pallets/werkzeug Werkzeug[watchdog]==3.0.1 # https://github.com/pallets/werkzeug
ipdb==0.13.13 # https://github.com/gotcha/ipdb ipdb==0.13.13 # https://github.com/gotcha/ipdb
{%- if cookiecutter.use_docker == 'y' %} {%- 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 %} {%- else %}
psycopg[binary]==3.1.17 # https://github.com/psycopg/psycopg psycopg[binary]==3.1.18 # https://github.com/psycopg/psycopg
{%- endif %} {%- endif %}
{%- if cookiecutter.use_async == 'y' or cookiecutter.use_celery == 'y' %} {%- if cookiecutter.use_async == 'y' or cookiecutter.use_celery == 'y' %}
watchfiles==0.21.0 # https://github.com/samuelcolvin/watchfiles 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 mypy==1.7.1 # https://github.com/python/mypy
django-stubs[compatible-mypy]==4.2.7 # https://github.com/typeddjango/django-stubs django-stubs[compatible-mypy]==4.2.7 # https://github.com/typeddjango/django-stubs
pytest==7.4.4 # https://github.com/pytest-dev/pytest pytest==8.0.1 # https://github.com/pytest-dev/pytest
pytest-sugar==0.9.7 # https://github.com/Frozenball/pytest-sugar pytest-sugar==1.0.0 # https://github.com/Frozenball/pytest-sugar
{%- if cookiecutter.use_drf == "y" %} {%- if cookiecutter.use_drf == "y" %}
djangorestframework-stubs[compatible-mypy]==3.14.5 # https://github.com/typeddjango/djangorestframework-stubs djangorestframework-stubs[compatible-mypy]==3.14.5 # https://github.com/typeddjango/djangorestframework-stubs
{%- endif %} {%- endif %}
@ -24,26 +24,20 @@ djangorestframework-stubs[compatible-mypy]==3.14.5 # https://github.com/typeddj
# Documentation # Documentation
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
sphinx==7.2.6 # https://github.com/sphinx-doc/sphinx sphinx==7.2.6 # https://github.com/sphinx-doc/sphinx
sphinx-autobuild==2021.3.14 # https://github.com/GaretJax/sphinx-autobuild sphinx-autobuild==2024.2.4 # https://github.com/GaretJax/sphinx-autobuild
# Code quality # Code quality
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
flake8==7.0.0 # https://github.com/PyCQA/flake8 ruff==0.2.1 # https://github.com/astral-sh/ruff
flake8-isort==6.1.1 # https://github.com/gforcada/flake8-isort coverage==7.4.1 # https://github.com/nedbat/coveragepy
coverage==7.4.0 # https://github.com/nedbat/coveragepy
black==23.12.1 # https://github.com/psf/black
djlint==1.34.1 # https://github.com/Riverside-Healthcare/djLint djlint==1.34.1 # https://github.com/Riverside-Healthcare/djLint
pylint-django==2.5.5 # https://github.com/PyCQA/pylint-django pre-commit==3.6.1 # https://github.com/pre-commit/pre-commit
{%- 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
# Django # Django
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
factory-boy==3.3.0 # https://github.com/FactoryBoy/factory_boy 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-extensions==3.2.3 # https://github.com/django-extensions/django-extensions
django-coverage-plugin==3.1.0 # https://github.com/nedbat/django_coverage_plugin 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

View File

@ -3,12 +3,12 @@
-r base.txt -r base.txt
gunicorn==21.2.0 # https://github.com/benoitc/gunicorn 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' %} {%- if cookiecutter.use_whitenoise == 'n' %}
Collectfast==2.2.0 # https://github.com/antonagestam/collectfast Collectfast==2.2.0 # https://github.com/antonagestam/collectfast
{%- endif %} {%- endif %}
{%- if cookiecutter.use_sentry == "y" %} {%- if cookiecutter.use_sentry == "y" %}
sentry-sdk==1.39.2 # https://github.com/getsentry/sentry-python sentry-sdk==1.40.5 # https://github.com/getsentry/sentry-python
{%- endif %} {%- endif %}
{%- if cookiecutter.use_docker == "n" and cookiecutter.windows == "y" %} {%- if cookiecutter.use_docker == "n" and cookiecutter.windows == "y" %}
hiredis==2.3.2 # https://github.com/redis/hiredis-py hiredis==2.3.2 # https://github.com/redis/hiredis-py

View File

@ -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

View File

@ -13,6 +13,13 @@ module.exports = merge(commonConfig, {
'/': 'http://django:8000', '/': 'http://django:8000',
{%- endif %} {%- endif %}
}, },
client: {
overlay: {
errors: true,
warnings: false,
runtimeErrors: true,
},
},
// We need hot=false (Disable HMR) to set liveReload=true // We need hot=false (Disable HMR) to set liveReload=true
hot: false, hot: false,
liveReload: true, liveReload: true,

View File

@ -1,2 +1,5 @@
__version__ = "{{ cookiecutter.version }}" __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(".")
)

View File

@ -5,10 +5,10 @@ from {{ cookiecutter.project_slug }}.users.tests.factories import UserFactory
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def media_storage(settings, tmpdir): def _media_storage(settings, tmpdir) -> None:
settings.MEDIA_ROOT = tmpdir.strpath settings.MEDIA_ROOT = tmpdir.strpath
@pytest.fixture @pytest.fixture()
def user(db) -> User: def user(db) -> User:
return UserFactory() return UserFactory()

View File

@ -1,6 +1,7 @@
import django.contrib.sites.models import django.contrib.sites.models
from django.contrib.sites.models import _simple_domain_name_validator 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): class Migration(migrations.Migration):
@ -38,5 +39,5 @@ class Migration(migrations.Migration):
}, },
bases=(models.Model,), bases=(models.Model,),
managers=[("objects", django.contrib.sites.models.SiteManager())], managers=[("objects", django.contrib.sites.models.SiteManager())],
) ),
] ]

View File

@ -1,5 +1,6 @@
import django.contrib.sites.models 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): class Migration(migrations.Migration):

View File

@ -23,7 +23,7 @@ def _update_or_create_site_with_sequence(site_model, connection, domain, name):
# site is created. # site is created.
# To avoid this, we need to manually update DB sequence and make sure it's # To avoid this, we need to manually update DB sequence and make sure it's
# greater than the maximum value. # 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: with connection.cursor() as cursor:
cursor.execute("SELECT last_value from django_site_id_seq") cursor.execute("SELECT last_value from django_site_id_seq")
(current_id,) = cursor.fetchone() (current_id,) = cursor.fetchone()

View File

@ -5,10 +5,11 @@ import typing
from allauth.account.adapter import DefaultAccountAdapter from allauth.account.adapter import DefaultAccountAdapter
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
from django.conf import settings from django.conf import settings
from django.http import HttpRequest
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from allauth.socialaccount.models import SocialLogin from allauth.socialaccount.models import SocialLogin
from django.http import HttpRequest
from {{cookiecutter.project_slug}}.users.models import User from {{cookiecutter.project_slug}}.users.models import User
@ -18,10 +19,19 @@ class AccountAdapter(DefaultAccountAdapter):
class SocialAccountAdapter(DefaultSocialAccountAdapter): 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) return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True)
def populate_user(self, request: HttpRequest, sociallogin: SocialLogin, data: dict[str, typing.Any]) -> User: def populate_user(
self,
request: HttpRequest,
sociallogin: SocialLogin,
data: dict[str, typing.Any],
) -> User:
""" """
Populates user information from social provider info. Populates user information from social provider info.

View File

@ -7,6 +7,7 @@ from django.utils.translation import gettext_lazy as _
from {{ cookiecutter.project_slug }}.users.forms import UserAdminChangeForm, UserAdminCreationForm from {{ cookiecutter.project_slug }}.users.forms import UserAdminChangeForm, UserAdminCreationForm
from {{ cookiecutter.project_slug }}.users.models import User from {{ cookiecutter.project_slug }}.users.models import User
if settings.DJANGO_ADMIN_FORCE_ALLAUTH: if settings.DJANGO_ADMIN_FORCE_ALLAUTH:
# Force the `admin` sign in process to go through the `django-allauth` workflow: # Force the `admin` sign in process to go through the `django-allauth` workflow:
# https://docs.allauth.org/en/latest/common/admin.html#admin # https://docs.allauth.org/en/latest/common/admin.html#admin

View File

@ -1,6 +1,8 @@
from rest_framework import status from rest_framework import status
from rest_framework.decorators import action 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.response import Response
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet

View File

@ -1,3 +1,5 @@
import contextlib
from django.apps import AppConfig from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -7,7 +9,5 @@ class UsersConfig(AppConfig):
verbose_name = _("Users") verbose_name = _("Users")
def ready(self): def ready(self):
try: with contextlib.suppress(ImportError):
import {{ cookiecutter.project_slug }}.users.signals # noqa: F401 import {{ cookiecutter.project_slug }}.users.signals # noqa: F401
except ImportError:
pass

View File

@ -1,8 +1,13 @@
from typing import TYPE_CHECKING
from django.contrib.auth.hashers import make_password from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import UserManager as DjangoUserManager from django.contrib.auth.models import UserManager as DjangoUserManager
if TYPE_CHECKING:
from {{ cookiecutter.project_slug }}.users.models import User # noqa: F401
class UserManager(DjangoUserManager):
class UserManager(DjangoUserManager["User"]):
"""Custom manager for the User model.""" """Custom manager for the User model."""
def _create_user(self, email: str, password: str | None, **extra_fields): 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. Create and save a user with the given email and password.
""" """
if not email: 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) email = self.normalize_email(email)
user = self.model(email=email, **extra_fields) user = self.model(email=email, **extra_fields)
user.password = make_password(password) user.password = make_password(password)
user.save(using=self._db) user.save(using=self._db)
return user 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_staff", False)
extra_fields.setdefault("is_superuser", False) extra_fields.setdefault("is_superuser", False)
return self._create_user(email, password, **extra_fields) 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_staff", True)
extra_fields.setdefault("is_superuser", True) extra_fields.setdefault("is_superuser", True)
if extra_fields.get("is_staff") is not 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: 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) return self._create_user(email, password, **extra_fields)

View File

@ -1,7 +1,8 @@
import django.contrib.auth.models import django.contrib.auth.models
import django.contrib.auth.validators import django.contrib.auth.validators
from django.db import migrations, models
import django.utils.timezone import django.utils.timezone
from django.db import migrations
from django.db import models
import {{cookiecutter.project_slug}}.users.models import {{cookiecutter.project_slug}}.users.models
@ -31,7 +32,7 @@ class Migration(migrations.Migration):
( (
"last_login", "last_login",
models.DateTimeField( models.DateTimeField(
blank=True, null=True, verbose_name="last login" blank=True, null=True, verbose_name="last login",
), ),
), ),
( (
@ -61,14 +62,14 @@ class Migration(migrations.Migration):
( (
"email", "email",
models.EmailField( models.EmailField(
blank=True, max_length=254, verbose_name="email address" blank=True, max_length=254, verbose_name="email address",
), ),
), ),
{%- else %} {%- else %}
( (
"email", "email",
models.EmailField( models.EmailField(
unique=True, max_length=254, verbose_name="email address" unique=True, max_length=254, verbose_name="email address",
), ),
), ),
{%- endif %} {%- endif %}
@ -91,13 +92,13 @@ class Migration(migrations.Migration):
( (
"date_joined", "date_joined",
models.DateTimeField( models.DateTimeField(
default=django.utils.timezone.now, verbose_name="date joined" default=django.utils.timezone.now, verbose_name="date joined",
), ),
), ),
( (
"name", "name",
models.CharField( models.CharField(
blank=True, max_length=255, verbose_name="Name of User" blank=True, max_length=255, verbose_name="Name of User",
), ),
), ),
( (

View File

@ -1,5 +1,12 @@
{%- if cookiecutter.username_type == "email" %}
from typing import ClassVar
{% endif -%}
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.db.models import CharField{% if cookiecutter.username_type == "email" %}, EmailField{% endif %} from django.db.models import CharField
{%- if cookiecutter.username_type == "email" %}
from django.db.models import EmailField
{%- endif %}
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
{%- if cookiecutter.username_type == "email" %} {%- if cookiecutter.username_type == "email" %}
@ -17,16 +24,16 @@ class User(AbstractUser):
# First and last name do not cover name patterns around the globe # First and last name do not cover name patterns around the globe
name = CharField(_("Name of User"), blank=True, max_length=255) name = CharField(_("Name of User"), blank=True, max_length=255)
first_name = None # type: ignore first_name = None # type: ignore[assignment]
last_name = None # type: ignore last_name = None # type: ignore[assignment]
{%- if cookiecutter.username_type == "email" %} {%- if cookiecutter.username_type == "email" %}
email = EmailField(_("email address"), unique=True) email = EmailField(_("email address"), unique=True)
username = None # type: ignore username = None # type: ignore[assignment]
USERNAME_FIELD = "email" USERNAME_FIELD = "email"
REQUIRED_FIELDS = [] REQUIRED_FIELDS = []
objects = UserManager() objects: ClassVar[UserManager] = UserManager()
{%- endif %} {%- endif %}
def get_absolute_url(self) -> str: def get_absolute_url(self) -> str:

View File

@ -1,7 +1,8 @@
from collections.abc import Sequence from collections.abc import Sequence
from typing import Any from typing import Any
from factory import Faker, post_generation from factory import Faker
from factory import post_generation
from factory.django import DjangoModelFactory from factory.django import DjangoModelFactory
from {{ cookiecutter.project_slug }}.users.models import User from {{ cookiecutter.project_slug }}.users.models import User
@ -15,7 +16,7 @@ class UserFactory(DjangoModelFactory):
name = Faker("name") name = Faker("name")
@post_generation @post_generation
def password(self, create: bool, extracted: Sequence[Any], **kwargs): def password(self, create: bool, extracted: Sequence[Any], **kwargs): # noqa: FBT001
password = ( password = (
extracted extracted
if extracted if extracted

View File

@ -1,4 +1,12 @@
import contextlib
from http import HTTPStatus
from importlib import reload
import pytest
from django.contrib import admin
from django.contrib.auth.models import AnonymousUser
from django.urls import reverse from django.urls import reverse
from pytest_django.asserts import assertRedirects
from {{ cookiecutter.project_slug }}.users.models import User from {{ cookiecutter.project_slug }}.users.models import User
@ -7,17 +15,17 @@ class TestUserAdmin:
def test_changelist(self, admin_client): def test_changelist(self, admin_client):
url = reverse("admin:users_user_changelist") url = reverse("admin:users_user_changelist")
response = admin_client.get(url) response = admin_client.get(url)
assert response.status_code == 200 assert response.status_code == HTTPStatus.OK
def test_search(self, admin_client): def test_search(self, admin_client):
url = reverse("admin:users_user_changelist") url = reverse("admin:users_user_changelist")
response = admin_client.get(url, data={"q": "test"}) 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): def test_add(self, admin_client):
url = reverse("admin:users_user_add") url = reverse("admin:users_user_add")
response = admin_client.get(url) response = admin_client.get(url)
assert response.status_code == 200 assert response.status_code == HTTPStatus.OK
response = admin_client.post( response = admin_client.post(
url, url,
@ -31,7 +39,7 @@ class TestUserAdmin:
"password2": "My_R@ndom-P@ssw0rd", "password2": "My_R@ndom-P@ssw0rd",
}, },
) )
assert response.status_code == 302 assert response.status_code == HTTPStatus.FOUND
{%- if cookiecutter.username_type == "email" %} {%- if cookiecutter.username_type == "email" %}
assert User.objects.filter(email="new-admin@example.com").exists() assert User.objects.filter(email="new-admin@example.com").exists()
{%- else %} {%- else %}
@ -46,4 +54,24 @@ class TestUserAdmin:
{%- endif %} {%- endif %}
url = reverse("admin:users_user_change", kwargs={"object_id": user.pk}) url = reverse("admin:users_user_change", kwargs={"object_id": user.pk})
response = admin_client.get(url) response = admin_client.get(url)
assert response.status_code == 200 assert response.status_code == HTTPStatus.OK
@pytest.fixture()
def _force_allauth(self, settings):
settings.DJANGO_ADMIN_FORCE_ALLAUTH = True
# Reload the admin module to apply the setting change
import {{ cookiecutter.project_slug }}.users.admin as users_admin
with contextlib.suppress(admin.sites.AlreadyRegistered):
reload(users_admin)
@pytest.mark.django_db()
@pytest.mark.usefixtures("_force_allauth")
def test_allauth_login(self, rf, settings):
request = rf.get("/fake-url")
request.user = AnonymousUser()
response = admin.site.login(request)
# The `admin` login view should redirect to the `allauth` login view
target_url = reverse(settings.LOGIN_URL) + "?next=" + request.path
assertRedirects(response, target_url, fetch_redirect_response=False)

View File

@ -1,14 +1,20 @@
from django.urls import resolve, reverse from django.urls import resolve
from django.urls import reverse
from {{ cookiecutter.project_slug }}.users.models import User from {{ cookiecutter.project_slug }}.users.models import User
def test_user_detail(user: User): def test_user_detail(user: User):
{%- if cookiecutter.username_type == "email" %} {%- if cookiecutter.username_type == "email" %}
assert reverse("api:user-detail", kwargs={"pk": user.pk}) == f"/api/users/{user.pk}/" assert (
reverse("api:user-detail", kwargs={"pk": user.pk}) == f"/api/users/{user.pk}/"
)
assert resolve(f"/api/users/{user.pk}/").view_name == "api:user-detail" assert resolve(f"/api/users/{user.pk}/").view_name == "api:user-detail"
{%- else %} {%- else %}
assert reverse("api:user-detail", kwargs={"username": user.username}) == f"/api/users/{user.username}/" assert (
reverse("api:user-detail", kwargs={"username": user.username})
== f"/api/users/{user.username}/"
)
assert resolve(f"/api/users/{user.username}/").view_name == "api:user-detail" assert resolve(f"/api/users/{user.username}/").view_name == "api:user-detail"
{%- endif %} {%- endif %}

View File

@ -6,7 +6,7 @@ from {{ cookiecutter.project_slug }}.users.models import User
class TestUserViewSet: class TestUserViewSet:
@pytest.fixture @pytest.fixture()
def api_rf(self) -> APIRequestFactory: def api_rf(self) -> APIRequestFactory:
return APIRequestFactory() return APIRequestFactory()
@ -26,7 +26,7 @@ class TestUserViewSet:
view.request = request view.request = request
response = view.me(request) # type: ignore response = view.me(request) # type: ignore[call-arg, arg-type, misc]
assert response.data == { assert response.data == {
{%- if cookiecutter.username_type == "email" %} {%- if cookiecutter.username_type == "email" %}

View File

@ -1,6 +1,5 @@
""" """Module for all Form Tests."""
Module for all Form Tests.
"""
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from {{ cookiecutter.project_slug }}.users.forms import UserAdminCreationForm from {{ cookiecutter.project_slug }}.users.forms import UserAdminCreationForm
@ -31,7 +30,7 @@ class TestUserAdminCreationForm:
{%- endif %} {%- endif %}
"password1": user.password, "password1": user.password,
"password2": user.password, "password2": user.password,
} },
) )
assert not form.is_valid() assert not form.is_valid()

View File

@ -6,12 +6,12 @@ from django.core.management import call_command
from {{ cookiecutter.project_slug }}.users.models import User from {{ cookiecutter.project_slug }}.users.models import User
@pytest.mark.django_db @pytest.mark.django_db()
class TestUserManager: class TestUserManager:
def test_create_user(self): def test_create_user(self):
user = User.objects.create_user( user = User.objects.create_user(
email="john@example.com", email="john@example.com",
password="something-r@nd0m!", password="something-r@nd0m!", # noqa: S106
) )
assert user.email == "john@example.com" assert user.email == "john@example.com"
assert not user.is_staff assert not user.is_staff
@ -22,7 +22,7 @@ class TestUserManager:
def test_create_superuser(self): def test_create_superuser(self):
user = User.objects.create_superuser( user = User.objects.create_superuser(
email="admin@example.com", email="admin@example.com",
password="something-r@nd0m!", password="something-r@nd0m!", # noqa: S106
) )
assert user.email == "admin@example.com" assert user.email == "admin@example.com"
assert user.is_staff assert user.is_staff
@ -32,12 +32,12 @@ class TestUserManager:
def test_create_superuser_username_ignored(self): def test_create_superuser_username_ignored(self):
user = User.objects.create_superuser( user = User.objects.create_superuser(
email="test@example.com", email="test@example.com",
password="something-r@nd0m!", password="something-r@nd0m!", # noqa: S106
) )
assert user.username is None assert user.username is None
@pytest.mark.django_db @pytest.mark.django_db()
def test_createsuperuser_command(): def test_createsuperuser_command():
"""Ensure createsuperuser command works with our custom manager.""" """Ensure createsuperuser command works with our custom manager."""
out = StringIO() out = StringIO()

View File

@ -1,3 +1,5 @@
from http import HTTPStatus
import pytest import pytest
from django.urls import reverse from django.urls import reverse
@ -5,17 +7,17 @@ from django.urls import reverse
def test_swagger_accessible_by_admin(admin_client): def test_swagger_accessible_by_admin(admin_client):
url = reverse("api-docs") url = reverse("api-docs")
response = admin_client.get(url) 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): def test_swagger_ui_not_accessible_by_normal_user(client):
url = reverse("api-docs") url = reverse("api-docs")
response = client.get(url) response = client.get(url)
assert response.status_code == 403 assert response.status_code == HTTPStatus.FORBIDDEN
def test_api_schema_generated_successfully(admin_client): def test_api_schema_generated_successfully(admin_client):
url = reverse("api-schema") url = reverse("api-schema")
response = admin_client.get(url) response = admin_client.get(url)
assert response.status_code == 200 assert response.status_code == HTTPStatus.OK

View File

@ -9,8 +9,9 @@ pytestmark = pytest.mark.django_db
def test_user_count(settings): def test_user_count(settings):
"""A basic test to execute the get_users_count Celery task.""" """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 settings.CELERY_TASK_ALWAYS_EAGER = True
task_result = get_users_count.delay() task_result = get_users_count.delay()
assert isinstance(task_result, EagerResult) assert isinstance(task_result, EagerResult)
assert task_result.result == 3 assert task_result.result == batch_size

View File

@ -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 from {{ cookiecutter.project_slug }}.users.models import User
@ -8,7 +9,10 @@ def test_detail(user: User):
assert reverse("users:detail", kwargs={"pk": user.pk}) == f"/users/{user.pk}/" assert reverse("users:detail", kwargs={"pk": user.pk}) == f"/users/{user.pk}/"
assert resolve(f"/users/{user.pk}/").view_name == "users:detail" assert resolve(f"/users/{user.pk}/").view_name == "users:detail"
{%- else %} {%- else %}
assert reverse("users:detail", kwargs={"username": user.username}) == f"/users/{user.username}/" assert (
reverse("users:detail", kwargs={"username": user.username})
== f"/users/{user.username}/"
)
assert resolve(f"/users/{user.username}/").view_name == "users:detail" assert resolve(f"/users/{user.username}/").view_name == "users:detail"
{%- endif %} {%- endif %}

View File

@ -1,10 +1,13 @@
from http import HTTPStatus
import pytest import pytest
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
from django.contrib.messages.middleware import MessageMiddleware from django.contrib.messages.middleware import MessageMiddleware
from django.contrib.sessions.middleware import SessionMiddleware 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.test import RequestFactory
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ 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.forms import UserAdminChangeForm
from {{ cookiecutter.project_slug }}.users.models import User from {{ cookiecutter.project_slug }}.users.models import User
from {{ cookiecutter.project_slug }}.users.tests.factories import UserFactory from {{ cookiecutter.project_slug }}.users.tests.factories import UserFactory
from {{ cookiecutter.project_slug }}.users.views import ( from {{ cookiecutter.project_slug }}.users.views import UserRedirectView
UserRedirectView, from {{ cookiecutter.project_slug }}.users.views import UserUpdateView
UserUpdateView, from {{ cookiecutter.project_slug }}.users.views import user_detail_view
user_detail_view,
)
pytestmark = pytest.mark.django_db pytestmark = pytest.mark.django_db
@ -102,7 +103,7 @@ class TestUserDetailView:
response = user_detail_view(request, username=user.username) response = user_detail_view(request, username=user.username)
{%- endif %} {%- endif %}
assert response.status_code == 200 assert response.status_code == HTTPStatus.OK
def test_not_authenticated(self, user: User, rf: RequestFactory): def test_not_authenticated(self, user: User, rf: RequestFactory):
request = rf.get("/fake-url/") request = rf.get("/fake-url/")
@ -116,5 +117,5 @@ class TestUserDetailView:
login_url = reverse(settings.LOGIN_URL) login_url = reverse(settings.LOGIN_URL)
assert isinstance(response, HttpResponseRedirect) assert isinstance(response, HttpResponseRedirect)
assert response.status_code == 302 assert response.status_code == HTTPStatus.FOUND
assert response.url == f"{login_url}?next=/fake-url/" assert response.url == f"{login_url}?next=/fake-url/"

View File

@ -1,10 +1,8 @@
from django.urls import path from django.urls import path
from {{ cookiecutter.project_slug }}.users.views import ( from {{ cookiecutter.project_slug }}.users.views import user_detail_view
user_detail_view, from {{ cookiecutter.project_slug }}.users.views import user_redirect_view
user_redirect_view, from {{ cookiecutter.project_slug }}.users.views import user_update_view
user_update_view,
)
app_name = "users" app_name = "users"
urlpatterns = [ urlpatterns = [

View File

@ -2,7 +2,9 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, RedirectView, UpdateView from django.views.generic import DetailView
from django.views.generic import RedirectView
from django.views.generic import UpdateView
from {{ cookiecutter.project_slug }}.users.models import User from {{ cookiecutter.project_slug }}.users.models import User
@ -27,7 +29,8 @@ class UserUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
success_message = _("Information successfully updated") success_message = _("Information successfully updated")
def get_success_url(self): 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() return self.request.user.get_absolute_url()
def get_object(self): def get_object(self):

View File

@ -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 %}