integreat_cms/core/settings.py
"""
Django settings for ``integreat-cms``.
This file only contains the options which deviate from the default values.
For the full list of settings and their values, see :doc:`django:ref/settings`.
For production use, some of the settings can be set with environment variables
(use the prefix ``INTEGREAT_CMS_``) or via the config file `/etc/integreat-cms.ini`.
See :doc:`/prod-server` for details.
"""
from __future__ import annotations
import os
import sys
from typing import TYPE_CHECKING
from urllib.parse import urlparse
from django.contrib.messages.constants import SUCCESS
from django.core.exceptions import ImproperlyConfigured
from django.utils.translation import gettext_lazy as _
from ..nominatim_api.utils import BoundingBox
from .logging_formatter import ColorFormatter, RequestFormatter
from .utils.strtobool import strtobool
if TYPE_CHECKING:
from typing import Any, Final
from django.utils.functional import Promise
###################
# CUSTOM SETTINGS #
###################
#: Build paths inside the project like this: ``os.path.join(BASE_DIR, ...)``
BASE_DIR: Final[str] = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
#: The URL to our webapp. This is used for urls in the ``sitemap.xml``
#: (see :mod:`~integreat_cms.sitemap` for more information).
WEBAPP_URL: Final[str] = os.environ.get(
"INTEGREAT_CMS_WEBAPP_URL", "https://integreat.app"
)
#: The URL to a domain that handles short links.
#: This is currently the same as `BASE_URL` but will change in the future.
SHORT_LINKS_URL: Final[str] = os.environ.get(
"INTEGREAT_CMS_SHORT_LINKS_URL", "http://localhost:8000"
)
#: The URL to the Matomo statistics server.
MATOMO_URL: Final[str] = os.environ.get(
"INTEGREAT_CMS_MATOMO_URL", "https://statistics.integreat-app.de"
)
#: Enable tracking of API requests in Matomo
MATOMO_TRACKING: Final[bool] = bool(
strtobool(os.environ.get("INTEGREAT_CMS_MATOMO_TRACKING", "False"))
)
#: The slug for the legal notice (see e.g. :class:`~integreat_cms.cms.models.pages.imprint_page_translation.ImprintPageTranslation`)
IMPRINT_SLUG: Final[str] = os.environ.get("INTEGREAT_CMS_IMPRINT_SLUG", "disclaimer")
#: The slug of the region "Testumgebung" - prevent sending PNs to actual users in development in
#: :func:`~integreat_cms.firebase_api.firebase_api_client.FirebaseApiClient.send_pn`
TEST_REGION_SLUG: Final[str] = "testumgebung"
#: Reserved region slugs, that are used in the webapp
RESERVED_REGION_SLUGS: Final[list[str]] = [
"landing",
"recommend",
"licenses",
"main-disclaimer",
"jpal",
"not-found",
"consent",
]
#: Reserved region page patterns
RESERVED_REGION_PAGE_PATTERNS: Final[list[str]] = [
IMPRINT_SLUG,
"news",
"events",
"locations",
"offers",
"search",
]
#: URL to the Integreat Website
WEBSITE_URL: Final[str] = os.environ.get(
"INTEGREAT_CMS_WEBSITE_URL", "https://integreat-app.de"
)
#: An alias of :attr:`~integreat_cms.core.settings.WEBAPP_URL`.
#: Used by `django-linkcheck <https://github.com/DjangoAdminHackers/django-linkcheck#site_domain-and-linkcheck_site_domains>`_
#: to determine whether a link is internal.
SITE_DOMAIN: Final[str] = WEBAPP_URL
#: URLs to the Integreat blog
BLOG_URLS: Final[dict[str, str]] = {
"en": f"{WEBSITE_URL}/en/blog/",
"de": f"{WEBSITE_URL}/blog/",
}
#: The blog URL to use when the blog is not available in the requested language
DEFAULT_BLOG_URL: Final[str] = BLOG_URLS["en"]
#: URL to the Integreat wiki
WIKI_URL: Final[str] = os.environ.get(
"INTEGREAT_CMS_WIKI_URL", "https://wiki.integreat-app.de"
)
#: RSS feed URLs to the Integreat blog
RSS_FEED_URLS: Final[dict[str, str]] = {
"en": f"{WEBSITE_URL}/en/feed/",
"de": f"{WEBSITE_URL}/feed/",
}
#: The RSS feed URL to use when the feed is not available in the requested language
DEFAULT_RSS_FEED_URL: Final[str] = RSS_FEED_URLS["en"]
#: How many days of chat history should be shown
AUTHOR_CHAT_HISTORY_DAYS: Final[int] = 30
#: The time span up to which recurrent events should be returned by the api
API_EVENTS_MAX_TIME_SPAN_DAYS: Final[int] = 31
#: The maximum duration of an event
MAX_EVENT_DURATION: Final[int] = int(
os.environ.get("INTEGREAT_CMS_MAX_EVENT_DURATION", 28)
)
#: The company operating this CMS
COMPANY: Final[str] = os.environ.get(
"INTEGREAT_CMS_COMPANY", "Tür an Tür – Digitalfabrik gGmbH"
)
#: The URL to the company's website
COMPANY_URL: Final[str] = os.environ.get(
"INTEGREAT_CMS_COMPANY_URL", "https://tuerantuer.de/digitalfabrik/"
)
#: The available inbuilt brandings of the CMS
AVAILABLE_BRANDINGS: Final[dict[str, str]] = {
"integreat": "Integreat",
"malte": "MALTE",
"aschaffenburg": "hallo aschaffenburg",
"netzwerk-obdach": "Netzwerk Obdach & Wohnen",
}
#: The branding of the CMS
BRANDING: Final[str] = os.environ.get("INTEGREAT_CMS_BRANDING", "integreat")
# pylint: disable=consider-using-assignment-expr
if BRANDING not in AVAILABLE_BRANDINGS:
raise ImproperlyConfigured(
f"The branding {BRANDING!r} is not supported, must be one of {list(AVAILABLE_BRANDINGS)}."
)
#: The readable title of the branding
BRANDING_TITLE: Final[str] = AVAILABLE_BRANDINGS[BRANDING]
#: Social media preview image
SOCIAL_PREVIEW_IMAGE: Final[str] = os.environ.get(
"INTEGREAT_SOCIAL_PREVIEW_IMAGE",
f"static/logos/{BRANDING}/social-media-preview.png",
)
#: The default bounding box for regions with indistinct borders
DEFAULT_BOUNDING_BOX: Final[BoundingBox] = BoundingBox(
latitude_min=47.3024876979,
latitude_max=54.983104153,
longitude_min=5.98865807458,
longitude_max=15.0169958839,
)
#: The default timeout in seconds for retrieving external APIs etc.
DEFAULT_REQUEST_TIMEOUT: Final[int] = int(
os.environ.get("INTEGREAT_CMS_DEFAULT_REQUEST_TIMEOUT", 10)
)
#: Where release notes are stored
RELEASE_NOTES_DIRS: Final[str] = os.path.join(BASE_DIR, "release_notes")
#: Custom path for additional local translation files
CUSTOM_LOCALE_PATH: Final[str] = os.environ.get(
"INTEGREAT_CMS_CUSTOM_LOCALE_PATH", "/etc/integreat-cms/locale"
)
#: The number of regions that are available via the dropdown
NUM_REGIONS_QUICK_ACCESS: Final[int] = 15
BACKGROUND_TASKS_ENABLED = bool(
strtobool(os.environ.get("INTEGREAT_CMS_BACKGROUND_TASKS_ENABLED", "True"))
)
#: The tag that events from external calendars need to get imported
EXTERNAL_CALENDAR_CATEGORY: Final[str] = BRANDING
##############################################################
# Firebase Push Notifications (Firebase Cloud Messaging FCM) #
##############################################################
#: FCM API Url
FCM_URL: Final[str] = os.environ.get(
"INTEGREAT_CMS_FCM_URL",
"https://fcm.googleapis.com/v1/projects/integreat-2020/messages:send",
)
#: FCM Data API Url
FCM_DATA_URL: Final[str] = os.environ.get(
"INTEGREAT_CMS_FCM_DATA_URL",
"https://fcmdata.googleapis.com/v1beta1/projects/integreat-2020/androidApps/1:164298278764:android:3fc1f67f3883df306fd549/deliveryData",
)
#: Path to the saved credential json file
FCM_CREDENTIALS: str | None = os.environ.get("INTEGREAT_CMS_FCM_CREDENTIALS")
#: Whether push notifications via Firebase are enabled.
#: This is ``True`` if :attr:`~integreat_cms.core.settings.FCM_CREDENTIALS` is set, ``False`` otherwise.
FCM_ENABLED: bool = bool(FCM_CREDENTIALS)
#: The available push notification channels
FCM_CHANNELS: Final[tuple[tuple[str, Promise], ...]] = (("news", _("News")),)
#: How many days push notifications are shown in the apps
FCM_HISTORY_DAYS: Final[int] = 28
#: The interval at which scheduled push notifications are sent out
#: Must be <= 60 and a divisor of 60
FCM_SCHEDULE_INTERVAL_MINUTES: Final[int] = int(
os.environ.get("INTEGREAT_CMS_FCM_SCHEDULE_INTERVAL_MINUTES", 60)
)
assert (
not 60 % FCM_SCHEDULE_INTERVAL_MINUTES
), "Interval must be <= 60 and a divisor of 60"
#: Duration (in hours) that we retain pending push notifications for retry attempts before discarding them
FCM_NOTIFICATION_RETAIN_TIME_IN_HOURS: Final[int] = int(
os.environ.get("INTEGREAT_CMS_NOTIFICATION_RETAIN_TIME_IN_HOURS", 24)
)
###########
# GVZ API #
###########
#: Whether or not the GVZ (Gemeindeverzeichnis) API is enabled. This is used to automatically import coordinates and
#: region aliases (see :mod:`~integreat_cms.gvz_api` for more information).
GVZ_API_ENABLED: Final[bool] = True
#: The URL to our GVZ (Gemeindeverzeichnis) API. This is used to automatically import coordinates and region aliases
#: (see :mod:`~integreat_cms.gvz_api` for more information).
GVZ_API_URL: Final[str] = "https://gvz.integreat-app.de"
#################
# Nominatim API #
#################
#: Whether or not the Nominatim API for OpenStreetMap queries is enabled.
#: This is used to automatically derive coordinates from addresses.
NOMINATIM_API_ENABLED: Final[bool] = bool(
strtobool(os.environ.get("INTEGREAT_CMS_NOMINATIM_API_ENABLED", "True"))
)
#: The URL to our Nominatim API.
#: This is used to automatically derive coordinates from addresses.
NOMINATIM_API_URL: Final[str] = os.environ.get(
"INTEGREAT_CMS_NOMINATIM_API_URL", "http://nominatim.maps.tuerantuer.org/nominatim/"
)
###############
# TEXTLAB API #
###############
#: URL to the Textlab API
TEXTLAB_API_URL: Final[str] = os.environ.get(
"INTEGREAT_CMS_TEXTLAB_API_URL", "https://textlab.online/api/"
)
#: Key for the Textlab API
TEXTLAB_API_KEY: str | None = os.environ.get("INTEGREAT_CMS_TEXTLAB_API_KEY")
#: Whether the Textlab API is enabled.
#: This is ``True`` if :attr:`~integreat_cms.core.settings.TEXTLAB_API_KEY` is set, ``False`` otherwise.
TEXTLAB_API_ENABLED: bool = bool(TEXTLAB_API_KEY)
#: Username for the Textlab API
TEXTLAB_API_USERNAME: Final[str] = os.environ.get(
"INTEGREAT_CMS_TEXTLAB_API_USERNAME", "Integreat"
)
#: Which language slugs are allowed for the Textlab API
TEXTLAB_API_LANGUAGES: Final[list[str]] = ["de"]
#: Which content types are enabled for the Textlab API
TEXTLAB_API_CONTENT_TYPES: Final[list[str]] = ["pagetranslation"]
#: How many seconds we should wait between the requests in the bulk management command
TEXTLAB_API_BULK_WAITING_TIME: Final[float] = float(
os.environ.get("INTEGREAT_CMS_TEXTLAB_API_BULK_WAITING_TIME", 0.5)
)
#: How many seconds we should wait after finishing a region in the bulk management command
TEXTLAB_API_BULK_COOL_DOWN_PERIOD: Final[float] = float(
os.environ.get("INTEGREAT_CMS_TEXTLAB_API_BULK_COOL_DOWN_PERIOD", 60)
)
#: Which text type / benchmark id to default to
TEXTLAB_API_DEFAULT_BENCHMARK_ID: Final[int] = int(
os.environ.get("INTEGREAT_CMS_TEXTLAB_API_DEFAULT_BENCHMARK_ID", 420)
)
#: The minimum HIX score required for machine translation
HIX_REQUIRED_FOR_MT: Final[float] = float(
os.environ.get("INTEGREAT_CMS_HIX_REQUIRED_FOR_MT", 15.0)
)
############
# WEBAUTHN #
############
#: Needed for `webauthn <https://pypi.org/project/webauthn/>`__
#: (this is a setting in case the application runs behind a proxy).
#: Used in the following views:
#:
#: - :class:`~integreat_cms.cms.views.settings.webauthn.register_user_fido_key_view.RegisterUserFidoKeyView`
#: - :class:`~integreat_cms.cms.views.authentication.webauthn.webauthn_verify_view.WebAuthnVerifyView`
BASE_URL: Final[str] = os.environ.get("INTEGREAT_CMS_BASE_URL", "http://localhost:8000")
#: Needed for `webauthn <https://pypi.org/project/webauthn/>`__
#: (this is a setting in case the application runs behind a proxy).
#: Used in the following views:
#:
#: - :class:`~integreat_cms.cms.views.settings.webauthn.get_mfa_challenge_view.GetMfaChallengeView`
#: - :class:`~integreat_cms.cms.views.settings.webauthn.register_user_fido_key_view.RegisterUserFidoKeyView`
#: - :class:`~integreat_cms.cms.views.authentication.webauthn.webauthn_assert_view.WebAuthnAssertView`
#: - :class:`~integreat_cms.cms.views.authentication.webauthn.webauthn_verify_view.WebAuthnVerifyView`
HOSTNAME: Final[str | None] = urlparse(BASE_URL).hostname
if not HOSTNAME:
raise ImproperlyConfigured(f"The base URL {BASE_URL!r} is is invalid.")
########################
# DJANGO CORE SETTINGS #
########################
#: A boolean that turns on/off debug mode (see :setting:`django:DEBUG`)
#:
#: .. warning::
#: Never deploy a site into production with :setting:`DEBUG` turned on!
DEBUG: Final[bool] = bool(strtobool(os.environ.get("INTEGREAT_CMS_DEBUG", "False")))
#: Whether this is a testing system (even if DEBUG is off)
TEST: Final[bool] = bool(strtobool(os.environ.get("INTEGREAT_CMS_TEST", "False")))
#: Enabled applications (see :setting:`django:INSTALLED_APPS`)
INSTALLED_APPS: Final[list[str]] = [
# Installed custom apps
"integreat_cms.cms",
"integreat_cms.core",
"integreat_cms.deepl_api",
"integreat_cms.google_translate_api",
"integreat_cms.firebase_api",
"integreat_cms.gvz_api",
"integreat_cms.matomo_api",
"integreat_cms.nominatim_api",
"integreat_cms.summ_ai_api",
"integreat_cms.textlab_api",
# Installed Django apps
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.humanize",
"django.contrib.messages",
"django.contrib.sessions",
"django.contrib.sitemaps",
"django.contrib.staticfiles",
# Installed third-party-apps
"corsheaders",
"db_mutex",
"import_export",
"linkcheck",
"polymorphic",
"rules.apps.AutodiscoverRulesConfig",
"treebeard",
"webpack_loader",
"widget_tweaks",
]
#: Check whether redis is activated
REDIS_CACHE: Final[bool] = bool(
strtobool(os.environ.get("INTEGREAT_CMS_REDIS_CACHE", "False"))
)
# Install cacheops only if redis cache is available
if REDIS_CACHE:
INSTALLED_APPS.append("cacheops")
# The default Django Admin application and debug toolbar will only be activated if the system is in debug mode.
if DEBUG:
INSTALLED_APPS.append("django.contrib.admin")
# Comment out the following line if you want to disable the Django debug toolbar
INSTALLED_APPS.append("debug_toolbar")
#: Activated middlewares (see :setting:`django:MIDDLEWARE`)
MIDDLEWARE: list[str] = [
"django.middleware.gzip.GZipMiddleware",
"corsheaders.middleware.CorsMiddleware",
"django.middleware.security.SecurityMiddleware",
"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.clickjacking.XFrameOptionsMiddleware",
"integreat_cms.core.middleware.RegionMiddleware",
"integreat_cms.core.middleware.AccessControlMiddleware",
"integreat_cms.core.middleware.TimezoneMiddleware",
]
# The Django debug toolbar middleware will only be activated if the debug_toolbar app is installed
if "debug_toolbar" in INSTALLED_APPS:
# The debug toolbar middleware should be put first (see :doc:`django-debug-toolbar:installation`)
MIDDLEWARE.insert(1, "debug_toolbar.middleware.DebugToolbarMiddleware")
MIDDLEWARE.append("integreat_cms.api.middleware.JsonDebugToolbarMiddleware")
#: Default URL dispatcher (see :setting:`django:ROOT_URLCONF`)
ROOT_URLCONF: Final[str] = "integreat_cms.core.urls"
#: Config for HTML templates (see :setting:`django:TEMPLATES`)
TEMPLATES: Final[list[dict[str, str | list | bool | dict[str, list[str] | bool]]]] = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [os.path.join(BASE_DIR, "api/v3/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",
"integreat_cms.core.context_processors.version_processor",
"integreat_cms.core.context_processors.settings_processor",
"integreat_cms.core.context_processors.constants_processor",
],
"debug": DEBUG,
},
},
]
#: WSGI (Web Server Gateway Interface) config (see :setting:`django:WSGI_APPLICATION`)
WSGI_APPLICATION: Final[str] = "integreat_cms.core.wsgi.application"
############
# DATABASE #
############
#: A dictionary containing the settings for all databases to be used with this Django installation
#: (see :setting:`django:DATABASES`)
DATABASES: dict[str, dict[str, str]] = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ.get("INTEGREAT_CMS_DB_NAME", "integreat"),
"USER": os.environ.get("INTEGREAT_CMS_DB_USER", "integreat"),
"PASSWORD": os.environ.get(
"INTEGREAT_CMS_DB_PASSWORD", "password" if DEBUG else ""
),
"HOST": os.environ.get("INTEGREAT_CMS_DB_HOST", "localhost"),
"PORT": os.environ.get("INTEGREAT_CMS_DB_PORT", "5432"),
}
}
#: Default primary key field type to use for models that don’t have a field with
#: :attr:`primary_key=True <django.db.models.Field.primary_key>`. (see :setting:`django:DEFAULT_AUTO_FIELD`)
DEFAULT_AUTO_FIELD: Final[str] = "django.db.models.BigAutoField"
############
# SECURITY #
############
#: This is a security measure to prevent HTTP Host header attacks, which are possible even under many seemingly-safe
#: web server configurations (see :setting:`django:ALLOWED_HOSTS` and :ref:`django:host-headers-virtual-hosting`)
ALLOWED_HOSTS: Final[list[str]] = [HOSTNAME, ".localhost", "127.0.0.1", "[::1]"] + [
x.strip()
for x in os.environ.get("INTEGREAT_CMS_ALLOWED_HOSTS", "").splitlines()
if x
]
#: A list of IP addresses, as strings, that allow the :func:`~django.template.context_processors.debug` context
#: processor to add some variables to the template context.
INTERNAL_IPS: Final[list[str]] = ["localhost", "127.0.0.1"]
#: The secret key for this particular Django installation (see :setting:`django:SECRET_KEY`)
#:
#: .. warning::
#: Provide a key via the environment variable ``INTEGREAT_CMS_SECRET_KEY`` in production and keep it secret!
SECRET_KEY: str = os.environ.get("INTEGREAT_CMS_SECRET_KEY", "dummy" if DEBUG else "")
#: A dotted path to the view function to be used when an incoming request is rejected by the CSRF protection
#: (see :setting:`django:CSRF_FAILURE_VIEW`)
CSRF_FAILURE_VIEW: Final[str] = "integreat_cms.cms.views.error_handler.csrf_failure"
################
# CORS HEADERS #
################
#: Allow access to all domains by setting the following variable to ``True``
#: (see `django-cors-headers/ <https://pypi.org/project/django-cors-headers/>`__)
CORS_ORIGIN_ALLOW_ALL: Final[bool] = True
#: Extend default headers with development header to differentiate dev traffic in statistics
#: (see `django-cors-headers/ <https://pypi.org/project/django-cors-headers/>`__)
CORS_ALLOW_HEADERS: Final[list[str]] = [
"accept",
"accept-encoding",
"authorization",
"content-type",
"dnt",
"origin",
"user-agent",
"x-csrftoken",
"x-requested-with",
"x-integreat-development",
]
##################
# AUTHENTICATION #
##################
#: The model to use to represent a User (see :setting:`django:AUTH_USER_MODEL` and :ref:`django:auth-custom-user`)
AUTH_USER_MODEL: Final[str] = "cms.User"
#: A list of authentication backend classes (as strings) to use when attempting to authenticate a user
#: (see :setting:`django:AUTHENTICATION_BACKENDS` and :ref:`django:authentication-backends`)
AUTHENTICATION_BACKENDS: Final[tuple[str, ...]] = (
"rules.permissions.ObjectPermissionBackend", # Object-based permission checks
"django.contrib.auth.backends.ModelBackend", # Login via username
"integreat_cms.core.authentication_backends.EmailAuthenticationBackend", # Login via email
)
PASSWORD_HASHERS: Final[list[str]] = [
"django.contrib.auth.hashers.Argon2PasswordHasher",
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
"integreat_cms.cms.auth.WPBCryptPasswordHasher",
]
#: The list of validators that are used to check the strength of user’s passwords
#: (see :setting:`django:AUTH_PASSWORD_VALIDATORS` and :ref:`django:password-validation`)
AUTH_PASSWORD_VALIDATORS: Final[list[dict[str, str]]] = [
{
"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"},
]
#: The URL where requests are redirected for login (see :setting:`django:LOGIN_URL`)
LOGIN_URL: Final[str] = "/login/"
#: The URL where requests are redirected after login (see :setting:`django:LOGIN_REDIRECT_URL`)
LOGIN_REDIRECT_URL: Final[str] = "/"
#: The URL where requests are redirected after logout (see :setting:`django:LOGOUT_REDIRECT_URL`)
LOGOUT_REDIRECT_URL: Final[str] = "/login/"
###########
# LOGGING #
###########
#: The log level for integreat-cms django apps
LOG_LEVEL: str = os.environ.get("INTEGREAT_CMS_LOG_LEVEL", "DEBUG" if DEBUG else "INFO")
#: The log level for the syslog
SYS_LOG_LEVEL: Final[str] = "INFO"
#: The log level for dependencies
DEPS_LOG_LEVEL: Final[str] = os.environ.get(
"INTEGREAT_CMS_DEPS_LOG_LEVEL", "INFO" if DEBUG else "WARN"
)
#: The file path of the logfile. Needs to be writable by the application.
LOGFILE: Final[str] = os.environ.get(
"INTEGREAT_CMS_LOGFILE", os.path.join(BASE_DIR, "integreat-cms.log")
)
#: A custom message store for logging (see :setting:`django:MESSAGE_STORAGE`)
MESSAGE_STORAGE: Final[str] = "integreat_cms.core.storages.MessageLoggerStorage"
#: Whether to log all entries from the messages framework
MESSAGE_LOGGING_ENABLED: bool = bool(
strtobool(os.environ.get("INTEGREAT_CMS_MESSAGE_LOGGING_ENABLED", str(DEBUG)))
)
#: Logging configuration dictionary (see :setting:`django:LOGGING`)
LOGGING: dict[str, Any] = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"console": {
"()": RequestFormatter,
"format": "{asctime} \x1b[1m{levelname}\x1b[0m {name} - {message}",
"datefmt": "%b %d %H:%M:%S",
"style": "{",
},
"console-colored": {
"()": ColorFormatter,
"format": "{asctime} {levelname} {name} - {message}",
"datefmt": "%b %d %H:%M:%S",
"style": "{",
},
"management-command": {
"()": ColorFormatter,
"format": "{message}",
"style": "{",
},
"logfile": {
"()": RequestFormatter,
"format": "{asctime} {levelname:7} {name} - {message}",
"datefmt": "%b %d %H:%M:%S",
"style": "{",
},
"syslog": {
"format": "INTEGREAT CMS - {levelname}: {message}",
"style": "{",
},
"email": {
"format": "Date and time: {asctime}\nSeverity: {levelname}\nLogger: {name}\nMessage: {message}\nFile: {funcName}() in {pathname}:{lineno}",
"datefmt": "%Y-%m-%d %H:%M:%S",
"style": "{",
},
},
"filters": {
"require_debug_true": {
"()": "django.utils.log.RequireDebugTrue",
},
"require_debug_false": {
"()": "django.utils.log.RequireDebugFalse",
},
"only_stdout": {
"()": "django.utils.log.CallbackFilter",
"callback": lambda record: record.levelno <= SUCCESS,
},
},
"handlers": {
"console": {
"filters": ["require_debug_true"],
"class": "logging.StreamHandler",
"formatter": "console",
},
"console-colored": {
"filters": ["require_debug_true"],
"class": "logging.StreamHandler",
"formatter": "console-colored",
},
# Send DEBUG, INFO and SUCCESS to stdout
"management-command-stdout": {
"class": "logging.StreamHandler",
"filters": ["only_stdout"],
"formatter": "management-command",
"level": "DEBUG",
"stream": sys.stdout,
},
# Send WARNING, ERROR and CRITICAL to stderr
"management-command-stderr": {
"class": "logging.StreamHandler",
"formatter": "management-command",
"level": "WARNING",
},
"logfile": {
"class": "logging.FileHandler",
"filename": LOGFILE,
"formatter": "logfile",
},
"authlog": {
"filters": ["require_debug_false"],
"class": "logging.handlers.SysLogHandler",
"address": "/dev/log",
"facility": "auth",
"formatter": "syslog",
},
"syslog": {
"filters": ["require_debug_false"],
"class": "logging.handlers.SysLogHandler",
"address": "/dev/log",
"facility": "syslog",
},
"mail_admins": {
"level": "ERROR",
"filters": ["require_debug_false"],
"class": "django.utils.log.AdminEmailHandler",
"formatter": "email",
},
},
"loggers": {
# Loggers of integreat-cms django apps
"integreat_cms": {
"handlers": ["console-colored", "logfile"],
"level": LOG_LEVEL,
},
"integreat_cms.core.management.commands": {
"handlers": [
"management-command-stdout",
"management-command-stderr",
"logfile",
],
"level": LOG_LEVEL,
"propagate": False,
},
# Syslog for authentication
"auth": {
"handlers": ["console", "logfile", "authlog", "syslog"],
"level": SYS_LOG_LEVEL,
},
# Loggers of dependencies
"aiohttp.client": {
"handlers": ["console", "logfile"],
"level": DEPS_LOG_LEVEL,
},
"deepl": {
"handlers": ["console", "logfile"],
"level": DEPS_LOG_LEVEL,
},
"django": {
"handlers": ["console", "logfile"],
"level": DEPS_LOG_LEVEL,
},
"geopy": {
"handlers": ["console", "logfile"],
"level": DEPS_LOG_LEVEL,
},
"linkcheck": {
"handlers": ["console", "logfile"],
"level": DEPS_LOG_LEVEL,
},
"PIL": {
"handlers": ["console", "logfile"],
"level": DEPS_LOG_LEVEL,
},
"requests": {
"handlers": ["console", "logfile"],
"level": DEPS_LOG_LEVEL,
},
"rules": {
"handlers": ["console", "logfile"],
"level": DEPS_LOG_LEVEL,
},
"urllib3": {
"handlers": ["console", "logfile"],
"level": DEPS_LOG_LEVEL,
},
"xhtml2pdf": {
"handlers": ["console", "logfile"],
"level": DEPS_LOG_LEVEL,
},
},
}
##########
# EMAILS #
##########
#: The backend to use for sending emails (see :setting:`django:EMAIL_BACKEND` and :doc:`django:topics/email`)
EMAIL_BACKEND: Final[str] = (
"django.core.mail.backends.console.EmailBackend"
if DEBUG
else "django.core.mail.backends.smtp.EmailBackend"
)
#: Default email address to use for various automated correspondence from the site manager(s)
#: (see :setting:`django:DEFAULT_FROM_EMAIL`)
DEFAULT_FROM_EMAIL: Final[str] = os.environ.get(
"INTEGREAT_CMS_SERVER_EMAIL", "keineantwort@integreat-app.de"
)
#: The email address that error messages come from, such as those sent to :attr:`~integreat_cms.core.settings.ADMINS`.
#: (see :setting:`django:SERVER_EMAIL`)
SERVER_EMAIL: Final[str] = os.environ.get(
"INTEGREAT_CMS_SERVER_EMAIL", "keineantwort@integreat-app.de"
)
#: A list of all the people who get code error notifications. When :attr:`~integreat_cms.core.settings.DEBUG` is ``False``,
#: Django emails these people the details of exceptions raised in the request/response cycle.
ADMINS: Final[list[tuple[str, str]]] = [("Integreat Helpdesk", "tech@integreat-app.de")]
#: The host to use for sending email (see :setting:`django:EMAIL_HOST`)
EMAIL_HOST: Final[str] = os.environ.get("INTEGREAT_CMS_EMAIL_HOST", "localhost")
#: Password to use for the SMTP server defined in :attr:`~integreat_cms.core.settings.EMAIL_HOST`
#: (see :setting:`django:EMAIL_HOST_PASSWORD`). If empty, Django won’t attempt authentication.
EMAIL_HOST_PASSWORD: Final[str | None] = os.environ.get(
"INTEGREAT_CMS_EMAIL_HOST_PASSWORD"
)
#: Username to use for the SMTP server defined in :attr:`~integreat_cms.core.settings.EMAIL_HOST`
#: (see :setting:`django:EMAIL_HOST_USER`). If empty, Django won’t attempt authentication.
EMAIL_HOST_USER: Final[str] = os.environ.get(
"INTEGREAT_CMS_EMAIL_HOST_USER", SERVER_EMAIL
)
#: Port to use for the SMTP server defined in :attr:`~integreat_cms.core.settings.EMAIL_HOST`
#: (see :setting:`django:EMAIL_PORT`)
EMAIL_PORT: Final[int] = int(os.environ.get("INTEGREAT_CMS_EMAIL_PORT", 587))
#: Whether to use a TLS (secure) connection when talking to the SMTP server.
#: This is used for explicit TLS connections, generally on port 587.
#: (see :setting:`django:EMAIL_USE_TLS`)
EMAIL_USE_TLS: Final[bool] = bool(
strtobool(os.environ.get("INTEGREAT_CMS_EMAIL_USE_TLS", "True"))
)
#: Whether to use an implicit TLS (secure) connection when talking to the SMTP server.
#: In most email documentation this type of TLS connection is referred to as SSL. It is generally used on port 465.
#: (see :setting:`django:EMAIL_USE_SSL`)
EMAIL_USE_SSL: Final[bool] = bool(
strtobool(os.environ.get("INTEGREAT_CMS_EMAIL_USE_SSL", "False"))
)
########################
# INTERNATIONALIZATION #
########################
#: A list of all available languages with locale files for translated strings
AVAILABLE_LANGUAGES: Final[dict[str, Promise]] = {
"de": _("German"),
"en": _("English"),
"nl": _("Dutch"),
}
#: The default UI languages
DEFAULT_LANGUAGES: Final[list[str]] = ["de", "en"]
#: The list of languages which are available in the UI
#: (see :setting:`django:LANGUAGES` and :doc:`django:topics/i18n/index`)
LANGUAGES: Final[list[tuple[str, Promise]]] = [
(language, AVAILABLE_LANGUAGES[language])
for language in filter(
None,
(
language.strip()
for language in os.environ.get(
"INTEGREAT_CMS_LANGUAGES", "\n".join(DEFAULT_LANGUAGES)
).splitlines()
),
)
]
#: A list of directories where Django looks for translation files
#: (see :setting:`django:LOCALE_PATHS` and :doc:`django:topics/i18n/index`)
LOCALE_PATHS: Final[list[str]] = [os.path.join(BASE_DIR, "locale"), CUSTOM_LOCALE_PATH]
#: A string representing the language slug for this installation
#: (see :setting:`django:LANGUAGE_CODE` and :doc:`django:topics/i18n/index`)
LANGUAGE_CODE: Final[str] = os.environ.get("INTEGREAT_CMS_LANGUAGE_CODE", "de")
#: A string representing the time zone for this installation
#: (see :setting:`django:TIME_ZONE` and :doc:`django:topics/i18n/index`)
TIME_ZONE: Final[str] = "UTC"
#: A string representing the time zone that is used for rendering
CURRENT_TIME_ZONE: Final[str] = os.environ.get(
"INTEGREAT_CMS_CURRENT_TIME_ZONE", "Europe/Berlin"
)
#: A boolean that specifies whether Django’s translation system should be enabled
#: (see :setting:`django:USE_I18N` and :doc:`django:topics/i18n/index`)
USE_I18N: bool = True
#: A boolean that specifies if localized formatting of data will be enabled by default or not
#: (see :setting:`django:USE_L10N` and :doc:`django:topics/i18n/index`)
USE_L10N: Final[bool] = True
#: A boolean that specifies if datetimes will be timezone-aware by default or not
#: (see :setting:`django:USE_TZ` and :doc:`django:topics/i18n/index`)
USE_TZ: Final[bool] = True
#: A full Python path to a Python package that contains custom format definitions for project locales.
#: (see :setting:`django:FORMAT_MODULE_PATH`)
FORMAT_MODULE_PATH: Final[list[str]] = [
"integreat_cms.core.formats",
]
#####################################
# MT Global - AUTOMATIC TRANSLATION #
#####################################
#: An integer specifying the number of translation credits available to regions for free
MT_CREDITS_FREE: Final[int] = int(os.environ.get("MT_CREDITS_FREE", 50_000))
#: An integer specifying the number of translation credits that can be bought as an add-on
MT_CREDITS_ADDON: Final[int] = int(os.environ.get("MT_CREDITS_ADDON", 1_000_000))
#: A floating point that specifies the percentage of MT_CREDITS_FREE used as a soft margin
MT_SOFT_MARGIN_FRACTION: Final[float] = float(
os.environ.get("INTEGREAT_CMS_MT_SOFT_MARGIN", 0.01)
)
#: The actual number of words which are used as soft margin
MT_SOFT_MARGIN: Final[int] = int(MT_SOFT_MARGIN_FRACTION * MT_CREDITS_FREE)
#################################
# DeepL - AUTOMATIC TRANSLATION #
#################################
#: The URL to DeepL API. If not set, the library selects the correct URL automatically
DEEPL_API_URL: Final[str | None] = os.environ.get("INTEGREAT_CMS_DEEPL_API_URL")
#: Authentication token for the DeepL API. If not set, automatic translations via DeepL are disabled
DEEPL_AUTH_KEY: str | None = os.environ.get("INTEGREAT_CMS_DEEPL_AUTH_KEY")
#: Whether automatic translations via DeepL are enabled.
#: This is ``True`` if :attr:`~integreat_cms.core.settings.DEEPL_AUTH_KEY` is set, ``False`` otherwise.
DEEPL_ENABLED: bool = bool(DEEPL_AUTH_KEY)
############################################
# Google Translate - AUTOMATIC TRANSLATION #
############################################
#: Version of Google Translate, either Basic or Advanced
GOOGLE_TRANSLATE_VERSION: str = os.environ.get(
"INTEGREAT_CMS_GOOGLE_TRANSLATE_VERSION", "Basic"
)
#: Path to the saved credential json file
GOOGLE_APPLICATION_CREDENTIALS: str | None = os.environ.get(
"INTEGREAT_CMS_GOOGLE_CREDENTIALS"
)
#: Google project id
GOOGLE_PROJECT_ID: str | None = os.environ.get("INTEGREAT_CMS_GOOGLE_PROJECT_ID")
#: Location for google translate. Default to "global". This must be non-global for custom glossaries.
GOOGLE_TRANSLATE_LOCATION: str = os.environ.get(
"INTEGREAT_CMS_GOOGLE_TRANSLATE_LOCATION", "global"
)
#: This is ``True`` when both the credentials and project id are provided
GOOGLE_TRANSLATE_ENABLED: bool = bool(GOOGLE_APPLICATION_CREDENTIALS) and bool(
GOOGLE_PROJECT_ID
)
#: `parent` parameter for Google Translate, consists of project id and location
GOOGLE_PARENT_PARAM: Final[str] = (
f"projects/{GOOGLE_PROJECT_ID}/locations/{GOOGLE_TRANSLATE_LOCATION}"
)
#########################
# SUMM.AI - EASY GERMAN #
#########################
#: The URL to our SUMM.AI API for automatic translations from German into Easy German
SUMM_AI_API_URL: Final[str] = os.environ.get(
"INTEGREAT_CMS_SUMM_AI_API_URL", "https://backend.summ-ai.com/translate/v1/"
)
#: Authentication token for SUMM.AI,
#: If not set, automatic translations to easy german are disabled
SUMM_AI_API_KEY: str | None = os.environ.get("INTEGREAT_CMS_SUMM_AI_API_KEY")
#: Whether SUMM.AI is enabled or not
#: This is ``True`` if SUMM_AI_API_KEY is set, ``False`` otherwise.
SUMM_AI_ENABLED: bool = bool(SUMM_AI_API_KEY)
#: Whether requests to the SUMM.AI are done with the ``is_test`` flag
SUMM_AI_TEST_MODE: Final[bool] = strtobool(
os.environ.get("INTEGREAT_CMS_SUMM_AI_TEST_MODE", str(DEBUG))
)
#: The timeout in minutes for requests to the SUMM.AI API
SUMM_AI_TIMEOUT: Final[int] = 10
#: The limit for "Too many requests".
SUMM_AI_MAX_CONCURRENT_REQUESTS = int(
os.environ.get("INTEGREAT_CMS_SUMM_AI_MAX_CONCURRENT_REQUESTS", 20)
)
#: Waiting time after "Too many requests" response was sent
SUMM_AI_RATE_LIMIT_COOLDOWN = float(
os.environ.get("INTEGREAT_CMS_SUMM_AI_RATE_LIMIT_COOLDOWN", 30)
)
#: Maximum amount of retries before giving up
#: Retries are reset if translation requests are successful after completing the cooldown
SUMM_AI_MAX_RETRIES = int(os.environ.get("INTEGREAT_CMS_SUMM_AI_MAX_RETRIES", 5))
#: The language slugs for German
SUMM_AI_GERMAN_LANGUAGE_SLUG: Final[str] = os.environ.get(
"INTEGREAT_CMS_SUMM_AI_GERMAN_LANGUAGE_SLUG", "de"
)
#: The language slug for Easy German
SUMM_AI_EASY_GERMAN_LANGUAGE_SLUG: Final[str] = os.environ.get(
"INTEGREAT_CMS_SUMM_AI_EASY_GERMAN_LANGUAGE_SLUG", "de-si"
)
#: The separator which is used to split compound words, e.g. Bundes-Kanzler (hyphen) or Bundes·kanzler (interpunct)
SUMM_AI_SEPARATOR: Final[str] = os.environ.get(
"INTEGREAT_CMS_SUMM_AI_SEPARATOR", "hyphen"
)
#: All plain text fields of the content models which should be translated
SUMM_AI_TEXT_FIELDS: Final[list[str]] = ["meta_description"]
#: All HTML fields of the content models which should be translated
SUMM_AI_HTML_FIELDS: Final[list[str]] = ["content"]
#: All fields of the content models which should not be translated, but inherited from the source translation
SUMM_AI_INHERITED_FIELDS: Final[list[str]] = ["title"]
#: Translate all <p> and <li> tags
SUMM_AI_HTML_TAGS: Final[list[str]] = ["p", "li"]
#: Value of the ``is_initial`` flag
SUMM_AI_IS_INITIAL: Final[bool] = bool(
strtobool(os.environ.get("INTEGREAT_CMS_SUMM_AI_IS_INITIAL", "True"))
)
# Slugs of regions that prefer Plain German over Easy German in the management command
SUMM_AI_PLAIN_GERMAN_REGIONS: Final[list[str]] = [
x.strip()
for x in os.environ.get(
"INTEGREAT_CMS_SUMM_AI_PLAIN_GERMAN_REGIONS", ""
).splitlines()
]
################
# STATIC FILES #
################
#: This setting defines the additional locations the :mod:`django.contrib.staticfiles` app will traverse to collect
#: static files for deployment or to serve them during development (see :setting:`django:STATICFILES_DIRS` and
#: :doc:`Managing static files <django:howto/static-files/index>`).
STATICFILES_DIRS: Final[list[str]] = [os.path.join(BASE_DIR, "static/dist")]
#: The absolute path to the output directory where :mod:`django.contrib.staticfiles` will put static files for
#: deployment (see :setting:`django:STATIC_ROOT` and :doc:`Managing static files <django:howto/static-files/index>`)
#: In debug mode, this is not required since :mod:`django.contrib.staticfiles` can directly serve these files.
STATIC_ROOT: Final[str | None] = os.environ.get("INTEGREAT_CMS_STATIC_ROOT")
#: URL to use in development when referring to static files located in :setting:`STATICFILES_DIRS`
#: (see :setting:`django:STATIC_URL` and :doc:`Managing static files <django:howto/static-files/index>`)
STATIC_URL: Final[str] = "/static/"
#: The list of finder backends that know how to find static files in various locations
#: (see :setting:`django:STATICFILES_FINDERS`)
STATICFILES_FINDERS: Final[tuple[str, ...]] = (
"django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
)
#################
# MEDIA LIBRARY #
#################
#: URL that handles the media served from :setting:`MEDIA_ROOT` (see :setting:`django:MEDIA_URL`)
MEDIA_URL: Final[str] = "/media/"
#: Absolute filesystem path to the directory that will hold user-uploaded files (see :setting:`django:MEDIA_ROOT`)
MEDIA_ROOT: Final[str] = os.environ.get(
"INTEGREAT_CMS_MEDIA_ROOT", os.path.join(BASE_DIR, "media")
)
#: The maximum size of media images in pixels (larger images will automatically be resized)
MEDIA_OPTIMIZED_SIZE: Final[int] = 3000
#: The maximum size of media thumbnails in pixels
MEDIA_THUMBNAIL_SIZE: Final[int] = 300
#: Whether thumbnails should be cropped (resulting in square thumbnails regardless of the aspect ratio of the image)
MEDIA_THUMBNAIL_CROP: Final[bool] = False
#: Enables the possibility to upload further file formats (e.g. DOC, GIF).
LEGACY_FILE_UPLOAD: Final[bool] = bool(
strtobool(os.environ.get("INTEGREAT_CMS_LEGACY_FILE_UPLOAD", "False"))
)
#: The maximum size of media files in bytes
MEDIA_MAX_UPLOAD_SIZE: Final[int] = int(
os.environ.get("INTEGREAT_CMS_MEDIA_MAX_UPLOAD_SIZE", 3 * 1024 * 1024)
)
#########
# CACHE #
#########
#: Configuration for caches (see :setting:`django:CACHES` and :doc:`django:topics/cache`).
#: Use a ``LocMemCache`` for development and a ``RedisCache`` whenever available.
CACHES: dict[str, dict[str, str | dict[str, str | bool]]] = {
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
},
}
# Use RedisCache when activated
if REDIS_CACHE:
if unix_socket := os.environ.get("INTEGREAT_CMS_REDIS_UNIX_SOCKET"):
# Use unix socket if available (and also tell cacheops about it)
redis_location = f"unix://{unix_socket}?db=0"
CACHEOPS_REDIS: Final[str] = f"unix://{unix_socket}?db=1"
else:
# If not, fall back to normal TCP connection
redis_location = "redis://127.0.0.1:6379/0"
CACHES["default"] = {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": redis_location,
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
"IGNORE_EXCEPTIONS": True, # Don't throw exceptions when redis is not available
},
}
#: Default cache timeout for cacheops
CACHEOPS_DEFAULTS: Final[dict[str, int]] = {"timeout": 60 * 60}
#: Which database tables should be cached
CACHEOPS: Final[dict[str, dict[str, str]]] = {
"auth.*": {"ops": "all"},
"cms.*": {"ops": "all"},
"linkcheck.*": {"ops": "all"},
"*.*": {},
}
#: Degrade gracefully on redis fail
CACHEOPS_DEGRADE_ON_FAILURE: Final[bool] = True
##############
# PAGINATION #
##############
#: Number of entries displayed per pagination chunk
#: see :class:`~django.core.paginator.Paginator`
PER_PAGE: Final[int] = 16
####################
# DJANGO LINKCHECK #
####################
#: Used by `django-linkcheck <https://github.com/DjangoAdminHackers/django-linkcheck#site_domain-and-linkcheck_site_domains>`_
#: to determine whether a link is internal.
LINKCHECK_SITE_DOMAINS: Final[list[str]] = [WEBAPP_URL, SHORT_LINKS_URL]
#: Disable linkcheck listeners e.g. when the fixtures are loaded
LINKCHECK_DISABLE_LISTENERS: bool = bool(
strtobool(os.environ.get("INTEGREAT_CMS_LINKCHECK_DISABLE_LISTENERS", "False"))
)
#: The maximum length of URLs which can be checked. Longer URLs will be silently ignored.
LINKCHECK_MAX_URL_LENGTH: Final[int] = 1024
#: URL types that are not supposed to be shown in the link list (e.g. phone numbers and emails)
LINKCHECK_IGNORED_URL_TYPES: Final[list[str]] = [
url_type.strip()
for url_type in os.environ.get(
"INTEGREAT_CMS_LINKCHECK_IGNORED_URL_TYPES", ""
).splitlines()
if url_type
]
#: Wheter email links are enabled
LINKCHECK_EMAIL_ENABLED: Final[bool] = "mailto" not in LINKCHECK_IGNORED_URL_TYPES
#: Wheter phone links are enabled
LINKCHECK_PHONE_ENABLED: Final[bool] = "phone" not in LINKCHECK_IGNORED_URL_TYPES
#: Whether archived pages should be ignored for linkcheck scan.
#: Since this causes a lot of overhead, only use this for the findlinks management command::
#:
#: $ INTEGREAT_CMS_LINKCHECK_COMMAND_RUNNING=1 integreat-cms-cli findlinks
LINKCHECK_COMMAND_RUNNING: Final[bool] = bool(
strtobool(os.environ.get("INTEGREAT_CMS_LINKCHECK_COMMAND_RUNNING", "False"))
)
#################
# INTERNAL URLS #
#################
#: The URLs which are treated as internal in TinyMCE custom link plugin
INTERNAL_URLS: Final[list[str]] = (
ALLOWED_HOSTS
+ [WEBAPP_URL, WEBSITE_URL]
+ [
x.strip()
for x in os.environ.get("INTEGREAT_CMS_INTERNAL_URLS", "").splitlines()
if x
]
)
#########################
# DJANGO WEBPACK LOADER #
#########################
#: Overwrite default bundle directory
WEBPACK_LOADER: Final[dict[str, dict[str, str]]] = {
"DEFAULT": {
"BUNDLE_DIR_NAME": "",
"STATS_FILE": os.path.join(BASE_DIR, "webpack-stats.json"),
}
}
########################
# DJANGO DEBUG TOOLBAR #
########################
#: This setting specifies the full Python path to each panel that you want included in the toolbar.
#: (see :doc:`django-debug-toolbar:configuration`)
DEBUG_TOOLBAR_PANELS: Final[list[str]] = [
"debug_toolbar.panels.timer.TimerPanel",
"debug_toolbar.panels.sql.SQLPanel",
"debug_toolbar.panels.cache.CachePanel",
"debug_toolbar.panels.signals.SignalsPanel",
"debug_toolbar.panels.templates.TemplatesPanel",
"debug_toolbar.panels.staticfiles.StaticFilesPanel",
"debug_toolbar.panels.logging.LoggingPanel",
"debug_toolbar.panels.redirects.RedirectsPanel",
"debug_toolbar.panels.profiling.ProfilingPanel",
"debug_toolbar.panels.request.RequestPanel",
"debug_toolbar.panels.headers.HeadersPanel",
"debug_toolbar.panels.history.HistoryPanel",
"debug_toolbar.panels.settings.SettingsPanel",
]
##############
# PDF EXPORT #
##############
#: The directory where PDF files are stored
PDF_ROOT: Final[str] = os.environ.get(
"INTEGREAT_CMS_PDF_ROOT", os.path.join(BASE_DIR, "pdf")
)
#: The URL path where PDF files are served for download
PDF_URL: Final[str] = "/pdf/"
#: Slugs of languages for which PDF export should be deactivated
PDF_DEACTIVATED_LANGUAGES: Final[str | list[str]] = os.environ.get(
"INTEGREAT_CMS_PDF_DEACTIVATED_LANGUAGES", []
)
#######################
# XLIFF SERIALIZATION #
#######################
#: A dictionary of modules containing serializer definitions (provided as strings),
#: keyed by a string identifier for that serialization type (see :setting:`django:SERIALIZATION_MODULES`).
SERIALIZATION_MODULES: Final[dict[str, str]] = {
"xliff": "integreat_cms.xliff.generic_serializer",
"xliff-1.2": "integreat_cms.xliff.xliff1_serializer",
"xliff-2.0": "integreat_cms.xliff.xliff2_serializer",
}
#: The xliff version to be used for exports
XLIFF_EXPORT_VERSION: Final[str] = os.environ.get(
"INTEGREAT_CMS_XLIFF_EXPORT_VERSION", "xliff-1.2"
)
#: The default fields to be used for the XLIFF serialization
XLIFF_DEFAULT_FIELDS: Final[tuple[str, ...]] = ("title", "content")
#: A mapping for changed field names to preserve backward compatibility after a database field was renamed
XLIFF_LEGACY_FIELDS: Final[dict[str, str]] = {"body": "content"}
#: The directory where xliff files are stored
XLIFF_ROOT: Final[str] = os.environ.get(
"INTEGREAT_CMS_XLIFF_ROOT", os.path.join(BASE_DIR, "xliff")
)
#: The directory to which xliff files should be uploaded (this should not be reachable by the webserver)
XLIFF_UPLOAD_DIR: Final[str] = os.path.join(XLIFF_ROOT, "upload")
#: The directory from which xliff files can be downloaded (this should be publicly available under the url specified in
#: :attr:`~integreat_cms.core.settings.XLIFF_URL`)
XLIFF_DOWNLOAD_DIR: Final[str] = os.path.join(XLIFF_ROOT, "download")
#: The URL path where XLIFF files are served for download
XLIFF_URL: Final[str] = "/xliff/"
############
# DB Mutex #
############
# Our database operations should never exceed 60 seconds
DB_MUTEX_TTL_SECONDS: Final[int] = 60
#############
# Dashboard #
#############
#: Days after which a page is considered to be outdated in the todo dashboard
OUTDATED_THRESHOLD_DAYS: Final[int] = 365
############
# Chat API #
############
#: Size of the sliding window used for rate limiting
USER_CHAT_WINDOW_MINUTES: Final[int] = 10
#: Maximum number of requests users are allowed to send within WINDOW_MINUTES minutes
USER_CHAT_WINDOW_LIMIT: Final[int] = 50
#: Zammad ticket group used for Integreat chat messages
USER_CHAT_TICKET_GROUP: Final[str] = "integreat-chat"