Apply ruff to codebase

This commit is contained in:
Jelmer Draaijer 2024-02-13 11:57:53 +01:00
parent 6ba6104f09
commit 0fc4ea6165
38 changed files with 204 additions and 128 deletions

View File

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

View File

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

View File

@ -1,3 +1,4 @@
# ruff: noqa
""" """
ASGI config for {{ cookiecutter.project_name }} project. ASGI config for {{ cookiecutter.project_name }} project.
@ -29,7 +30,7 @@ django_application = get_asgi_application()
# application = HelloWorldApplication(application) # application = HelloWorldApplication(application)
# Import websocket application here, so apps from django_application are loaded first # Import websocket application here, so apps from django_application are loaded first
from config.websocket import websocket_application # noqa isort:skip from config.websocket import websocket_application
async def application(scope, receive, send): async def application(scope, receive, send):
@ -38,4 +39,5 @@ async def application(scope, receive, send):
elif scope["type"] == "websocket": elif scope["type"] == "websocket":
await websocket_application(scope, receive, send) await websocket_application(scope, receive, send)
else: else:
raise NotImplementedError(f"Unknown scope type {scope['type']}") msg = f"Unknown scope type {scope['type']}"
raise NotImplementedError(msg)

View File

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

View File

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

View File

@ -1,3 +1,4 @@
# ruff: noqa: E501
{% if cookiecutter.use_sentry == 'y' -%} {% if cookiecutter.use_sentry == 'y' -%}
import logging import logging
@ -12,7 +13,12 @@ from sentry_sdk.integrations.logging import LoggingIntegration
from sentry_sdk.integrations.redis import RedisIntegration from sentry_sdk.integrations.redis import RedisIntegration
{% endif -%} {% endif -%}
from .base import * # noqa from .base import * # noqa: F403
from .base import DATABASES
from .base import INSTALLED_APPS
{%- if cookiecutter.use_drf == "y" %}
from .base import SPECTACULAR_SETTINGS
{%- endif %}
from .base import env from .base import env
# GENERAL # GENERAL
@ -24,7 +30,7 @@ ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["{{ cookiecutter.domai
# DATABASES # DATABASES
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
DATABASES["default"]["CONN_MAX_AGE"] = env.int("CONN_MAX_AGE", default=60) # noqa: F405 DATABASES["default"]["CONN_MAX_AGE"] = env.int("CONN_MAX_AGE", default=60)
# CACHES # CACHES
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
@ -38,7 +44,7 @@ CACHES = {
# https://github.com/jazzband/django-redis#memcached-exceptions-behavior # https://github.com/jazzband/django-redis#memcached-exceptions-behavior
"IGNORE_EXCEPTIONS": True, "IGNORE_EXCEPTIONS": True,
}, },
} },
} }
# SECURITY # SECURITY
@ -56,17 +62,23 @@ CSRF_COOKIE_SECURE = True
# TODO: set this to 60 seconds first and then to 518400 once you prove the former works # TODO: set this to 60 seconds first and then to 518400 once you prove the former works
SECURE_HSTS_SECONDS = 60 SECURE_HSTS_SECONDS = 60
# https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-include-subdomains # https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-include-subdomains
SECURE_HSTS_INCLUDE_SUBDOMAINS = env.bool("DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS", default=True) SECURE_HSTS_INCLUDE_SUBDOMAINS = env.bool(
"DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS",
default=True,
)
# https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-preload # https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-preload
SECURE_HSTS_PRELOAD = env.bool("DJANGO_SECURE_HSTS_PRELOAD", default=True) SECURE_HSTS_PRELOAD = env.bool("DJANGO_SECURE_HSTS_PRELOAD", default=True)
# https://docs.djangoproject.com/en/dev/ref/middleware/#x-content-type-options-nosniff # https://docs.djangoproject.com/en/dev/ref/middleware/#x-content-type-options-nosniff
SECURE_CONTENT_TYPE_NOSNIFF = env.bool("DJANGO_SECURE_CONTENT_TYPE_NOSNIFF", default=True) SECURE_CONTENT_TYPE_NOSNIFF = env.bool(
"DJANGO_SECURE_CONTENT_TYPE_NOSNIFF",
default=True,
)
{% if cookiecutter.cloud_provider != 'None' -%} {% if cookiecutter.cloud_provider != 'None' -%}
# STORAGES # STORAGES
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# https://django-storages.readthedocs.io/en/latest/#installation # https://django-storages.readthedocs.io/en/latest/#installation
INSTALLED_APPS += ["storages"] # noqa: F405 INSTALLED_APPS += ["storages"]
{%- endif -%} {%- endif -%}
{% if cookiecutter.cloud_provider == 'AWS' %} {% if cookiecutter.cloud_provider == 'AWS' %}
# https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings
@ -197,7 +209,7 @@ ADMIN_URL = env("DJANGO_ADMIN_URL")
# Anymail # Anymail
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# https://anymail.readthedocs.io/en/stable/installation/#installing-anymail # https://anymail.readthedocs.io/en/stable/installation/#installing-anymail
INSTALLED_APPS += ["anymail"] # noqa: F405 INSTALLED_APPS += ["anymail"]
# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend
# https://anymail.readthedocs.io/en/stable/installation/#anymail-settings-reference # https://anymail.readthedocs.io/en/stable/installation/#anymail-settings-reference
{%- if cookiecutter.mail_service == 'Mailgun' %} {%- if cookiecutter.mail_service == 'Mailgun' %}
@ -273,7 +285,7 @@ COMPRESS_STORAGE = "compressor.storage.GzipCompressorFileStorage"
COMPRESS_STORAGE = STORAGES["staticfiles"]["BACKEND"] COMPRESS_STORAGE = STORAGES["staticfiles"]["BACKEND"]
{%- endif %} {%- endif %}
# https://django-compressor.readthedocs.io/en/latest/settings/#django.conf.settings.COMPRESS_URL # https://django-compressor.readthedocs.io/en/latest/settings/#django.conf.settings.COMPRESS_URL
COMPRESS_URL = STATIC_URL{% if cookiecutter.use_whitenoise == 'y' or cookiecutter.cloud_provider == 'None' %} # noqa: F405{% endif %} COMPRESS_URL = STATIC_URL{% if cookiecutter.use_whitenoise == 'y' or cookiecutter.cloud_provider == 'None' %}{% endif %}
{%- if cookiecutter.use_whitenoise == 'y' %} {%- if cookiecutter.use_whitenoise == 'y' %}
# https://django-compressor.readthedocs.io/en/latest/settings/#django.conf.settings.COMPRESS_OFFLINE # https://django-compressor.readthedocs.io/en/latest/settings/#django.conf.settings.COMPRESS_OFFLINE
COMPRESS_OFFLINE = True # Offline compression is required when using Whitenoise COMPRESS_OFFLINE = True # Offline compression is required when using Whitenoise
@ -291,7 +303,7 @@ COMPRESS_FILTERS = {
# Collectfast # Collectfast
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# https://github.com/antonagestam/collectfast#installation # https://github.com/antonagestam/collectfast#installation
INSTALLED_APPS = ["collectfast"] + INSTALLED_APPS # noqa: F405 INSTALLED_APPS = ["collectfast", *INSTALLED_APPS]
{% endif %} {% endif %}
# LOGGING # LOGGING
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
@ -351,7 +363,7 @@ LOGGING = {
"level": "DEBUG", "level": "DEBUG",
"class": "logging.StreamHandler", "class": "logging.StreamHandler",
"formatter": "verbose", "formatter": "verbose",
} },
}, },
"root": {"level": "INFO", "handlers": ["console"]}, "root": {"level": "INFO", "handlers": ["console"]},
"loggers": { "loggers": {
@ -403,7 +415,7 @@ sentry_sdk.init(
# django-rest-framework # django-rest-framework
# ------------------------------------------------------------------------------- # -------------------------------------------------------------------------------
# Tools that generate code samples can use SERVERS to point to the correct domain # Tools that generate code samples can use SERVERS to point to the correct domain
SPECTACULAR_SETTINGS["SERVERS"] = [ # noqa: F405 SPECTACULAR_SETTINGS["SERVERS"] = [
{"url": "https://{{ cookiecutter.domain_name }}", "description": "Production server"}, {"url": "https://{{ cookiecutter.domain_name }}", "description": "Production server"},
] ]

View File

@ -2,7 +2,8 @@
With these settings, tests run faster. With these settings, tests run faster.
""" """
from .base import * # noqa from .base import * # noqa: F403
from .base import TEMPLATES
from .base import env from .base import env
# GENERAL # GENERAL
@ -27,7 +28,7 @@ EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
# DEBUGGING FOR TEMPLATES # DEBUGGING FOR TEMPLATES
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
TEMPLATES[0]["OPTIONS"]["debug"] = True # type: ignore # noqa: F405 TEMPLATES[0]["OPTIONS"]["debug"] = True # type: ignore[index]
# MEDIA # MEDIA
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View File

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

View File

@ -1,3 +1,4 @@
# ruff: noqa
""" """
WSGI config for {{ cookiecutter.project_name }} project. WSGI config for {{ cookiecutter.project_name }} project.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,12 @@
from django.conf import settings from django.conf import settings
from django.contrib import admin from django.contrib import admin
from django.contrib.auth import admin as auth_admin from django.contrib.auth import admin as auth_admin
from django.contrib.auth import get_user_model, decorators from django.contrib.auth import decorators
from django.contrib.auth import get_user_model
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from {{ cookiecutter.project_slug }}.users.forms import UserAdminChangeForm, UserAdminCreationForm from {{ cookiecutter.project_slug }}.users.forms import UserAdminChangeForm
from {{ cookiecutter.project_slug }}.users.forms import UserAdminCreationForm
User = get_user_model() User = get_user_model()

View File

@ -3,7 +3,6 @@ from rest_framework import serializers
from {{ cookiecutter.project_slug }}.users.models import User as UserType from {{ cookiecutter.project_slug }}.users.models import User as UserType
User = get_user_model() User = get_user_model()

View File

@ -1,7 +1,9 @@
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from rest_framework import status from rest_framework import status
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.mixins import ListModelMixin, RetrieveModelMixin, UpdateModelMixin from rest_framework.mixins import ListModelMixin
from rest_framework.mixins import RetrieveModelMixin
from rest_framework.mixins import UpdateModelMixin
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet

View File

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

View File

@ -15,7 +15,8 @@ class UserManager(DjangoUserManager["User"]):
Create and save a user with the given email and password. Create and save a user with the given email and password.
""" """
if not email: if not email:
raise ValueError("The given email must be set") msg = "The given email must be set"
raise ValueError(msg)
email = self.normalize_email(email) email = self.normalize_email(email)
user = self.model(email=email, **extra_fields) user = self.model(email=email, **extra_fields)
user.password = make_password(password) user.password = make_password(password)
@ -32,8 +33,10 @@ class UserManager(DjangoUserManager["User"]):
extra_fields.setdefault("is_superuser", True) extra_fields.setdefault("is_superuser", True)
if extra_fields.get("is_staff") is not True: if extra_fields.get("is_staff") is not True:
raise ValueError("Superuser must have is_staff=True.") msg = "Superuser must have is_staff=True."
raise ValueError(msg)
if extra_fields.get("is_superuser") is not True: if extra_fields.get("is_superuser") is not True:
raise ValueError("Superuser must have is_superuser=True.") msg = "Superuser must have is_superuser=True."
raise ValueError(msg)
return self._create_user(email, password, **extra_fields) return self._create_user(email, password, **extra_fields)

View File

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

View File

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

View File

@ -2,7 +2,8 @@ from collections.abc import Sequence
from typing import Any from typing import Any
from django.contrib.auth import get_user_model 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 from factory.django import DjangoModelFactory
@ -14,7 +15,7 @@ class UserFactory(DjangoModelFactory):
name = Faker("name") name = Faker("name")
@post_generation @post_generation
def password(self, create: bool, extracted: Sequence[Any], **kwargs): def password(self, create: bool, extracted: Sequence[Any], **kwargs): # noqa: FBT001
password = ( password = (
extracted extracted
if extracted if extracted

View File

@ -1,3 +1,5 @@
import contextlib
from http import HTTPStatus
from importlib import reload from importlib import reload
import pytest import pytest
@ -13,17 +15,17 @@ class TestUserAdmin:
def test_changelist(self, admin_client): def test_changelist(self, admin_client):
url = reverse("admin:users_user_changelist") url = reverse("admin:users_user_changelist")
response = admin_client.get(url) response = admin_client.get(url)
assert response.status_code == 200 assert response.status_code == HTTPStatus.OK
def test_search(self, admin_client): def test_search(self, admin_client):
url = reverse("admin:users_user_changelist") url = reverse("admin:users_user_changelist")
response = admin_client.get(url, data={"q": "test"}) response = admin_client.get(url, data={"q": "test"})
assert response.status_code == 200 assert response.status_code == HTTPStatus.OK
def test_add(self, admin_client): def test_add(self, admin_client):
url = reverse("admin:users_user_add") url = reverse("admin:users_user_add")
response = admin_client.get(url) response = admin_client.get(url)
assert response.status_code == 200 assert response.status_code == HTTPStatus.OK
response = admin_client.post( response = admin_client.post(
url, url,
@ -37,7 +39,7 @@ class TestUserAdmin:
"password2": "My_R@ndom-P@ssw0rd", "password2": "My_R@ndom-P@ssw0rd",
}, },
) )
assert response.status_code == 302 assert response.status_code == HTTPStatus.FOUND
{%- if cookiecutter.username_type == "email" %} {%- if cookiecutter.username_type == "email" %}
assert User.objects.filter(email="new-admin@example.com").exists() assert User.objects.filter(email="new-admin@example.com").exists()
{%- else %} {%- else %}
@ -52,21 +54,19 @@ class TestUserAdmin:
{%- endif %} {%- endif %}
url = reverse("admin:users_user_change", kwargs={"object_id": user.pk}) url = reverse("admin:users_user_change", kwargs={"object_id": user.pk})
response = admin_client.get(url) response = admin_client.get(url)
assert response.status_code == 200 assert response.status_code == HTTPStatus.OK
@pytest.fixture @pytest.fixture()
def force_allauth(self, settings): def _force_allauth(self, settings):
settings.DJANGO_ADMIN_FORCE_ALLAUTH = True settings.DJANGO_ADMIN_FORCE_ALLAUTH = True
# Reload the admin module to apply the setting change # Reload the admin module to apply the setting change
import {{ cookiecutter.project_slug }}.users.admin as users_admin import {{ cookiecutter.project_slug }}.users.admin as users_admin
try: with contextlib.suppress(admin.sites.AlreadyRegistered):
reload(users_admin) reload(users_admin)
except admin.sites.AlreadyRegistered:
pass
@pytest.mark.django_db @pytest.mark.django_db()
@pytest.mark.usefixtures("force_allauth") @pytest.mark.usefixtures("_force_allauth")
def test_allauth_login(self, rf, settings): def test_allauth_login(self, rf, settings):
request = rf.get("/fake-url") request = rf.get("/fake-url")
request.user = AnonymousUser() request.user = AnonymousUser()

View File

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

View File

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

View File

@ -30,7 +30,7 @@ class TestUserAdminCreationForm:
{%- endif %} {%- endif %}
"password1": user.password, "password1": user.password,
"password2": user.password, "password2": user.password,
} },
) )
assert not form.is_valid() assert not form.is_valid()

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
from django.urls import resolve, reverse from django.urls import resolve
from django.urls import reverse
from {{ cookiecutter.project_slug }}.users.models import User from {{ cookiecutter.project_slug }}.users.models import User
@ -8,7 +9,10 @@ def test_detail(user: User):
assert reverse("users:detail", kwargs={"pk": user.pk}) == f"/users/{user.pk}/" assert reverse("users:detail", kwargs={"pk": user.pk}) == f"/users/{user.pk}/"
assert resolve(f"/users/{user.pk}/").view_name == "users:detail" assert resolve(f"/users/{user.pk}/").view_name == "users:detail"
{%- else %} {%- else %}
assert reverse("users:detail", kwargs={"username": user.username}) == f"/users/{user.username}/" assert (
reverse("users:detail", kwargs={"username": user.username})
== f"/users/{user.username}/"
)
assert resolve(f"/users/{user.username}/").view_name == "users:detail" assert resolve(f"/users/{user.username}/").view_name == "users:detail"
{%- endif %} {%- endif %}

View File

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

View File

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

View File

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