Merge branch 'master' into allauth

# Conflicts:
#	{{cookiecutter.project_slug}}/requirements/base.txt
This commit is contained in:
Bruno Alla 2024-02-24 18:03:38 +01:00
commit 8f2b894176
No known key found for this signature in database
65 changed files with 577 additions and 337 deletions

View File

@ -1115,7 +1115,7 @@
"twitter_username": "Qoyyuum" "twitter_username": "Qoyyuum"
}, },
{ {
"name": "mfosterw", "name": "Matthew Foster Walsh",
"github_login": "mfosterw", "github_login": "mfosterw",
"twitter_username": "" "twitter_username": ""
}, },
@ -1518,5 +1518,15 @@
"name": "henningbra", "name": "henningbra",
"github_login": "henningbra", "github_login": "henningbra",
"twitter_username": "" "twitter_username": ""
},
{
"name": "Paul Wulff",
"github_login": "mtmpaulwulff",
"twitter_username": ""
},
{
"name": "Mounir",
"github_login": "mounirmesselmeni",
"twitter_username": ""
} }
] ]

View File

@ -3,6 +3,22 @@
version: 2 version: 2
updates: updates:
# Update Python deps for the template (not the generated project)
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "daily"
labels:
- "project infrastructure"
# Update Python deps for the documentation
- package-ecosystem: "pip"
directory: "docs/"
schedule:
interval: "daily"
labels:
- "project infrastructure"
# Update GitHub actions in workflows # Update GitHub actions in workflows
- package-ecosystem: "github-actions" - package-ecosystem: "github-actions"
directory: "/" directory: "/"

View File

@ -26,14 +26,14 @@ repos:
args: ["--tab-width", "2"] args: ["--tab-width", "2"]
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v3.15.0 rev: v3.15.1
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py311-plus] args: [--py311-plus]
exclude: hooks/ exclude: hooks/
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 24.1.1 rev: 24.2.0
hooks: hooks:
- id: black - id: black

View File

@ -14,8 +14,6 @@ pin: True
label_prs: update label_prs: update
requirements: requirements:
- "requirements.txt"
- "docs/requirements.txt"
- "{{cookiecutter.project_slug}}/requirements/base.txt" - "{{cookiecutter.project_slug}}/requirements/base.txt"
- "{{cookiecutter.project_slug}}/requirements/local.txt" - "{{cookiecutter.project_slug}}/requirements/local.txt"
- "{{cookiecutter.project_slug}}/requirements/production.txt" - "{{cookiecutter.project_slug}}/requirements/production.txt"

View File

@ -3,6 +3,134 @@ All enhancements and patches to Cookiecutter Django will be documented in this f
<!-- GENERATOR_PLACEHOLDER --> <!-- GENERATOR_PLACEHOLDER -->
## 2024.02.23
### Changed
- Switch to local imports within app ([#4883](https://github.com/cookiecutter/cookiecutter-django/pull/4883))
- Install ruff extension in `devcontainer.json` ([#4887](https://github.com/cookiecutter/cookiecutter-django/pull/4887))
### Updated
- Bump webpack-dev-server to 5.0.2 ([#4875](https://github.com/cookiecutter/cookiecutter-django/pull/4875))
## 2024.02.21
### Changed
- Switch to `celery.shared_task` to define tasks ([#4881](https://github.com/cookiecutter/cookiecutter-django/pull/4881))
- Replace usages of `get_user_model` by importing model directly ([#4879](https://github.com/cookiecutter/cookiecutter-django/pull/4879))
### Updated
- Auto-update pre-commit hooks ([#4873](https://github.com/cookiecutter/cookiecutter-django/pull/4873))
- Update pre-commit to 3.6.2 ([#4874](https://github.com/cookiecutter/cookiecutter-django/pull/4874))
- Update ruff to 0.2.2 ([#4871](https://github.com/cookiecutter/cookiecutter-django/pull/4871))
## 2024.02.19
### Updated
- Update sentry-sdk to 1.40.5 ([#4876](https://github.com/cookiecutter/cookiecutter-django/pull/4876))
## 2024.02.17
### Updated
- Update pytest to 8.0.1 ([#4870](https://github.com/cookiecutter/cookiecutter-django/pull/4870))
## 2024.02.16
### Changed
- Speed up GitHub CI for Docker setup ([#4863](https://github.com/cookiecutter/cookiecutter-django/pull/4863))
### Documentation
- Add link to the ruff repository in requirements ([#4866](https://github.com/cookiecutter/cookiecutter-django/pull/4866))
## 2024.02.13
### Changed
- Ruff linting &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 ## 2024.02.05

View File

@ -1454,6 +1454,13 @@ Listed in alphabetical order.
</td> </td>
<td></td> <td></td>
</tr> </tr>
<tr>
<td>Matthew Foster Walsh</td>
<td>
<a href="https://github.com/mfosterw">mfosterw</a>
</td>
<td></td>
</tr>
<tr> <tr>
<td>Matthew Sisley</td> <td>Matthew Sisley</td>
<td> <td>
@ -1489,13 +1496,6 @@ Listed in alphabetical order.
</td> </td>
<td></td> <td></td>
</tr> </tr>
<tr>
<td>mfosterw</td>
<td>
<a href="https://github.com/mfosterw">mfosterw</a>
</td>
<td></td>
</tr>
<tr> <tr>
<td>Michael Gecht</td> <td>Michael Gecht</td>
<td> <td>
@ -1552,6 +1552,13 @@ Listed in alphabetical order.
</td> </td>
<td></td> <td></td>
</tr> </tr>
<tr>
<td>Mounir</td>
<td>
<a href="https://github.com/mounirmesselmeni">mounirmesselmeni</a>
</td>
<td></td>
</tr>
<tr> <tr>
<td>mozillazg</td> <td>mozillazg</td>
<td> <td>
@ -1650,6 +1657,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

@ -1,20 +1,18 @@
cookiecutter==2.5.0 cookiecutter==2.6.0
sh==2.0.6; sys_platform != "win32" sh==2.0.6; sys_platform != "win32"
binaryornot==0.4.4 binaryornot==0.4.4
# Code quality # Code quality
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
black==24.1.1 ruff==0.2.2
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.2
# Testing # Testing
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
tox==4.12.1 tox==4.13.0
pytest==8.0.0 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
@ -23,6 +21,6 @@ pyyaml==6.0.1
# Scripting # Scripting
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
PyGithub==2.2.0 PyGithub==2.2.0
gitpython==3.1.41 gitpython==3.1.42
jinja2==3.1.3 jinja2==3.1.3
requests==2.31.0 requests==2.31.0

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.02.05" version = "2024.02.23"
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

@ -35,24 +35,13 @@
"analysis.typeCheckingMode": "basic", "analysis.typeCheckingMode": "basic",
"defaultInterpreterPath": "/usr/local/bin/python", "defaultInterpreterPath": "/usr/local/bin/python",
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.organizeImports": true "source.organizeImports": "always"
}, },
// 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
@ -65,8 +54,7 @@
// python // python
"ms-python.python", "ms-python.python",
"ms-python.vscode-pylance", "ms-python.vscode-pylance",
"ms-python.isort", "charliermarsh.ruff",
"ms-python.black-formatter",
// django // django
"batisteo.vscode-django" "batisteo.vscode-django"
] ]

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.2
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: 24.1.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,5 +1,5 @@
# 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 docker.io/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 docker.io/python as python-build-stage FROM docker.io/python as python-build-stage

View File

@ -1,5 +1,5 @@
# 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 docker.io/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

View File

@ -25,7 +25,7 @@ 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 docker.io/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 docker.io/python as python-build-stage FROM docker.io/python as python-build-stage

View File

@ -1,4 +1,4 @@
FROM docker.io/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

@ -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.
@ -29,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):
@ -38,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,3 +1,4 @@
# 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
@ -137,7 +138,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 +213,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 +277,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 +383,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
@ -197,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' %}
@ -273,7 +285,7 @@ COMPRESS_STORAGE = "compressor.storage.GzipCompressorFileStorage"
COMPRESS_STORAGE = STORAGES["staticfiles"]["BACKEND"] 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
@ -291,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
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
@ -351,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": {
@ -403,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,7 +28,7 @@ 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
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

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.

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

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

@ -31,7 +31,7 @@
"webpack": "^5.65.0", "webpack": "^5.65.0",
"webpack-bundle-tracker": "^3.0.1", "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": "^5.0.2",
"webpack-merge": "^5.8.0" "webpack-merge": "^5.8.0"
}, },
"engines": { "engines": {

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.3 # 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.27.0.post1 # https://github.com/encode/uvicorn uvicorn[standard]==0.27.1 # https://github.com/encode/uvicorn
{%- endif %} {%- endif %}
# Django # Django
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
django==4.2.10 # 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[mfa]==0.61.0 # https://github.com/pennersr/django-allauth django-allauth[mfa]==0.61.1 # https://github.com/pennersr/django-allauth
django-crispy-forms==2.1 # https://github.com/django-crispy-forms/django-crispy-forms 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' %}

View File

@ -15,7 +15,7 @@ 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==8.0.0 # https://github.com/pytest-dev/pytest pytest==8.0.1 # https://github.com/pytest-dev/pytest
pytest-sugar==1.0.0 # 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
@ -28,16 +28,10 @@ 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.2 # https://github.com/astral-sh/ruff
flake8-isort==6.1.1 # https://github.com/gforcada/flake8-isort coverage==7.4.3 # https://github.com/nedbat/coveragepy
coverage==7.4.1 # https://github.com/nedbat/coveragepy
black==24.1.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.2 # 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
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View File

@ -8,7 +8,7 @@ psycopg[c]==3.1.18 # https://github.com/psycopg/psycopg
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.40.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

@ -6,13 +6,16 @@ module.exports = merge(commonConfig, {
devtool: 'inline-source-map', devtool: 'inline-source-map',
devServer: { devServer: {
port: 3000, port: 3000,
proxy: { proxy: [
{%- if cookiecutter.use_docker == 'n' %} {
'/': 'http://0.0.0.0:8000', context: ['/'],
{%- else %} {%- if cookiecutter.use_docker == 'n' %}
'/': 'http://django:8000', target: 'http://0.0.0.0:8000',
{%- endif %} {%- else %}
}, target: 'http://django:8000',
{%- endif %}
},
],
client: { client: {
overlay: { overlay: {
errors: true, errors: 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

@ -1,17 +1,17 @@
from django.conf import settings from django.conf import settings
from django.contrib import admin from django.contrib import admin
from django.contrib.auth import admin as auth_admin from django.contrib.auth import admin as auth_admin
from django.contrib.auth import get_user_model, decorators from django.contrib.auth.decorators import login_required
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from {{ cookiecutter.project_slug }}.users.forms import UserAdminChangeForm, UserAdminCreationForm from .forms import UserAdminChangeForm
from .forms import UserAdminCreationForm
User = get_user_model() from .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
admin.site.login = decorators.login_required(admin.site.login) # type: ignore[method-assign] admin.site.login = login_required(admin.site.login) # type: ignore[method-assign]
@admin.register(User) @admin.register(User)

View File

@ -1,13 +1,9 @@
from django.contrib.auth import get_user_model
from rest_framework import serializers from rest_framework import serializers
from {{ cookiecutter.project_slug }}.users.models import User as UserType from {{ cookiecutter.project_slug }}.users.models import User
User = get_user_model() class UserSerializer(serializers.ModelSerializer[User]):
class UserSerializer(serializers.ModelSerializer[UserType]):
class Meta: class Meta:
model = User model = User
{%- if cookiecutter.username_type == "email" %} {%- if cookiecutter.username_type == "email" %}

View File

@ -1,13 +1,14 @@
from django.contrib.auth import get_user_model
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
from .serializers import UserSerializer from {{ cookiecutter.project_slug }}.users.models import User
User = get_user_model() from .serializers import UserSerializer
class UserViewSet(RetrieveModelMixin, ListModelMixin, UpdateModelMixin, GenericViewSet): class UserViewSet(RetrieveModelMixin, ListModelMixin, UpdateModelMixin, 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,13 +1,12 @@
from allauth.account.forms import SignupForm from allauth.account.forms import SignupForm
from allauth.socialaccount.forms import SignupForm as SocialSignupForm from allauth.socialaccount.forms import SignupForm as SocialSignupForm
from django.contrib.auth import forms as admin_forms from django.contrib.auth import forms as admin_forms
from django.contrib.auth import get_user_model
{%- if cookiecutter.username_type == "email" %} {%- if cookiecutter.username_type == "email" %}
from django.forms import EmailField from django.forms import EmailField
{%- endif %} {%- endif %}
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
User = get_user_model() from .models import User
class UserAdminChangeForm(admin_forms.UserChangeForm): class UserAdminChangeForm(admin_forms.UserChangeForm):

View File

@ -4,7 +4,7 @@ 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: if TYPE_CHECKING:
from {{ cookiecutter.project_slug }}.users.models import User # noqa: F401 from .models import User # noqa: F401
class UserManager(DjangoUserManager["User"]): class UserManager(DjangoUserManager["User"]):
@ -15,7 +15,8 @@ class UserManager(DjangoUserManager["User"]):
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)
@ -32,8 +33,10 @@ class UserManager(DjangoUserManager["User"]):
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,14 +1,17 @@
{%- if cookiecutter.username_type == "email" %} {%- if cookiecutter.username_type == "email" %}
from typing import ClassVar from typing import ClassVar
{%- endif %}
{% 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" %}
from {{ cookiecutter.project_slug }}.users.managers import UserManager from .managers import UserManager
{%- endif %} {%- endif %}
@ -21,11 +24,11 @@ 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 = []

View File

@ -1,11 +1,9 @@
from django.contrib.auth import get_user_model from celery import shared_task
from config import celery_app from .models import User
User = get_user_model()
@celery_app.task() @shared_task()
def get_users_count(): def get_users_count():
"""A pointless Celery task to demonstrate usage.""" """A pointless Celery task to demonstrate usage."""
return User.objects.count() return User.objects.count()

View File

@ -1,10 +1,12 @@
from collections.abc import Sequence from collections.abc import Sequence
from typing import Any from typing import Any
from django.contrib.auth import get_user_model from factory import Faker
from factory import Faker, post_generation from factory import post_generation
from factory.django import DjangoModelFactory from factory.django import DjangoModelFactory
from {{ cookiecutter.project_slug }}.users.models import User
class UserFactory(DjangoModelFactory): class UserFactory(DjangoModelFactory):
{%- if cookiecutter.username_type == "username" %} {%- if cookiecutter.username_type == "username" %}
@ -14,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
@ -37,5 +39,5 @@ class UserFactory(DjangoModelFactory):
instance.save() instance.save()
class Meta: class Meta:
model = get_user_model() model = User
django_get_or_create = ["{{cookiecutter.username_type}}"] django_get_or_create = ["{{cookiecutter.username_type}}"]

View File

@ -1,3 +1,5 @@
import contextlib
from http import HTTPStatus
from importlib import reload from importlib import reload
import pytest import pytest
@ -13,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,
@ -37,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 %}
@ -52,21 +54,19 @@ 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 @pytest.fixture()
def force_allauth(self, settings): def _force_allauth(self, settings):
settings.DJANGO_ADMIN_FORCE_ALLAUTH = True settings.DJANGO_ADMIN_FORCE_ALLAUTH = True
# Reload the admin module to apply the setting change # Reload the admin module to apply the setting change
import {{ cookiecutter.project_slug }}.users.admin as users_admin # pylint: disable=import-outside-toplevel import {{ cookiecutter.project_slug }}.users.admin as users_admin
try: with contextlib.suppress(admin.sites.AlreadyRegistered):
reload(users_admin) reload(users_admin)
except admin.sites.AlreadyRegistered:
pass
@pytest.mark.django_db @pytest.mark.django_db()
@pytest.mark.usefixtures("force_allauth") @pytest.mark.usefixtures("_force_allauth")
def test_allauth_login(self, rf, settings): def test_allauth_login(self, rf, settings):
request = rf.get("/fake-url") request = rf.get("/fake-url")
request.user = AnonymousUser() request.user = AnonymousUser()

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

@ -30,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 .views import user_detail_view
user_detail_view, from .views import user_redirect_view
user_redirect_view, from .views import user_update_view
user_update_view,
)
app_name = "users" app_name = "users"
urlpatterns = [ urlpatterns = [

View File

@ -1,11 +1,12 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.mixins import LoginRequiredMixin 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
User = get_user_model() from {{ cookiecutter.project_slug }}.users.models import User
class UserDetailView(LoginRequiredMixin, DetailView): class UserDetailView(LoginRequiredMixin, DetailView):
@ -28,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):