akarpov/config/settings/base.py

646 lines
22 KiB
Python

"""
Base settings to build other settings files upon.
"""
from pathlib import Path
import environ
import structlog
from celery.schedules import crontab
from sentry_sdk.integrations.celery import CeleryIntegration
ROOT_DIR = Path(__file__).resolve(strict=True).parent.parent.parent
# akarpov/
APPS_DIR = ROOT_DIR / "akarpov"
env = environ.Env()
READ_DOT_ENV_FILE = env.bool("DJANGO_READ_DOT_ENV_FILE", default=True)
if READ_DOT_ENV_FILE:
# OS environment variables take precedence over variables from .env
env.read_env(str(ROOT_DIR / ".env"))
# GENERAL
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#debug
DEBUG = env.bool("DJANGO_DEBUG", False)
# Local time zone. Choices are
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# though not all of them may be available with every OS.
# In Windows, this must be set to your system time zone.
TIME_ZONE = "Europe/Moscow"
DATETIME_FORMAT = "%H:%M:%S %d.%m.%Y"
# https://docs.djangoproject.com/en/dev/ref/settings/#language-code
LANGUAGE_CODE = "en-us"
# https://docs.djangoproject.com/en/dev/ref/settings/#site-id
SITE_ID = 1
# https://docs.djangoproject.com/en/dev/ref/settings/#use-i18n
USE_I18N = True
# https://docs.djangoproject.com/en/dev/ref/settings/#use-tz
USE_TZ = True
# https://docs.djangoproject.com/en/dev/ref/settings/#locale-paths
LOCALE_PATHS = [str(ROOT_DIR / "locale")]
LANGUAGES = [
("en-us", "English"),
("ru", "Russian"),
]
# DATABASES
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#databases
DATABASES = {"default": env.db("DATABASE_URL")}
DATABASES["default"]["ATOMIC_REQUESTS"] = True
# https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-DEFAULT_AUTO_FIELD
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# CACHES
# ------------------------------------------------------------------------------
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": env("REDIS_CACHE_URL", default="redis://localhost:6379/1"),
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
},
}
}
CACHE_MIDDLEWARE_KEY_PREFIX = "cache_middleware"
CACHE_MIDDLEWARE_SECONDS = 0
CACHE_TTL = 60 * 10
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "default"
CACHEOPS = {
"auth.user": {"ops": "get", "timeout": 60 * 15},
"auth.*": {"ops": ("fetch", "get"), "timeout": 60 * 2},
"blog.post": {"ops": ("fetch", "get"), "timeout": 20 * 15},
"themes.theme": {"ops": ("fetch", "get"), "timeout": 60 * 60},
"gallery.*": {"ops": ("fetch", "get", "list"), "timeout": 60 * 15},
"files.*": {"ops": ("fetch", "get"), "timeout": 60},
"auth.permission": {"ops": "all", "timeout": 60 * 15},
"music.*": {"ops": ("fetch", "get", "list"), "timeout": 60 * 15},
}
CACHEOPS_REDIS = env.str("REDIS_URL")
# URLS
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#root-urlconf
ROOT_URLCONF = "config.urls"
# https://docs.djangoproject.com/en/dev/ref/settings/#wsgi-application
WSGI_APPLICATION = "config.wsgi.application"
ASGI_APPLICATION = "config.asgi.application"
# CHANNELS
# ------------------------------------------------------------------------------
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [env("REDIS_URL")],
},
},
}
# APPS
# ------------------------------------------------------------------------------
DJANGO_APPS = [
"daphne",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.sites",
"django.contrib.sitemaps",
"django.contrib.messages",
"django.contrib.staticfiles",
"django.contrib.humanize", # Handy template tags
# required for jazzmin to work
"jazzmin",
"django.contrib.admin",
"django.forms",
]
THIRD_PARTY_APPS = [
"django.contrib.postgres",
"crispy_forms",
"crispy_bootstrap5",
"allauth",
"allauth.account",
"allauth.socialaccount",
"django_celery_beat",
"rest_framework",
"rest_framework.authtoken",
"corsheaders",
"drf_spectacular",
"ckeditor",
"ckeditor_uploader",
"colorfield",
"polymorphic",
"cacheops",
"extra_settings",
"akarpov.contrib.chunked_upload",
"active_link",
"robots",
"django_filters",
"django_tables2",
"location_field",
"django_elasticsearch_dsl",
]
HEALTH_CHECKS = [
"health_check", # required
"health_check.db", # stock Django health checkers
"health_check.cache",
"health_check.storage",
"health_check.contrib.celery",
"health_check.contrib.celery_ping",
"health_check.contrib.migrations",
"health_check.contrib.psutil", # disk and memory utilization
"health_check.contrib.redis",
]
ALLAUTH_PROVIDERS = [
"allauth.socialaccount.providers.github",
"allauth.socialaccount.providers.google",
# "allauth.socialaccount.providers.telegram", TODO
# "allauth.socialaccount.providers.yandex", TODO
]
LOCAL_APPS = [
"akarpov.users",
"akarpov.about",
"akarpov.blog",
"akarpov.files",
"akarpov.music",
"akarpov.gallery",
"akarpov.tools.qr",
"akarpov.pipeliner",
"akarpov.users.themes",
"akarpov.notifications",
"akarpov.test_platform",
"akarpov.tools.shortener",
"akarpov.tools.promocodes",
]
# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
INSTALLED_APPS = (
DJANGO_APPS + LOCAL_APPS + THIRD_PARTY_APPS + HEALTH_CHECKS + ALLAUTH_PROVIDERS
)
# MIGRATIONS
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#migration-modules
MIGRATION_MODULES = {"sites": "akarpov.contrib.sites.migrations"}
# AUTHENTICATION
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#authentication-backends
AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend",
"allauth.account.auth_backends.AuthenticationBackend",
]
# https://docs.djangoproject.com/en/dev/ref/settings/#auth-user-model
AUTH_USER_MODEL = "users.User"
# https://docs.djangoproject.com/en/dev/ref/settings/#login-redirect-url
LOGIN_REDIRECT_URL = "users:redirect"
# https://docs.djangoproject.com/en/dev/ref/settings/#login-url
LOGIN_URL = "account_login"
# PASSWORDS
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers
PASSWORD_HASHERS = [
# https://docs.djangoproject.com/en/dev/topics/auth/passwords/#using-argon2-with-django
"django.contrib.auth.hashers.Argon2PasswordHasher",
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
"django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
"django.contrib.auth.hashers.BCryptSHA256PasswordHasher",
]
# 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.MinimumLengthValidator"},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
# MIDDLEWARE
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#middleware
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.middleware.cache.UpdateCacheMiddleware",
"corsheaders.middleware.CorsMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"django_structlog.middlewares.RequestMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.locale.LocaleMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.common.BrokenLinkEmailsMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"django.middleware.cache.FetchFromCacheMiddleware",
]
# STATIC
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#static-root
STATIC_ROOT = str(ROOT_DIR / "staticfiles")
# https://docs.djangoproject.com/en/dev/ref/settings/#static-url
STATIC_URL = "/static/"
# https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS
STATICFILES_DIRS = [str(APPS_DIR / "static")]
# https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders
STATICFILES_FINDERS = [
"django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
]
# MEDIA
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#media-root
MEDIA_ROOT = str(APPS_DIR / "media")
# https://docs.djangoproject.com/en/dev/ref/settings/#media-url
MEDIA_URL = "/media/"
# TEMPLATES
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#templates
TEMPLATES = [
{
# https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-TEMPLATES-BACKEND
"BACKEND": "django.template.backends.django.DjangoTemplates",
# https://docs.djangoproject.com/en/dev/ref/settings/#dirs
"DIRS": [str(APPS_DIR / "templates")],
# https://docs.djangoproject.com/en/dev/ref/settings/#app-dirs
"APP_DIRS": True,
"OPTIONS": {
# https://docs.djangoproject.com/en/dev/ref/settings/#template-context-processors
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.template.context_processors.i18n",
"django.template.context_processors.media",
"django.template.context_processors.static",
"django.template.context_processors.tz",
"django.contrib.messages.context_processors.messages",
"akarpov.users.context_processors.allauth_settings",
],
},
}
]
# https://docs.djangoproject.com/en/dev/ref/settings/#form-renderer
FORM_RENDERER = "django.forms.renderers.TemplatesSetting"
# http://django-crispy-forms.readthedocs.io/en/latest/install.html#template-packs
CRISPY_TEMPLATE_PACK = "bootstrap5"
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
# FIXTURES
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#fixture-dirs
FIXTURE_DIRS = (str(APPS_DIR / "fixtures"),)
# SECURITY
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#session-cookie-httponly
SESSION_COOKIE_HTTPONLY = True
# https://docs.djangoproject.com/en/dev/ref/settings/#csrf-cookie-httponly
CSRF_COOKIE_HTTPONLY = True
# https://docs.djangoproject.com/en/dev/ref/settings/#secure-browser-xss-filter
SECURE_BROWSER_XSS_FILTER = True
# https://docs.djangoproject.com/en/dev/ref/settings/#x-frame-options
X_FRAME_OPTIONS = "DENY"
# EMAIL
# ------------------------------------------------------------------------------
"""
host: EMAIL_HOST
port: EMAIL_PORT
username: EMAIL_HOST_USER
password: EMAIL_HOST_PASSWORD
use_tls: EMAIL_USE_TLS
use_ssl: EMAIL_USE_SSL
timeout: EMAIL_TIMEOUT
ssl_keyfile: EMAIL_SSL_KEYFILE
ssl_certfile: EMAIL_SSL_CERTFILE
"""
# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend
EMAIL_BACKEND = env(
"DJANGO_EMAIL_BACKEND",
default="django.core.mail.backends.smtp.EmailBackend",
)
# https://docs.djangoproject.com/en/dev/ref/settings/#email-timeout
EMAIL_TIMEOUT = 5
EMAIL_HOST_PASSWORD = env(
"EMAIL_HOST_PASSWORD",
default="",
)
EMAIL_HOST_USER = env(
"EMAIL_HOST_USER",
default="",
)
EMAIL_USE_SSL = env(
"EMAIL_USE_SSL",
default=False,
)
EMAIL_HOST = env("EMAIL_HOST", default="mailhog")
# https://docs.djangoproject.com/en/dev/ref/settings/#email-port
EMAIL_PORT = env("EMAIL_PORT", default="1025")
EMAIL_FROM = env("EMAIL_FROM", default="noreply@akarpov.ru")
DEFAULT_FROM_EMAIL = env("EMAIL_FROM", default="noreply@akarpov.ru")
SERVER_EMAIL = env("EMAIL_FROM", default="noreply@akarpov.ru")
# ADMIN
# ------------------------------------------------------------------------------
# Django Admin URL.
ADMIN_URL = "admin/"
# https://docs.djangoproject.com/en/dev/ref/settings/#admins
ADMINS = [("""sanspie""", "alexandr.d.karpov@gmail.com")]
# https://docs.djangoproject.com/en/dev/ref/settings/#managers
MANAGERS = ADMINS
# LOGGING
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#logging
# See https://docs.djangoproject.com/en/dev/topics/logging for
# more details on how to customize your logging configuration.
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"json_formatter": {
"()": structlog.stdlib.ProcessorFormatter,
"processor": structlog.processors.JSONRenderer(),
},
"plain_console": {
"()": structlog.stdlib.ProcessorFormatter,
"processor": structlog.dev.ConsoleRenderer(),
},
"key_value": {
"()": structlog.stdlib.ProcessorFormatter,
"processor": structlog.processors.KeyValueRenderer(
key_order=["timestamp", "level", "event", "logger"]
),
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "plain_console",
},
"json_file": {
"class": "logging.handlers.WatchedFileHandler",
"filename": "logs/json.log",
"formatter": "json_formatter",
},
"flat_line_file": {
"class": "logging.handlers.WatchedFileHandler",
"filename": "logs/flat_line.log",
"formatter": "key_value",
},
},
"loggers": {
"django_structlog": {
"handlers": ["console", "flat_line_file", "json_file"],
"level": "INFO",
},
# Make sure to replace the following logger's name for yours
"django_structlog_demo_project": {
"handlers": ["console", "flat_line_file", "json_file"],
"level": "INFO",
},
},
}
structlog.configure(
processors=[
structlog.contextvars.merge_contextvars,
structlog.stdlib.filter_by_level,
structlog.processors.TimeStamper(fmt="iso"),
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.stdlib.PositionalArgumentsFormatter(),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
structlog.processors.UnicodeDecoder(),
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
],
context_class=dict,
logger_factory=structlog.stdlib.LoggerFactory(),
cache_logger_on_first_use=True,
)
# Celery
# ------------------------------------------------------------------------------
if USE_TZ:
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#std:setting-timezone
CELERY_TIMEZONE = TIME_ZONE
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#std:setting-broker_url
CELERY_BROKER_URL = env("CELERY_BROKER_URL")
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#std:setting-result_backend
CELERY_RESULT_BACKEND = CELERY_BROKER_URL
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#result-extended
CELERY_RESULT_EXTENDED = True
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#std:setting-accept_content
CELERY_ACCEPT_CONTENT = ["json"]
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#std:setting-task_serializer
CELERY_TASK_SERIALIZER = "json"
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#std:setting-result_serializer
CELERY_RESULT_SERIALIZER = "json"
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#task-time-limit
CELERY_TASK_TIME_LIMIT = 20 * 60
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#task-soft-time-limit
CELERY_TASK_SOFT_TIME_LIMIT = 10 * 60
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#beat-scheduler
CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler"
CELERY_BEAT_SCHEDULE = {
"update-index-every-hour": {
"task": "akarpov.files.tasks.update_index_task",
"schedule": crontab(minute="0"),
},
}
# django-allauth
# ------------------------------------------------------------------------------
ACCOUNT_ALLOW_REGISTRATION = env.bool("DJANGO_ACCOUNT_ALLOW_REGISTRATION", True)
# https://django-allauth.readthedocs.io/en/latest/configuration.html
ACCOUNT_AUTHENTICATION_METHOD = "username"
# https://django-allauth.readthedocs.io/en/latest/configuration.html
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = True
ACCOUNT_LOGIN_ON_PASSWORD_RESET = True
SOCIALACCOUNT_EMAIL_VERIFICATION = False
# https://django-allauth.readthedocs.io/en/latest/configuration.html
ACCOUNT_EMAIL_VERIFICATION = "mandatory"
# https://django-allauth.readthedocs.io/en/latest/configuration.html
ACCOUNT_ADAPTER = "akarpov.users.adapters.AccountAdapter"
# https://django-allauth.readthedocs.io/en/latest/forms.html
ACCOUNT_FORMS = {"signup": "akarpov.users.forms.UserSignupForm"}
# https://django-allauth.readthedocs.io/en/latest/configuration.html
SOCIALACCOUNT_ADAPTER = "akarpov.users.adapters.SocialAccountAdapter"
# https://django-allauth.readthedocs.io/en/latest/forms.html
SOCIALACCOUNT_FORMS = {"signup": "akarpov.users.forms.UserSocialSignupForm"}
SOCIALACCOUNT_PROVIDERS = {
"github": {
"SCOPE": [
"user",
"read:org",
],
},
"google": {
"SCOPE": [
"profile",
"email",
],
"AUTH_PARAMS": {
"access_type": "online",
},
},
}
ACCOUNT_DEFAULT_HTTP_PROTOCOL = env("HTTP_PROTOCOL", default="http")
# django-rest-framework
# -------------------------------------------------------------------------------
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework.authentication.SessionAuthentication",
"rest_framework.authentication.TokenAuthentication",
),
"DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",),
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
}
CORS_URLS_REGEX = r"^/api/.*$"
SPECTACULAR_SETTINGS = {
"TITLE": "akarpov API",
"SCHEMA_PATH_PREFIX": "/api/v[0-9]",
"DESCRIPTION": "Documentation of API endpoints of akarpov",
"VERSION": "1.0.0",
"SERVE_INCLUDE_SCHEMA": False,
"SERVERS": [
{"url": "http://127.0.0.1:8000", "description": "Local Development server"},
{"url": "https://new.akarpov.ru", "description": "Production server"},
],
}
# CKEDITOR
# ------------------------------------------------------------------------------
CKEDITOR_UPLOAD_PATH = "uploads/"
CKEDITOR_CONFIGS = {
"default": {
"width": "full",
"extra_plugins": [
"autosave",
"autogrow",
"autolink",
"autoembed",
"clipboard",
"dialog",
"dialogui",
],
"autosave": {
"autoLoad": True,
"delay": 60,
"NotOlderThen": 20,
},
},
}
# JAZZMIN
# ------------------------------------------------------------------------------
JAZZMIN_SETTINGS = {
"site_title": "sanspie's site admin",
"site_header": "site admin",
"site_brand": "sanspie's site",
"site_logo": "images/favicons/favicon.ico",
"login_logo": "images/favicons/favicon.ico",
"site_icon": None,
"welcome_sign": "welcum back",
"copyright": "admin on akarpov.ru",
"user_avatar": "image_cropped",
}
# DRF_CHUNKED_UPLOAD
# ------------------------------------------------------------------------------
DRF_CHUNKED_UPLOAD_ABSTRACT_MODEL = False
DRF_CHUNKED_UPLOAD_PATH = "uploads/chucked/"
# SHORTENER
# ------------------------------------------------------------------------------
SHORTENER_ADD_SLUG = True
SHORTENER_SLUG_LENGTH = 3
# let nginx do some magic here
SHORTENER_REDIRECT_TO = "https://akarpov.ru"
SHORTENER_HOST = "https://akarpov.ru"
# ACTIVE_LINK
# ------------------------------------------------------------------------------
ACTIVE_LINK_CSS_CLASS = "nav-active"
# MUSIC
# ------------------------------------------------------------------------------
# MPD
MUSIC_MPD_HOST = env("MPD_HOST", default="")
MUSIC_MPD_PASSWORD = env("MPD_PASSWORD", default="")
# SPOTIFY
MUSIC_SPOTIFY_ID = env("SPOTIFY_ID", default="")
MUSIC_SPOTIFY_SECRET = env("SPOTIFY_SECRET", default="")
# YANDEX_MUSIC
MUSIC_YANDEX_TOKEN = env("YANDEX_TOKEN", default="")
# ROBOTS
# ------------------------------------------------------------------------------
ROBOTS_USE_SITEMAP = True
ROBOTS_USE_SCHEME_IN_HOST = True
# LOCATION_FIELD
# ------------------------------------------------------------------------------
LOCATION_FIELD = {
"map.provider": "openstreetmap",
"search.provider": "nominatim",
}
# SENTRY
# ------------------------------------------------------------------------------
dsn = env("SENTRY_DSN", default="")
if dsn:
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
sentry_sdk.init(
dsn=dsn,
traces_sample_rate=1.0,
integrations=[
DjangoIntegration(
transaction_style="url",
middleware_spans=True,
signals_spans=True,
cache_spans=True,
),
CeleryIntegration(monitor_beat_tasks=True, propagate_traces=True),
],
)
# ELASTICSEARCH
# ------------------------------------------------------------------------------
ELASTICSEARCH_DSL = {
"default": {"hosts": env("ELASTIC_SEARCH", default="http://127.0.0.1:9200/")},
}
USE_DEBUG_TOOLBAR = False