schon/schon/settings/base.py
Egor fureunoir Gorbunov eef774c3a3 feat(markdown): integrate markdown rendering and editor support
Replace WYSIWYG editor with Markdown editor across all relevant models and admin fields. Add utilities for rendering and stripping markdown. Adjust serializers, views, and templates to support markdown content. Introduce `PastedImage` model and upload endpoint for handling inline image uploads in markdown.

This change simplifies content formatting while enhancing flexibility with markdown support.
2026-02-27 23:36:51 +03:00

549 lines
15 KiB
Python

import logging
from datetime import datetime
from os import getenv, name
from pathlib import Path
from typing import Any
from django.core.exceptions import ImproperlyConfigured
SCHON_VERSION = "2026.1"
RELEASE_DATE = datetime(2026, 1, 5)
PROJECT_NAME = getenv("SCHON_PROJECT_NAME", "Schon")
TASKBOARD_URL = getenv(
"SCHON_TASKBOARD_URL",
"https://plane.wiseless.xyz/spaces/issues/dd33cb0ab9b04ef08a10f7eefae6d90c/?board=kanban",
)
SUPPORT_CONTACT = getenv("SCHON_SUPPORT_CONTACT", "https://t.me/fureunoir")
BASE_DIR: Path = Path(__file__).resolve().parent.parent.parent
INITIALIZED: bool = (BASE_DIR / ".initialized").exists()
SECRET_KEY: str = getenv("SECRET_KEY", "SUPER_SECRET_KEY")
DEBUG: bool = bool(int(getenv("DEBUG", "1")))
DEBUG_DATABASE: bool = bool(int(getenv("DEBUG_DATABASE", "0")))
DEBUG_CELERY: bool = bool(int(getenv("DEBUG_DATABASE", "0")))
BASE_DOMAIN: str = getenv("SCHON_BASE_DOMAIN", "localhost")
STOREFRONT_DOMAIN: str = getenv("SCHON_STOREFRONT_DOMAIN", "localhost")
ALLOW_MESSAGING: bool = bool(int(getenv("SCHON_ALLOW_MESSAGING", "0")))
ALLOWED_HOSTS: set[str] = {
"app",
"worker",
"beat",
"localhost",
"127.0.0.1",
}
if DEBUG:
ALLOWED_HOSTS.add("*")
else:
for entry in getenv("ALLOWED_HOSTS", "").split(" "):
ALLOWED_HOSTS.add(entry)
ALLOWED_HOSTS: tuple[str, ...] = tuple(ALLOWED_HOSTS)
CSRF_TRUSTED_ORIGINS: set[str] = {
"http://127.0.0.1",
"http://localhost",
}
for entry in getenv("CSRF_TRUSTED_ORIGINS", "").split(" "):
CSRF_TRUSTED_ORIGINS.add(entry)
CSRF_TRUSTED_ORIGINS: tuple[str, ...] = tuple(CSRF_TRUSTED_ORIGINS)
if DEBUG:
CORS_ALLOW_ALL_ORIGINS = True
else:
CORS_ALLOWED_ORIGINS: set[str] = {
"http://127.0.0.1",
"http://localhost",
}
for entry in getenv("CORS_ALLOWED_ORIGINS", "").split(" "):
CORS_ALLOWED_ORIGINS.add(entry)
CORS_ALLOWED_ORIGINS: tuple[str, ...] = tuple(CORS_ALLOWED_ORIGINS)
CORS_ALLOW_METHODS = (
"DELETE",
"GET",
"OPTIONS",
"PATCH",
"POST",
"PUT",
)
CORS_ALLOW_HEADERS = (
"accept",
"accept-encoding",
"accept-language",
"authorization",
"baggage",
"connection",
"content-type",
"dnt",
"host",
"origin",
"referer",
"sec-fetch-dest",
"sec-fetch-mode",
"sec-fetch-site",
"sec-gpc",
"sentry-trace",
"user-agent",
"x-csrftoken",
"x-schon-auth",
"x-requested-with",
)
USE_X_FORWARDED_HOST = True
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") if not DEBUG else None
X_FRAME_OPTIONS = "SAMEORIGIN"
UNSAFE_CACHE_KEYS: list[str] = []
SITE_ID: int = 1
INSTALLED_APPS: list[str] = [
"unfold",
"unfold.contrib.filters",
"unfold.contrib.forms",
"unfold.contrib.inlines",
"unfold.contrib.constance",
"unfold.contrib.import_export",
"unfold_markdown",
"constance",
"modeltranslation",
"django.contrib.admin",
"django.contrib.admindocs",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"django.contrib.sitemaps",
"django.contrib.gis",
"django.contrib.humanize",
"health_check",
"cacheops",
"django_celery_beat",
"django_celery_results",
"django_extensions",
"django_redis",
"widget_tweaks",
"mptt",
"rest_framework",
"rest_framework_simplejwt",
"rest_framework_simplejwt.token_blacklist",
"drf_spectacular_websocket",
"drf_spectacular",
"drf_spectacular_sidecar",
"django_json_widget",
"django_elasticsearch_dsl",
"djangoql",
"import_export",
"dbbackup",
"corsheaders",
"constance.backends.database",
"graphene_django",
"channels",
"engine.core",
"engine.payments",
"engine.vibes_auth",
"engine.blog",
]
if DEBUG:
wn_app_index = INSTALLED_APPS.index("django.contrib.staticfiles") - 1
INSTALLED_APPS.insert(wn_app_index, "whitenoise.runserver_nostatic")
INSTALLED_APPS.append("debug_toolbar")
MIDDLEWARE: list[str] = [
"schon.middleware.BlockInvalidHostMiddleware",
"schon.middleware.RateLimitMiddleware",
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"corsheaders.middleware.CorsMiddleware",
"schon.middleware.CustomCommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.contrib.admindocs.middleware.XViewMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"schon.middleware.CustomLocaleMiddleware",
"schon.middleware.CamelCaseMiddleWare",
]
if DEBUG:
MIDDLEWARE.insert(
MIDDLEWARE.index("django.contrib.sessions.middleware.SessionMiddleware"),
"debug_toolbar.middleware.DebugToolbarMiddleware",
)
TEMPLATES: list[
dict[str, str | list[str | Path] | dict[str, str | list[str]] | Path | bool]
] = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [
BASE_DIR / "engine/vibes_auth/templates",
BASE_DIR / "engine/core/templates",
BASE_DIR / "engine/payments/templates",
],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
USE_I18N: bool = True
LOCALE_PATHS: tuple[Path, ...] = (
(BASE_DIR / "schon/locale"),
(BASE_DIR / "engine/blog/locale"),
(BASE_DIR / "engine/core/locale"),
(BASE_DIR / "engine/payments/locale"),
(BASE_DIR / "engine/vibes_auth/locale"),
)
LANGUAGES: tuple[tuple[str, str], ...] = (
("ar-ar", "العربية"),
("cs-cz", "Česky"),
("da-dk", "Dansk"),
("de-de", "Deutsch"),
("en-gb", "English (British)"),
("en-us", "English (American)"),
("es-es", "Español"),
("fa-ir", "فارسی"),
("fr-fr", "Français"),
("he-il", "עברית"),
("hi-in", "हिंदी"),
("hr-hr", "Hrvatski"),
("id-id", "Bahasa Indonesia"),
("it-it", "Italiano"),
("ja-jp", "日本語"),
("kk-kz", "Қазақ"),
("ko-kr", "한국어"),
("nl-nl", "Nederlands"),
("no-no", "Norsk"),
("pl-pl", "Polska"),
("pt-br", "Português"),
("ro-ro", "Română"),
("ru-ru", "Русский"),
("sv-se", "Svenska"),
("th-th", "ไทย"),
("tr-tr", "Türkçe"),
("vi-vn", "Tiếng Việt"),
("zh-hans", "简体中文"),
)
LANGUAGE_CODE: str = getenv("SCHON_LANGUAGE_CODE", "en-gb")
LANGUAGES_FLAGS: dict[str, str] = {
"ar-ar": "🇸🇦",
"cs-cz": "🇨🇿",
"da-dk": "🇩🇰",
"de-de": "🇩🇪",
"en-gb": "🇬🇧",
"en-us": "🇺🇸",
"es-es": "🇪🇸",
"fa-ir": "🇮🇷",
"fr-fr": "🇫🇷",
"he-il": "🇮🇱",
"hi-in": "🇮🇳",
"hr-hr": "🇭🇷",
"id-id": "🇮🇩",
"it-it": "🇮🇹",
"ja-jp": "🇯🇵",
"kk-kz": "🇰🇿",
"ko-kr": "🇰🇷",
"nl-nl": "🇳🇱",
"no-no": "🇳🇴",
"pl-pl": "🇵🇱",
"pt-br": "🇧🇷",
"ro-ro": "🇷🇴",
"ru-ru": "🇷🇺",
"sv-se": "🇸🇪",
"th-th": "🇹🇭",
"tr-tr": "🇹🇷",
"vi-vn": "🇻🇳",
"zh-hans": "🇨🇳",
}
CURRENCIES_BY_LANGUAGES: tuple[tuple[str, str], ...] = (
("ar-ar", "AED"),
("cs-cz", "CZK"),
("da-dk", "DKK"),
("de-de", "EUR"),
("en-gb", "GBP"),
("en-us", "USD"),
("es-es", "EUR"),
("fa-ir", "IRR"),
("fr-fr", "EUR"),
("he-il", "ILS"),
("hi-in", "INR"),
("hr-hr", "EUR"),
("id-id", "IDR"),
("it-it", "EUR"),
("ja-jp", "JPY"),
("kk-kz", "KZT"),
("ko-kr", "KRW"),
("nl-nl", "EUR"),
("no-no", "NOK"),
("pl-pl", "PLN"),
("pt-br", "BRL"),
("ro-ro", "RON"),
("ru-ru", "RUB"),
("sv-se", "SEK"),
("th-th", "THB"),
("tr-tr", "TRY"),
("vi-vn", "VND"),
("zh-hans", "CNY"),
)
CURRENCIES_WITH_SYMBOLS: tuple[tuple[str, str], ...] = (
("AED", "د.إ"),
("BRL", "R$"),
("CNY", "¥"),
("CZK", ""),
("DKK", "kr"),
("EUR", ""),
("GBP", "£"),
("IDR", "Rp"),
("ILS", ""),
("INR", ""),
("IRR", ""),
("JPY", "¥"),
("KRW", ""),
("KZT", ""),
("NOK", "kr"),
("PLN", ""),
("RON", "lei"),
("RUB", ""),
("SEK", "kr"),
("THB", "฿"),
("TRY", ""),
("USD", "$"),
("VND", ""),
)
LANGUAGE_URL_OVERRIDES: dict[str, str] = {
code.split("-")[0]: code for code, _ in LANGUAGES if "-" in code
}
CURRENCY_CODE: str = dict(CURRENCIES_BY_LANGUAGES).get(LANGUAGE_CODE, "")
MODELTRANSLATION_FALLBACK_LANGUAGES: tuple[str, ...] = (LANGUAGE_CODE, "en-us", "de-de")
ROOT_URLCONF: str = "schon.urls"
WSGI_APPLICATION: str = "schon.wsgi.application"
ASGI_APPLICATION: str = "schon.asgi.application"
DEFAULT_AUTO_FIELD: str = "django.db.models.BigAutoField"
TIME_ZONE: str = getenv("TIME_ZONE", "Europe/London")
WHITENOISE_MANIFEST_STRICT: bool = False
STATIC_URL: str = f"https://api.{BASE_DOMAIN}/static/" if INITIALIZED else "/static/"
STATIC_ROOT: Path = BASE_DIR / "static"
MEDIA_URL: str = f"https://api.{BASE_DOMAIN}/media/" if INITIALIZED else "/media/"
MEDIA_ROOT: Path = BASE_DIR / "media"
AUTH_USER_MODEL: str = "vibes_auth.User"
AUTH_PASSWORD_VALIDATORS: list[dict[str, str | int]] = [
{
"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",
},
]
APPEND_SLASH: bool = True
REDIS_PASSWORD: str = getenv("REDIS_PASSWORD", default="")
REDIS_URL: str = f"redis://:{REDIS_PASSWORD}@redis:6379/0"
CHANNEL_LAYERS: dict[str, dict[str, str | dict[str, list[str]]]] = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [getenv("CHANNEL_REDIS_URL", REDIS_URL)],
},
},
}
INTERNAL_IPS: list[str] = [
"127.0.0.1",
]
if DEBUG:
import socket
# Docker: resolve container's gateway IP so debug toolbar works
try:
_, _, ips = socket.gethostbyname_ex(socket.gethostname())
INTERNAL_IPS += [".".join(ip.split(".")[:-1] + ["1"]) for ip in ips]
except socket.gaierror:
pass
if getenv("SENTRY_DSN"):
import sentry_sdk
from sentry_sdk.integrations.celery import CeleryIntegration
from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.integrations.logging import LoggingIntegration
from sentry_sdk.integrations.redis import RedisIntegration
from sentry_sdk.types import Event, Hint
def scrub_sensitive(
data: dict[str, Any] | list[Any] | str,
) -> dict[str, Any] | list[Any] | str | None:
if isinstance(data, dict):
# noinspection PyShadowingNames
cleaned: dict[str, Any] = {}
for key, value in data.items():
if key.lower() in ("password", "confirm_password"):
cleaned[key] = "[FILTERED]"
else:
cleaned[key] = scrub_sensitive(value)
return cleaned
if isinstance(data, list):
return [scrub_sensitive(item) for item in data]
return data
def before_send(event: Event, hint: Hint) -> Event:
if hint:
pass
request = event.get("request", {})
data = request.get("data", {})
if data:
request["data"] = scrub_sensitive(data)
event["request"] = request
return event
ignore_errors: list[str] = []
sentry_sdk.init(
dsn=getenv("SENTRY_DSN"),
integrations=[
DjangoIntegration(),
LoggingIntegration(level=logging.INFO, event_level=logging.ERROR),
CeleryIntegration(),
RedisIntegration(),
],
environment="development" if DEBUG else "production",
release=f"schon@{SCHON_VERSION}",
traces_sample_rate=1.0 if DEBUG else 0.2,
profiles_sample_rate=1.0 if DEBUG else 0.1,
max_request_body_size="always",
before_send=before_send,
ignore_errors=ignore_errors,
debug=False,
)
SESSION_COOKIE_HTTPONLY: bool = True
CSRF_COOKIE_HTTPONLY: bool = True
LANGUAGE_COOKIE_HTTPONLY: bool = True
DATA_UPLOAD_MAX_NUMBER_FIELDS: int = 8888
RATELIMIT_EXCEPTION_CLASS: str = "schon.utils.misc.RatelimitedError"
ADMINS: list[tuple[str, ...]] = [("Egor Gorbunov", "contact@fureunoir.com")]
STORAGES: dict[str, Any] = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedStaticFilesStorage"
if DEBUG
else "whitenoise.storage.CompressedManifestStaticFilesStorage"
},
}
if getenv("DBBACKUP_HOST") and getenv("DBBACKUP_USER") and getenv("DBBACKUP_PASS"):
dbbackup_server_type = getenv("DBBACKUP_TYPE", "sftp")
project_name = (
getenv("SCHON_PROJECT_NAME", "schon_common").lower().replace(" ", "_")
)
raw_path = getenv("DBBACKUP_PATH", f"/backups/{project_name}/")
cleaned = raw_path.strip("/")
remote_dir = f"{cleaned}/"
match dbbackup_server_type:
case "sftp":
STORAGES.update(
{
"dbbackup": {
"BACKEND": "storages.backends.sftpstorage.SFTPStorage",
"OPTIONS": {
"host": getenv("DBBACKUP_HOST"),
"root_path": f"/{remote_dir}",
"params": {
"username": getenv("DBBACKUP_USER"),
"password": getenv("DBBACKUP_PASS"),
"allow_agent": False,
"look_for_keys": False,
},
"interactive": False,
"file_mode": 0o600,
"dir_mode": 0o700,
},
}
}
)
case "ftp":
STORAGES.update(
{
"dbbackup": {
"BACKEND": "storages.backends.ftp.FTPStorage",
"OPTIONS": {
"location": (
f"ftp://{getenv('DBBACKUP_USER')}:{getenv('DBBACKUP_PASS')}@{getenv('DBBACKUP_HOST')}:21/{raw_path}"
),
},
}
}
)
case _:
raise ImproperlyConfigured(f"Invalid DBBACKUP_TYPE: {dbbackup_server_type}")
else:
STORAGES.update(
{
"dbbackup": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
"OPTIONS": {
"location": "/app/backups/",
},
}
}
)
if name == "nt":
GDAL_LIBRARY_PATH = r"C:\OSGeo4W\bin\gdal311.dll"
GEOS_LIBRARY_PATH = r"C:\OSGeo4W\bin\geos_c.dll"