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",
"github_login": "nixsiow",
"twitter_username": "nixsiow"
},
{
"name": "Jens Kaeske",
"github_login": "jkaeske",
"twitter_username": ""
},
{
"name": "henningbra",
"github_login": "henningbra",
"twitter_username": ""
},
{
"name": "Paul Wulff",
"github_login": "mtmpaulwulff",
"twitter_username": ""
}
]

View File

@ -23,7 +23,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: tiangolo/issue-manager@0.4.1
- uses: tiangolo/issue-manager@0.5.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
config: >
@ -39,5 +39,9 @@ jobs:
"waiting": {
"delay": 864000,
"message": "Automatically closing after waiting for additional info. To re-open, please provide the additional information requested."
},
"wontfix": {
"delay": 864000,
"message": "As discussed, we won't be implementing this. Automatically closing."
}
}

View File

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

View File

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

View File

@ -3,6 +3,247 @@ All enhancements and patches to Cookiecutter Django will be documented in this f
<!-- 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

View File

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

View File

@ -4,40 +4,30 @@ Linters
.. index:: linters
flake8
ruff
------
To run flake8: ::
Ruff is a Python linter and code formatter, written in Rust.
It is a aggregation of flake8, pylint, pyupgrade and many more.
$ flake8
Ruff comes with a linter (``ruff check``) and a formatter (``ruff format``).
The linter is a wrapper around flake8, pylint, and other linters,
and the formatter is a wrapper around black, isort, and other formatters.
The config for flake8 is located in setup.cfg. It specifies:
To run ruff without modifying your files: ::
* Set max line length to 120 chars
* Exclude ``.tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules``
$ ruff format --diff .
$ ruff check .
pylint
------
Ruff is capable of fixing most of the problems it encounters.
Be sure you commit first before running `ruff` so you can restore to a savepoint (and amend afterwards to prevent a double commit. : ::
To run pylint: ::
$ ruff format .
$ ruff check --fix .
# be careful with the --unsafe-fixes option, it can break your code
$ ruff check --fix --unsafe-fixes .
$ pylint <python files that you wish to lint>
The config for pylint is located in .pylintrc. It specifies:
* Use the pylint_django plugin. If using Celery, also use pylint_celery.
* Set max line length to 120 chars
* Disable linting messages for missing docstring and invalid name
* max-parents=13
pycodestyle
-----------
This is included in flake8's checks, but you can also run it separately to see a more detailed report: ::
$ pycodestyle <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``
The config for ruff is located in pyproject.toml.
On of the most important option is `tool.ruff.lint.select`.
`select` determines which linters are run. In example, `DJ <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/>`_

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

View File

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

View File

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

View File

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

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
to keep up to date.
"""
from __future__ import annotations
import os

View File

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

View File

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

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
# Make sure the check doesn't raise any warnings
docker compose -f local.yml run django python manage.py check --fail-level WARNING
docker compose -f local.yml run \
-e DJANGO_SECRET_KEY="$(openssl rand -base64 64)" \
-e REDIS_URL=redis://redis:6379/0 \
-e CELERY_BROKER_URL=redis://redis:6379/0 \
-e DJANGO_AWS_ACCESS_KEY_ID=x \
-e DJANGO_AWS_SECRET_ACCESS_KEY=x \
-e DJANGO_AWS_STORAGE_BUCKET_NAME=x \
-e DJANGO_ADMIN_URL=x \
-e MAILGUN_API_KEY=x \
-e MAILGUN_DOMAIN=x \
django python manage.py check --settings=config.settings.production --deploy --database default --fail-level WARNING
# Generate the HTML for the documentation
docker compose -f local.yml run docs make html

View File

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

View File

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

View File

@ -36,7 +36,7 @@ jobs:
- name: Run pre-commit
uses: pre-commit/action@v3.0.0
# With no caching at all the entire ci process takes 4m 30s to complete!
# With no caching at all the entire ci process takes 3m to complete!
pytest:
runs-on: ubuntu-latest
{%- if cookiecutter.use_docker == 'n' %}
@ -69,7 +69,7 @@ jobs:
{%- if cookiecutter.use_docker == 'y' %}
- name: Build the Stack
run: docker compose -f local.yml build
run: docker compose -f local.yml build django
- name: Run DB Migrations
run: docker compose -f local.yml run --rm django python manage.py migrate

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
FROM node:20-bookworm-slim
FROM docker.io/node:20-bookworm-slim
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/postgres/maintenance/_sourced /usr/local/bin/maintenance/_sourced

View File

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

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

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
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 \
&& touch /etc/traefik/acme/acme.json \
&& chmod 600 /etc/traefik/acme/acme.json

View File

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

View File

@ -1,12 +1,10 @@
from django.conf import settings
from rest_framework.routers import DefaultRouter, SimpleRouter
from rest_framework.routers import DefaultRouter
from rest_framework.routers import SimpleRouter
from {{ cookiecutter.project_slug }}.users.api.views import UserViewSet
if settings.DEBUG:
router = DefaultRouter()
else:
router = SimpleRouter()
router = DefaultRouter() if settings.DEBUG else SimpleRouter()
router.register("users", UserViewSet)

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@ -3,12 +3,12 @@
-r base.txt
gunicorn==21.2.0 # https://github.com/benoitc/gunicorn
psycopg[c]==3.1.17 # https://github.com/psycopg/psycopg
psycopg[c]==3.1.18 # https://github.com/psycopg/psycopg
{%- if cookiecutter.use_whitenoise == 'n' %}
Collectfast==2.2.0 # https://github.com/antonagestam/collectfast
{%- endif %}
{%- if cookiecutter.use_sentry == "y" %}
sentry-sdk==1.39.2 # https://github.com/getsentry/sentry-python
sentry-sdk==1.40.5 # https://github.com/getsentry/sentry-python
{%- endif %}
{%- if cookiecutter.use_docker == "n" and cookiecutter.windows == "y" %}
hiredis==2.3.2 # https://github.com/redis/hiredis-py

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',
{%- endif %}
},
client: {
overlay: {
errors: true,
warnings: false,
runtimeErrors: true,
},
},
// We need hot=false (Disable HMR) to set liveReload=true
hot: false,
liveReload: true,

View File

@ -1,2 +1,5 @@
__version__ = "{{ cookiecutter.version }}"
__version_info__ = tuple(int(num) if num.isdigit() else num for num in __version__.replace("-", ".", 1).split("."))
__version_info__ = tuple(
int(num) if num.isdigit() else num
for num in __version__.replace("-", ".", 1).split(".")
)

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import django.contrib.sites.models
from django.db import migrations, models
from django.db import migrations
from django.db import models
class Migration(migrations.Migration):

View File

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

View File

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

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.models import User
if settings.DJANGO_ADMIN_FORCE_ALLAUTH:
# Force the `admin` sign in process to go through the `django-allauth` workflow:
# https://docs.allauth.org/en/latest/common/admin.html#admin

View File

@ -1,6 +1,8 @@
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.mixins import ListModelMixin, RetrieveModelMixin, UpdateModelMixin
from rest_framework.mixins import ListModelMixin
from rest_framework.mixins import RetrieveModelMixin
from rest_framework.mixins import UpdateModelMixin
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet

View File

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

View File

@ -1,8 +1,13 @@
from typing import TYPE_CHECKING
from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import UserManager as DjangoUserManager
if TYPE_CHECKING:
from {{ cookiecutter.project_slug }}.users.models import User # noqa: F401
class UserManager(DjangoUserManager):
class UserManager(DjangoUserManager["User"]):
"""Custom manager for the User model."""
def _create_user(self, email: str, password: str | None, **extra_fields):
@ -10,25 +15,28 @@ class UserManager(DjangoUserManager):
Create and save a user with the given email and password.
"""
if not email:
raise ValueError("The given email must be set")
msg = "The given email must be set"
raise ValueError(msg)
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.password = make_password(password)
user.save(using=self._db)
return user
def create_user(self, email: str, password: str | None = None, **extra_fields):
def create_user(self, email: str, password: str | None = None, **extra_fields): # type: ignore[override]
extra_fields.setdefault("is_staff", False)
extra_fields.setdefault("is_superuser", False)
return self._create_user(email, password, **extra_fields)
def create_superuser(self, email: str, password: str | None = None, **extra_fields):
def create_superuser(self, email: str, password: str | None = None, **extra_fields): # type: ignore[override]
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True)
if extra_fields.get("is_staff") is not True:
raise ValueError("Superuser must have is_staff=True.")
msg = "Superuser must have is_staff=True."
raise ValueError(msg)
if extra_fields.get("is_superuser") is not True:
raise ValueError("Superuser must have is_superuser=True.")
msg = "Superuser must have is_superuser=True."
raise ValueError(msg)
return self._create_user(email, password, **extra_fields)

View File

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

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

View File

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

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

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
def test_user_detail(user: User):
{%- if cookiecutter.username_type == "email" %}
assert reverse("api:user-detail", kwargs={"pk": user.pk}) == f"/api/users/{user.pk}/"
assert (
reverse("api:user-detail", kwargs={"pk": user.pk}) == f"/api/users/{user.pk}/"
)
assert resolve(f"/api/users/{user.pk}/").view_name == "api:user-detail"
{%- else %}
assert reverse("api:user-detail", kwargs={"username": user.username}) == f"/api/users/{user.username}/"
assert (
reverse("api:user-detail", kwargs={"username": user.username})
== f"/api/users/{user.username}/"
)
assert resolve(f"/api/users/{user.username}/").view_name == "api:user-detail"
{%- endif %}

View File

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

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 {{ cookiecutter.project_slug }}.users.forms import UserAdminCreationForm
@ -31,7 +30,7 @@ class TestUserAdminCreationForm:
{%- endif %}
"password1": user.password,
"password2": user.password,
}
},
)
assert not form.is_valid()

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@ -2,7 +2,9 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.messages.views import SuccessMessageMixin
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, RedirectView, UpdateView
from django.views.generic import DetailView
from django.views.generic import RedirectView
from django.views.generic import UpdateView
from {{ cookiecutter.project_slug }}.users.models import User
@ -27,7 +29,8 @@ class UserUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
success_message = _("Information successfully updated")
def get_success_url(self):
assert self.request.user.is_authenticated # for mypy to know that the user is authenticated
# for mypy to know that the user is authenticated
assert self.request.user.is_authenticated
return self.request.user.get_absolute_url()
def get_object(self):

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