mirror of
https://github.com/cookiecutter/cookiecutter-django.git
synced 2024-11-22 09:36:52 +03:00
Merge pull request #4834 from foarsitter/ruff
Ruff linting & formatting
This commit is contained in:
commit
5bc8ac664c
|
@ -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/>`_
|
||||
|
|
|
@ -4,9 +4,7 @@ binaryornot==0.4.4
|
|||
|
||||
# Code quality
|
||||
# ------------------------------------------------------------------------------
|
||||
black==24.2.0
|
||||
isort==5.13.2
|
||||
flake8==7.0.0
|
||||
ruff==0.2.1
|
||||
django-upgrade==1.16.0
|
||||
djlint==1.34.1
|
||||
pre-commit==3.6.1
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -33,26 +33,15 @@ repos:
|
|||
- 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: 24.2.0
|
||||
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
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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" %}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# ruff: noqa
|
||||
"""
|
||||
ASGI config for {{ cookiecutter.project_name }} project.
|
||||
|
||||
|
@ -29,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):
|
||||
|
@ -38,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)
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# ruff: noqa: ERA001, E501
|
||||
"""Base settings to build other settings files upon."""
|
||||
|
||||
from pathlib import Path
|
||||
|
@ -136,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"},
|
||||
|
@ -209,7 +212,7 @@ TEMPLATES = [
|
|||
"{{cookiecutter.project_slug}}.users.context_processors.allauth_settings",
|
||||
],
|
||||
},
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#form-renderer
|
||||
|
@ -273,7 +276,7 @@ LOGGING = {
|
|||
"level": "DEBUG",
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "verbose",
|
||||
}
|
||||
},
|
||||
},
|
||||
"root": {"level": "INFO", "handlers": ["console"]},
|
||||
}
|
||||
|
@ -379,7 +382,7 @@ WEBPACK_LOADER = {
|
|||
"STATS_FILE": BASE_DIR / "webpack-stats.json",
|
||||
"POLL_INTERVAL": 0.1,
|
||||
"IGNORE": [r".+\.hot-update.js", r".+\.map"],
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
{%- endif %}
|
||||
|
|
|
@ -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...
|
||||
|
|
|
@ -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
|
||||
|
@ -197,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' %}
|
||||
|
@ -273,7 +285,7 @@ COMPRESS_STORAGE = "compressor.storage.GzipCompressorFileStorage"
|
|||
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
|
||||
|
@ -291,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
|
||||
# ------------------------------------------------------------------------------
|
||||
|
@ -351,7 +363,7 @@ LOGGING = {
|
|||
"level": "DEBUG",
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "verbose",
|
||||
}
|
||||
},
|
||||
},
|
||||
"root": {"level": "INFO", "handlers": ["console"]},
|
||||
"loggers": {
|
||||
|
@ -403,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"},
|
||||
]
|
||||
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
With these settings, tests run faster.
|
||||
"""
|
||||
|
||||
from .base import * # noqa
|
||||
from .base import * # noqa: F403
|
||||
from .base import TEMPLATES
|
||||
from .base import env
|
||||
|
||||
# GENERAL
|
||||
|
@ -27,7 +28,7 @@ EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
|
|||
|
||||
# DEBUGGING FOR TEMPLATES
|
||||
# ------------------------------------------------------------------------------
|
||||
TEMPLATES[0]["OPTIONS"]["debug"] = True # type: ignore # noqa: F405
|
||||
TEMPLATES[0]["OPTIONS"]["debug"] = True # type: ignore[index]
|
||||
|
||||
# MEDIA
|
||||
# ------------------------------------------------------------------------------
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# ruff: noqa
|
||||
"""
|
||||
WSGI config for {{ cookiecutter.project_name }} project.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 "
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# ruff: noqa
|
||||
import os
|
||||
from collections.abc import Sequence
|
||||
from pathlib import Path
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -28,15 +28,9 @@ 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
|
||||
ruff==0.2.1
|
||||
coverage==7.4.1 # https://github.com/nedbat/coveragepy
|
||||
black==24.2.0 # https://github.com/psf/black
|
||||
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.1 # https://github.com/pre-commit/pre-commit
|
||||
|
||||
# Django
|
||||
|
|
|
@ -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
|
0
{{cookiecutter.project_slug}}/tests/__init__.py
Normal file
0
{{cookiecutter.project_slug}}/tests/__init__.py
Normal 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(".")
|
||||
)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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())],
|
||||
)
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth import admin as auth_admin
|
||||
from django.contrib.auth import get_user_model, decorators
|
||||
from django.contrib.auth import decorators
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from {{ cookiecutter.project_slug }}.users.forms import UserAdminChangeForm, UserAdminCreationForm
|
||||
from {{ cookiecutter.project_slug }}.users.forms import UserAdminChangeForm
|
||||
from {{ cookiecutter.project_slug }}.users.forms import UserAdminCreationForm
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@ from rest_framework import serializers
|
|||
|
||||
from {{ cookiecutter.project_slug }}.users.models import User as UserType
|
||||
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
from django.contrib.auth import get_user_model
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.mixins import ListModelMixin, RetrieveModelMixin, UpdateModelMixin
|
||||
from rest_framework.mixins import ListModelMixin
|
||||
from rest_framework.mixins import RetrieveModelMixin
|
||||
from rest_framework.mixins import UpdateModelMixin
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -15,7 +15,8 @@ class UserManager(DjangoUserManager["User"]):
|
|||
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)
|
||||
|
@ -32,8 +33,10 @@ class UserManager(DjangoUserManager["User"]):
|
|||
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)
|
||||
|
|
|
@ -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",
|
||||
),
|
||||
),
|
||||
(
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
{%- if cookiecutter.username_type == "email" %}
|
||||
from typing import ClassVar
|
||||
{%- endif %}
|
||||
|
||||
{% 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" %}
|
||||
|
@ -21,11 +24,11 @@ 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 = []
|
||||
|
|
|
@ -2,7 +2,8 @@ from collections.abc import Sequence
|
|||
from typing import Any
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from factory import Faker, post_generation
|
||||
from factory import Faker
|
||||
from factory import post_generation
|
||||
from factory.django import DjangoModelFactory
|
||||
|
||||
|
||||
|
@ -14,7 +15,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
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import contextlib
|
||||
from http import HTTPStatus
|
||||
from importlib import reload
|
||||
|
||||
import pytest
|
||||
|
@ -13,17 +15,17 @@ class TestUserAdmin:
|
|||
def test_changelist(self, admin_client):
|
||||
url = reverse("admin:users_user_changelist")
|
||||
response = admin_client.get(url)
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
|
||||
def test_search(self, admin_client):
|
||||
url = reverse("admin:users_user_changelist")
|
||||
response = admin_client.get(url, data={"q": "test"})
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
|
||||
def test_add(self, admin_client):
|
||||
url = reverse("admin:users_user_add")
|
||||
response = admin_client.get(url)
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
|
||||
response = admin_client.post(
|
||||
url,
|
||||
|
@ -37,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 %}
|
||||
|
@ -52,21 +54,19 @@ 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):
|
||||
@pytest.fixture()
|
||||
def _force_allauth(self, settings):
|
||||
settings.DJANGO_ADMIN_FORCE_ALLAUTH = True
|
||||
# Reload the admin module to apply the setting change
|
||||
import {{ cookiecutter.project_slug }}.users.admin as users_admin # pylint: disable=import-outside-toplevel
|
||||
import {{ cookiecutter.project_slug }}.users.admin as users_admin
|
||||
|
||||
try:
|
||||
with contextlib.suppress(admin.sites.AlreadyRegistered):
|
||||
reload(users_admin)
|
||||
except admin.sites.AlreadyRegistered:
|
||||
pass
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.usefixtures("force_allauth")
|
||||
@pytest.mark.django_db()
|
||||
@pytest.mark.usefixtures("_force_allauth")
|
||||
def test_allauth_login(self, rf, settings):
|
||||
request = rf.get("/fake-url")
|
||||
request.user = AnonymousUser()
|
||||
|
|
|
@ -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 %}
|
||||
|
||||
|
|
|
@ -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" %}
|
||||
|
|
|
@ -30,7 +30,7 @@ class TestUserAdminCreationForm:
|
|||
{%- endif %}
|
||||
"password1": user.password,
|
||||
"password2": user.password,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
assert not form.is_valid()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 %}
|
||||
|
||||
|
|
|
@ -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/"
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -3,7 +3,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
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
@ -28,7 +30,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):
|
||||
|
|
Loading…
Reference in New Issue
Block a user