""" Django settings for impress project. Generated by 'django-admin startproject' using Django 3.1.5. For more information on this file, see https://docs.djangoproject.com/en/3.1/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/3.1/ref/settings/ """ import os import tomllib from socket import gethostbyname, gethostname from django.utils.translation import gettext_lazy as _ import dj_database_url import posthog import sentry_sdk from configurations import Configuration, values from corsheaders.defaults import default_headers from csp.constants import NONE from lasuite.configuration.values import SecretFileValue from lasuite.oidc_login.enums import OIDCUserEndpointFormat from sentry_sdk.integrations.django import DjangoIntegration from sentry_sdk.integrations.logging import ignore_logger # pylint: disable=too-many-lines # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) DATA_DIR = os.getenv("DATA_DIR", os.path.join("/", "data")) KB = 1024 MB = KB * KB GB = MB * KB def get_release(): """ Get the current release of the application """ try: with open(os.path.join(BASE_DIR, "pyproject.toml"), "rb") as f: pyproject_data = tomllib.load(f) return pyproject_data["project"]["version"] except FileNotFoundError, KeyError: return "NA" # Default: not available class Base(Configuration): """ This is the base configuration every configuration (aka environment) should inherit from. It is recommended to configure third-party applications by creating a configuration mixins in ./configurations and compose the Base configuration with those mixins. It depends on an environment variable that SHOULD be defined: * DJANGO_SECRET_KEY You may also want to override default configuration by setting the following environment variables: * SENTRY_DSN * DB_NAME * DB_HOST * DB_PASSWORD * DB_USER """ DEBUG = False USE_SWAGGER = False API_VERSION = "v1.0" # Security ALLOWED_HOSTS = values.ListValue([]) SECRET_KEY = SecretFileValue(None) SERVER_TO_SERVER_API_TOKENS = values.ListValue([]) # Application definition ROOT_URLCONF = "impress.urls" WSGI_APPLICATION = "impress.wsgi.application" # Database DATABASES = { "default": dj_database_url.config() if values.DatabaseURLValue( None, environ_name="DATABASE_URL", environ_prefix=None ) else { "ENGINE": values.Value( "django.db.backends.postgresql", environ_name="DB_ENGINE", environ_prefix=None, ), "NAME": values.Value( "impress", environ_name="DB_NAME", environ_prefix=None ), "USER": values.Value("dinum", environ_name="DB_USER", environ_prefix=None), "PASSWORD": SecretFileValue( "pass", environ_name="DB_PASSWORD", environ_prefix=None ), "HOST": values.Value( "localhost", environ_name="DB_HOST", environ_prefix=None ), "PORT": values.Value(5432, environ_name="DB_PORT", environ_prefix=None), # Psycopg pool can be configured in the post_setup method } } DEFAULT_AUTO_FIELD = "django.db.models.AutoField" # Search SEARCH_INDEXER_CLASS = values.Value( default=None, environ_name="SEARCH_INDEXER_CLASS", environ_prefix=None, ) SEARCH_INDEXER_BATCH_SIZE = values.IntegerValue( default=100_000, environ_name="SEARCH_INDEXER_BATCH_SIZE", environ_prefix=None ) INDEXING_URL = values.Value( default=None, environ_name="INDEXING_URL", environ_prefix=None ) SEARCH_INDEXER_COUNTDOWN = values.IntegerValue( default=1, environ_name="SEARCH_INDEXER_COUNTDOWN", environ_prefix=None ) SEARCH_INDEXER_SECRET = values.Value( default=None, environ_name="SEARCH_INDEXER_SECRET", environ_prefix=None ) SEARCH_URL = values.Value( default=None, environ_name="SEARCH_URL", environ_prefix=None ) SEARCH_INDEXER_QUERY_LIMIT = values.PositiveIntegerValue( default=50, environ_name="SEARCH_INDEXER_QUERY_LIMIT", environ_prefix=None ) MEDIA_AUTH_ORIGINAL_URL_HEADER = values.Value( default="HTTP_X_ORIGINAL_URL", environ_name="MEDIA_AUTH_ORIGINAL_URL_HEADER", environ_prefix=None, ) # Static files (CSS, JavaScript, Images) STATIC_URL = "/static/" STATIC_ROOT = os.path.join(DATA_DIR, "static") MEDIA_URL = "/media/" MEDIA_ROOT = os.path.join(DATA_DIR, "media") MEDIA_BASE_URL = values.Value( None, environ_name="MEDIA_BASE_URL", environ_prefix=None ) SITE_ID = 1 STORAGES = { "default": { "BACKEND": "storages.backends.s3.S3Storage", }, "staticfiles": { "BACKEND": values.Value( "whitenoise.storage.CompressedManifestStaticFilesStorage", environ_name="STORAGES_STATICFILES_BACKEND", ), }, # django-silk looks up its binary cProfile (.prof) storage under this # exact alias (see silk.models). Routing it through the S3 backend keeps # profiling artifacts off the pod filesystem, which is read-only / # ephemeral in Kubernetes; the `silk/` prefix isolates them in the # bucket. Only used when SILK_ENABLED and the binary profiler are on. "SILKY_STORAGE": { "BACKEND": "storages.backends.s3.S3Storage", "OPTIONS": {"location": "silk"}, }, } # Media AWS_S3_ENDPOINT_URL = values.Value( environ_name="AWS_S3_ENDPOINT_URL", environ_prefix=None ) AWS_S3_ACCESS_KEY_ID = SecretFileValue( environ_name="AWS_S3_ACCESS_KEY_ID", environ_prefix=None ) AWS_S3_SECRET_ACCESS_KEY = SecretFileValue( environ_name="AWS_S3_SECRET_ACCESS_KEY", environ_prefix=None ) AWS_S3_REGION_NAME = values.Value( environ_name="AWS_S3_REGION_NAME", environ_prefix=None ) AWS_STORAGE_BUCKET_NAME = values.Value( "impress-media-storage", environ_name="AWS_STORAGE_BUCKET_NAME", environ_prefix=None, ) AWS_S3_SIGNATURE_VERSION = values.Value( "s3v4", environ_name="AWS_S3_SIGNATURE_VERSION", environ_prefix=None, ) # Document images DOCUMENT_IMAGE_MAX_SIZE = values.IntegerValue( 10 * MB, # 10MB environ_name="DOCUMENT_IMAGE_MAX_SIZE", environ_prefix=None, ) REACTIONS_MAX_PER_COMMENT = values.IntegerValue( 15, environ_name="REACTIONS_MAX_PER_COMMENT", environ_prefix=None, ) DOCUMENT_UNSAFE_MIME_TYPES = [ # Executable Files "application/x-msdownload", "application/x-bat", "application/x-dosexec", "application/x-sh", "application/x-ms-dos-executable", "application/x-msi", "application/java-archive", "application/octet-stream", # Dynamic Web Pages "application/x-httpd-php", "application/x-asp", "application/x-aspx", "application/jsp", "application/xhtml+xml", "application/x-python-code", "application/x-perl", "text/html", "text/javascript", "text/x-php", # System Files "application/x-msdownload", "application/x-sys", "application/x-drv", "application/cpl", "application/x-apple-diskimage", # Script Files "application/javascript", "application/x-vbscript", "application/x-powershell", "application/x-shellscript", # Compressed/Archive Files "application/zip", "application/x-tar", "application/gzip", "application/x-bzip2", "application/x-7z-compressed", "application/x-rar", "application/x-rar-compressed", "application/x-compress", "application/x-lzma", # Macros in Documents "application/vnd.ms-word", "application/vnd.ms-excel", "application/vnd.ms-powerpoint", "application/vnd.ms-word.document.macroenabled.12", "application/vnd.ms-excel.sheet.macroenabled.12", "application/vnd.ms-powerpoint.presentation.macroenabled.12", # Disk Images & Virtual Disk Files "application/x-iso9660-image", "application/x-vmdk", "application/x-apple-diskimage", "application/x-dmg", # Other Dangerous MIME Types "application/x-ms-application", "application/x-msdownload", "application/x-shockwave-flash", "application/x-silverlight-app", "application/x-java-vm", "application/x-bittorrent", "application/hta", "application/x-csh", "application/x-ksh", "application/x-ms-regedit", "application/x-msdownload", "application/xml", ] DOCUMENT_ATTACHMENT_CHECK_UNSAFE_MIME_TYPES_ENABLED = values.BooleanValue( True, environ_name="DOCUMENT_ATTACHMENT_CHECK_UNSAFE_MIME_TYPES_ENABLED", environ_prefix=None, ) # Document versions DOCUMENT_VERSIONS_PAGE_SIZE = 50 # Document /all endpoint DOCUMENT_ALL_ENDPOINT_ENABLED = values.BooleanValue( default=True, environ_name="DOCUMENT_ALL_ENDPOINT_ENABLED", environ_prefix=None, ) # Internationalization # https://docs.djangoproject.com/en/3.1/topics/i18n/ # Languages LANGUAGE_CODE = values.Value("en-us") # cookie & language is set from frontend LANGUAGE_COOKIE_NAME = "docs_language" LANGUAGE_COOKIE_PATH = "/" DRF_NESTED_MULTIPART_PARSER = { # output of parser is converted to querydict # if is set to False, dict python is returned "querydict": False, } # Careful! Languages should be ordered by priority, as this tuple is used to get # fallback/default languages throughout the app. LANGUAGES = values.SingleNestedTupleValue( ( ("en-us", "English"), ("fr-fr", "Français"), ("de-de", "Deutsch"), ("nl-nl", "Nederlands"), ("es-es", "Español"), ) ) LOCALE_PATHS = (os.path.join(BASE_DIR, "locale"),) TIME_ZONE = "UTC" USE_I18N = True USE_TZ = True # Templates TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [os.path.join(BASE_DIR, "templates")], "OPTIONS": { "context_processors": [ "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", "django.template.context_processors.csrf", "django.template.context_processors.debug", "django.template.context_processors.i18n", "django.template.context_processors.media", "django.template.context_processors.request", "django.template.context_processors.tz", ], "loaders": [ "django.template.loaders.filesystem.Loader", "django.template.loaders.app_directories.Loader", ], }, }, ] MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "whitenoise.middleware.WhiteNoiseMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.locale.LocaleMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", "corsheaders.middleware.CorsMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "core.middleware.ForceSessionMiddleware", "core.middleware.SaveRawBodyMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "dockerflow.django.middleware.DockerflowMiddleware", "csp.middleware.CSPMiddleware", "waffle.middleware.WaffleMiddleware", ] AUTHENTICATION_BACKENDS = [ "django.contrib.auth.backends.ModelBackend", "core.authentication.backends.OIDCAuthenticationBackend", ] # Django applications from the highest priority to the lowest INSTALLED_APPS = [ # impress "core", "demo", "drf_spectacular", # Third party apps "corsheaders", "django_filters", "dockerflow.django", "rest_framework", "parler", "treebeard", "easy_thumbnails", "waffle", # Django "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.postgres", "django.contrib.sessions", "django.contrib.sites", "django.contrib.messages", "django.contrib.staticfiles", # OIDC third party "mozilla_django_oidc", "lasuite.malware_detection", "lasuite.marketing", "csp", ] # Cache CACHES = { "default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}, } REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": ( "core.authentication.backends.SessionAuthentication", ), "DEFAULT_PARSER_CLASSES": [ "rest_framework.parsers.JSONParser", "nested_multipart_parser.drf.DrfNestedParser", ], "DEFAULT_RENDERER_CLASSES": [ # 🔒️ Disable BrowsableAPIRenderer which provides forms allowing a user to # see all the data in the database (ie a serializer with a ForeignKey field # will generate a form with a field with all possible values of the FK). "rest_framework.renderers.JSONRenderer", ], "EXCEPTION_HANDLER": "core.api.exception_handler", "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", "PAGE_SIZE": 20, "DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.URLPathVersioning", "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", "DEFAULT_THROTTLE_CLASSES": [ "lasuite.drf.throttling.MonitoredScopedRateThrottle", ], "DEFAULT_THROTTLE_RATES": { "user_list_sustained": values.Value( default="180/hour", environ_name="API_USERS_LIST_THROTTLE_RATE_SUSTAINED", environ_prefix=None, ), "user_list_burst": values.Value( default="30/minute", environ_name="API_USERS_LIST_THROTTLE_RATE_BURST", environ_prefix=None, ), "document": values.Value( default="80/minute", environ_name="API_DOCUMENT_THROTTLE_RATE", environ_prefix=None, ), "document_access": values.Value( default="50/minute", environ_name="API_DOCUMENT_ACCESS_THROTTLE_RATE", environ_prefix=None, ), "invitation": values.Value( default="60/minute", environ_name="API_INVITATION_THROTTLE_RATE", environ_prefix=None, ), "document_ask_for_access": values.Value( default="30/minute", environ_name="API_DOCUMENT_ASK_FOR_ACCESS_THROTTLE_RATE", environ_prefix=None, ), "config": values.Value( default="30/minute", environ_name="API_CONFIG_THROTTLE_RATE", environ_prefix=None, ), }, } MONITORED_THROTTLE_FAILURE_CALLBACK = ( "core.api.throttling.sentry_monitoring_throttle_failure" ) SPECTACULAR_SETTINGS = { "TITLE": "Impress API", "DESCRIPTION": "This is the impress API schema.", "VERSION": "1.0.0", "SERVE_INCLUDE_SCHEMA": False, "ENABLE_DJANGO_DEPLOY_CHECK": values.BooleanValue( default=False, environ_name="SPECTACULAR_SETTINGS_ENABLE_DJANGO_DEPLOY_CHECK", ), "COMPONENT_SPLIT_REQUEST": True, # OTHER SETTINGS "SWAGGER_UI_DIST": "SIDECAR", # shorthand to use the sidecar instead "SWAGGER_UI_FAVICON_HREF": "SIDECAR", "REDOC_DIST": "SIDECAR", } TRASHBIN_CUTOFF_DAYS = values.IntegerValue( 30, environ_name="TRASHBIN_CUTOFF_DAYS", environ_prefix=None ) # Mail EMAIL_BACKEND = values.Value("django.core.mail.backends.smtp.EmailBackend") EMAIL_BRAND_NAME = values.Value(None) EMAIL_HOST = values.Value(None) EMAIL_HOST_USER = values.Value(None) EMAIL_HOST_PASSWORD = SecretFileValue(None) EMAIL_LOGO_IMG = values.Value(None) EMAIL_PORT = values.PositiveIntegerValue(None) EMAIL_URL_APP = values.Value(None) EMAIL_USE_TLS = values.BooleanValue(False) EMAIL_USE_SSL = values.BooleanValue(False) EMAIL_FROM = values.Value("from@example.com") AUTH_USER_MODEL = "core.User" INVITATION_VALIDITY_DURATION = 604800 # 7 days, in seconds # CORS CORS_ALLOW_CREDENTIALS = True CORS_ALLOW_ALL_ORIGINS = values.BooleanValue(False) CORS_ALLOWED_ORIGINS = values.ListValue([]) CORS_ALLOWED_ORIGIN_REGEXES = values.ListValue([]) # Sentry SENTRY_DSN = values.Value(None, environ_name="SENTRY_DSN", environ_prefix=None) # Collaboration # TODO(yhub): unused since the yhub migration — yhub has no management API # (reset-connections / get-connections). Kept until a yhub kick and # connection-info API exist and CollaborationService is reinstated. COLLABORATION_API_URL = values.Value( None, environ_name="COLLABORATION_API_URL", environ_prefix=None ) # TODO(yhub): unused since the yhub migration, see COLLABORATION_API_URL. COLLABORATION_SERVER_SECRET = SecretFileValue( None, environ_name="COLLABORATION_SERVER_SECRET", environ_prefix=None ) COLLABORATION_WS_URL = values.Value( None, environ_name="COLLABORATION_WS_URL", environ_prefix=None ) COLLABORATION_WS_NOT_CONNECTED_READ_ONLY = values.BooleanValue( default=values.BooleanValue( # COLLABORATION_WS_NOT_CONNECTED_READY_ONLY compat default=False, environ_name="COLLABORATION_WS_NOT_CONNECTED_READY_ONLY", environ_prefix=None, ), environ_name="COLLABORATION_WS_NOT_CONNECTED_READ_ONLY", environ_prefix=None, ) COLLABORATION_WS_INACTIVITY_TIMEOUT = values.IntegerValue( None, environ_name="COLLABORATION_WS_INACTIVITY_TIMEOUT", environ_prefix=None, ) # JWT # RSA private key (PEM) used to sign the tokens issued by # core.services.jwt_services.JWTService. Prefer the JWT_PRIVATE_KEY_FILE # environment variable, a PEM does not fit well in an environment variable. JWT_PRIVATE_KEY = SecretFileValue( None, environ_name="JWT_PRIVATE_KEY", environ_prefix=None, ) # Lifetime, in seconds, of the tokens issued by the JWT service. It is both # the "exp" claim horizon and the cache timeout of the generated tokens. JWT_TOKEN_LIFETIME = values.IntegerValue( default=3600, environ_name="JWT_TOKEN_LIFETIME", environ_prefix=None, ) # Frontend FRONTEND_THEME = values.Value( None, environ_name="FRONTEND_THEME", environ_prefix=None ) FRONTEND_HOMEPAGE_FEATURE_ENABLED = values.BooleanValue( default=True, environ_name="FRONTEND_HOMEPAGE_FEATURE_ENABLED", environ_prefix=None, ) FRONTEND_CSS_URL = values.Value( None, environ_name="FRONTEND_CSS_URL", environ_prefix=None ) FRONTEND_JS_URL = values.Value( None, environ_name="FRONTEND_JS_URL", environ_prefix=None ) FRONTEND_SILENT_LOGIN_ENABLED = values.BooleanValue( default=False, environ_name="FRONTEND_SILENT_LOGIN_ENABLED", environ_prefix=None ) THEME_CUSTOMIZATION_FILE_PATH = values.Value( os.path.join(BASE_DIR, "impress/configuration/theme/default.json"), environ_name="THEME_CUSTOMIZATION_FILE_PATH", environ_prefix=None, ) THEME_CUSTOMIZATION_CACHE_TIMEOUT = values.IntegerValue( 60 * 60 * 24, environ_name="THEME_CUSTOMIZATION_CACHE_TIMEOUT", environ_prefix=None, ) # Posthog POSTHOG_KEY = SecretFileValue(None, environ_name="POSTHOG_KEY", environ_prefix=None) POSTHOG_HOST = values.Value( "https://eu.i.posthog.com", environ_name="POSTHOG_HOST", environ_prefix=None ) # Easy thumbnails THUMBNAIL_EXTENSION = "webp" THUMBNAIL_TRANSPARENCY_EXTENSION = "webp" THUMBNAIL_DEFAULT_STORAGE_ALIAS = "default" THUMBNAIL_ALIASES = {} # Celery CELERY_BROKER_URL = values.Value("redis://redis:6379/0") CELERY_BROKER_TRANSPORT_OPTIONS = values.DictValue({}) # Session SESSION_ENGINE = "django.contrib.sessions.backends.cache" SESSION_CACHE_ALIAS = "default" SESSION_COOKIE_AGE = values.PositiveIntegerValue( default=60 * 60 * 12, environ_name="SESSION_COOKIE_AGE", environ_prefix=None ) SESSION_COOKIE_NAME = "docs_sessionid" # OIDC - Authorization Code Flow OIDC_AUTHENTICATE_CLASS = values.Value( "lasuite.oidc_login.views.OIDCAuthenticationRequestView", environ_name="OIDC_AUTHENTICATE_CLASS", environ_prefix=None, ) OIDC_CALLBACK_CLASS = values.Value( "lasuite.oidc_login.views.OIDCAuthenticationCallbackView", environ_name="OIDC_CALLBACK_CLASS", environ_prefix=None, ) OIDC_CREATE_USER = values.BooleanValue( default=True, environ_name="OIDC_CREATE_USER", ) OIDC_RP_SIGN_ALGO = values.Value( "RS256", environ_name="OIDC_RP_SIGN_ALGO", environ_prefix=None ) OIDC_RP_CLIENT_ID = values.Value( "impress", environ_name="OIDC_RP_CLIENT_ID", environ_prefix=None ) OIDC_RP_CLIENT_SECRET = SecretFileValue( None, environ_name="OIDC_RP_CLIENT_SECRET", environ_prefix=None, ) OIDC_OP_JWKS_ENDPOINT = values.Value( environ_name="OIDC_OP_JWKS_ENDPOINT", environ_prefix=None ) OIDC_OP_AUTHORIZATION_ENDPOINT = values.Value( environ_name="OIDC_OP_AUTHORIZATION_ENDPOINT", environ_prefix=None ) OIDC_OP_TOKEN_ENDPOINT = values.Value( None, environ_name="OIDC_OP_TOKEN_ENDPOINT", environ_prefix=None ) OIDC_OP_USER_ENDPOINT = values.Value( None, environ_name="OIDC_OP_USER_ENDPOINT", environ_prefix=None ) OIDC_OP_USER_ENDPOINT_FORMAT = values.Value( OIDCUserEndpointFormat.AUTO.name, environ_name="OIDC_OP_USER_ENDPOINT_FORMAT", eviron_prefix=None, choices=[e.name for e in OIDCUserEndpointFormat], ) OIDC_OP_LOGOUT_ENDPOINT = values.Value( None, environ_name="OIDC_OP_LOGOUT_ENDPOINT", environ_prefix=None ) OIDC_AUTH_REQUEST_EXTRA_PARAMS = values.DictValue( {}, environ_name="OIDC_AUTH_REQUEST_EXTRA_PARAMS", environ_prefix=None ) OIDC_RP_SCOPES = values.Value( "openid email", environ_name="OIDC_RP_SCOPES", environ_prefix=None ) LOGIN_REDIRECT_URL = values.Value( None, environ_name="LOGIN_REDIRECT_URL", environ_prefix=None ) LOGIN_REDIRECT_URL_FAILURE = values.Value( None, environ_name="LOGIN_REDIRECT_URL_FAILURE", environ_prefix=None ) LOGOUT_REDIRECT_URL = values.Value( None, environ_name="LOGOUT_REDIRECT_URL", environ_prefix=None ) OIDC_USE_NONCE = values.BooleanValue( default=True, environ_name="OIDC_USE_NONCE", environ_prefix=None ) OIDC_REDIRECT_REQUIRE_HTTPS = values.BooleanValue( default=False, environ_name="OIDC_REDIRECT_REQUIRE_HTTPS", environ_prefix=None ) OIDC_REDIRECT_ALLOWED_HOSTS = values.ListValue( default=[], environ_name="OIDC_REDIRECT_ALLOWED_HOSTS", environ_prefix=None ) OIDC_STORE_ID_TOKEN = values.BooleanValue( default=True, environ_name="OIDC_STORE_ID_TOKEN", environ_prefix=None ) OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION = values.BooleanValue( default=True, environ_name="OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION", environ_prefix=None, ) OIDC_USE_PKCE = values.BooleanValue( default=False, environ_name="OIDC_USE_PKCE", environ_prefix=None ) OIDC_PKCE_CODE_CHALLENGE_METHOD = values.Value( default="S256", environ_name="OIDC_PKCE_CODE_CHALLENGE_METHOD", environ_prefix=None, ) OIDC_PKCE_CODE_VERIFIER_SIZE = values.IntegerValue( default=64, environ_name="OIDC_PKCE_CODE_VERIFIER_SIZE", environ_prefix=None ) OIDC_STORE_ACCESS_TOKEN = values.BooleanValue( default=False, environ_name="OIDC_STORE_ACCESS_TOKEN", environ_prefix=None ) OIDC_STORE_REFRESH_TOKEN = values.BooleanValue( default=False, environ_name="OIDC_STORE_REFRESH_TOKEN", environ_prefix=None ) OIDC_STORE_REFRESH_TOKEN_KEY = values.Value( default=None, environ_name="OIDC_STORE_REFRESH_TOKEN_KEY", environ_prefix=None, ) # WARNING: Enabling this setting allows multiple user accounts to share the same email # address. This may cause security issues and is not recommended for production use when # email is activated as fallback for identification (see previous setting). OIDC_ALLOW_DUPLICATE_EMAILS = values.BooleanValue( default=False, environ_name="OIDC_ALLOW_DUPLICATE_EMAILS", environ_prefix=None, ) USER_OIDC_ESSENTIAL_CLAIMS = values.ListValue( default=[], environ_name="USER_OIDC_ESSENTIAL_CLAIMS", environ_prefix=None ) OIDC_USERINFO_FULLNAME_FIELDS = values.ListValue( default=values.ListValue( # retrocompatibility default=["first_name", "last_name"], environ_name="USER_OIDC_FIELDS_TO_FULLNAME", environ_prefix=None, ), environ_name="OIDC_USERINFO_FULLNAME_FIELDS", environ_prefix=None, ) OIDC_USERINFO_SHORTNAME_FIELD = values.Value( default=values.Value( # retrocompatibility default="first_name", environ_name="USER_OIDC_FIELD_TO_SHORTNAME", environ_prefix=None, ), environ_name="OIDC_USERINFO_SHORTNAME_FIELD", environ_prefix=None, ) # OIDC Resource Server OIDC_RESOURCE_SERVER_ENABLED = values.BooleanValue( default=False, environ_name="OIDC_RESOURCE_SERVER_ENABLED", environ_prefix=None ) OIDC_RS_BACKEND_CLASS = values.Value( "lasuite.oidc_resource_server.backend.ResourceServerBackend", environ_name="OIDC_RS_BACKEND_CLASS", environ_prefix=None, ) OIDC_OP_URL = values.Value(None, environ_name="OIDC_OP_URL", environ_prefix=None) OIDC_VERIFY_SSL = values.BooleanValue( default=True, environ_name="OIDC_VERIFY_SSL", environ_prefix=None ) OIDC_TIMEOUT = values.PositiveIntegerValue( 3, environ_name="OIDC_TIMEOUT", environ_prefix=None ) OIDC_PROXY = values.Value(None, environ_name="OIDC_PROXY", environ_prefix=None) OIDC_OP_INTROSPECTION_ENDPOINT = values.Value( None, environ_name="OIDC_OP_INTROSPECTION_ENDPOINT", environ_prefix=None ) OIDC_RS_CLIENT_ID = values.Value( None, environ_name="OIDC_RS_CLIENT_ID", environ_prefix=None ) OIDC_RS_CLIENT_SECRET = values.Value( None, environ_name="OIDC_RS_CLIENT_SECRET", environ_prefix=None ) OIDC_RS_AUDIENCE_CLAIM = values.Value( "client_id", environ_name="OIDC_RS_AUDIENCE_CLAIM", environ_prefix=None ) OIDC_RS_ENCRYPTION_ENCODING = values.Value( "A256GCM", environ_name="OIDC_RS_ENCRYPTION_ENCODING", environ_prefix=None ) OIDC_RS_ENCRYPTION_ALGO = values.Value( "RSA-OAEP", environ_name="OIDC_RS_ENCRYPTION_ALGO", environ_prefix=None ) OIDC_RS_SIGNING_ALGO = values.Value( "ES256", environ_name="OIDC_RS_SIGNING_ALGO", environ_prefix=None ) OIDC_RS_SCOPES = values.ListValue( ["openid"], environ_name="OIDC_RS_SCOPES", environ_prefix=None ) OIDC_RS_ALLOWED_AUDIENCES = values.ListValue( default=[], environ_name="OIDC_RS_ALLOWED_AUDIENCES", environ_prefix=None, ) OIDC_RS_PRIVATE_KEY_STR = values.Value( default=None, environ_name="OIDC_RS_PRIVATE_KEY_STR", environ_prefix=None, ) OIDC_RS_ENCRYPTION_KEY_TYPE = values.Value( default="RSA", environ_name="OIDC_RS_ENCRYPTION_KEY_TYPE", environ_prefix=None, ) # External API Configuration # Configure available routes and actions for external_api endpoints EXTERNAL_API = values.DictValue( default={ "documents": { "enabled": True, "actions": [ "list", "retrieve", "create", "children", ], }, "document_access": { "enabled": False, "actions": [], }, "document_invitation": { "enabled": False, "actions": [], }, "users": { "enabled": True, "actions": ["get_me"], }, }, environ_name="EXTERNAL_API", environ_prefix=None, ) ALLOW_LOGOUT_GET_METHOD = values.BooleanValue( default=True, environ_name="ALLOW_LOGOUT_GET_METHOD", environ_prefix=None ) # AI settings AI_ALLOW_REACH_FROM = values.Value( choices=("public", "authenticated", "restricted"), default="authenticated", environ_name="AI_ALLOW_REACH_FROM", environ_prefix=None, ) MISTRAL_SDK_BASE_URL = values.Value( None, environ_name="MISTRAL_SDK_BASE_URL", environ_prefix=None ) MISTRAL_SDK_API_KEY = SecretFileValue( None, environ_name="MISTRAL_SDK_API_KEY", environ_prefix=None ) OPENAI_SDK_API_KEY = SecretFileValue( default=SecretFileValue( # retrocompatibility None, environ_name="AI_API_KEY", environ_prefix=None, ), environ_name="OPENAI_SDK_API_KEY", environ_prefix=None, ) OPENAI_SDK_BASE_URL = values.Value( default=values.Value( # retrocompatibility None, environ_name="AI_BASE_URL", environ_prefix=None ), environ_name="OPENAI_SDK_BASE_URL", environ_prefix=None, ) AI_BOT = values.DictValue( default={ "name": _("Docs AI"), "color": "#8bc6ff", }, environ_name="AI_BOT", environ_prefix=None, ) AI_DOCUMENT_RATE_THROTTLE_RATES = { "minute": 5, "hour": 100, "day": 500, } # Master settings to enable AI features, if you set it to False, # all AI features will be disabled even if the other settings are enabled. AI_FEATURE_ENABLED = values.BooleanValue( default=False, environ_name="AI_FEATURE_ENABLED", environ_prefix=None ) # Far better UI but more flaky for the moment # ⚠️ AGPL license, be sure to comply with the Blocknote license # if you enable it (https://www.blocknotejs.org/) AI_FEATURE_BLOCKNOTE_ENABLED = values.BooleanValue( default=False, environ_name="AI_FEATURE_BLOCKNOTE_ENABLED", environ_prefix=None ) # UI with less features but more stable # MIT friendly license, you can enable it without worrying about the license AI_FEATURE_LEGACY_ENABLED = values.BooleanValue( default=True, environ_name="AI_FEATURE_LEGACY_ENABLED", environ_prefix=None ) AI_MODEL = values.Value(None, environ_name="AI_MODEL", environ_prefix=None) AI_VERCEL_SDK_VERSION = values.IntegerValue( 6, environ_name="AI_VERCEL_SDK_VERSION", environ_prefix=None ) AI_USER_RATE_THROTTLE_RATES = { "minute": 3, "hour": 50, "day": 200, } LANGFUSE_SECRET_KEY = SecretFileValue( None, environ_name="LANGFUSE_SECRET_KEY", environ_prefix=None ) LANGFUSE_PUBLIC_KEY = values.Value( None, environ_name="LANGFUSE_PUBLIC_KEY", environ_prefix=None ) LANGFUSE_BASE_URL = values.Value( None, environ_name="LANGFUSE_BASE_URL", environ_prefix=None ) # Y provider microservice Y_PROVIDER_API_KEY = SecretFileValue( environ_name="Y_PROVIDER_API_KEY", environ_prefix=None, ) Y_PROVIDER_API_BASE_URL = values.Value( environ_name="Y_PROVIDER_API_BASE_URL", environ_prefix=None, ) # DocSpec API microservice DOCSPEC_API_URL = values.Value(environ_name="DOCSPEC_API_URL", environ_prefix=None) # Imported file settings CONVERSION_UPLOAD_ENABLED = values.BooleanValue( False, environ_name="CONVERSION_UPLOAD_ENABLED", environ_prefix=None ) CONVERSION_FILE_MAX_SIZE = values.IntegerValue( 20 * MB, environ_name="CONVERSION_FILE_MAX_SIZE", environ_prefix=None, ) CONVERSION_FILE_EXTENSIONS_ALLOWED = values.ListValue( default=[".docx", ".md"], environ_name="CONVERSION_FILE_EXTENSIONS_ALLOWED", environ_prefix=None, ) # Conversion endpoint CONVERSION_API_ENDPOINT = values.Value( default="convert", environ_name="CONVERSION_API_ENDPOINT", environ_prefix=None, ) CONVERSION_API_CONTENT_FIELD = values.Value( default="content", environ_name="CONVERSION_API_CONTENT_FIELD", environ_prefix=None, ) CONVERSION_API_TIMEOUT = values.Value( default=30, environ_name="CONVERSION_API_TIMEOUT", environ_prefix=None, ) CONVERSION_API_SECURE = values.Value( default=False, environ_name="CONVERSION_API_SECURE", environ_prefix=None, ) NO_WEBSOCKET_CACHE_TIMEOUT = values.Value( default=120, environ_name="NO_WEBSOCKET_CACHE_TIMEOUT", environ_prefix=None, ) # Logging # We want to make it easy to log to console but by default we log production # to Sentry and don't want to log to console. LOGGING = { "version": 1, "disable_existing_loggers": False, "formatters": { "simple": { "format": "{asctime} {name} {levelname} {message}", "style": "{", }, }, "handlers": { "console": { "class": "logging.StreamHandler", "formatter": "simple", }, }, # Override root logger to send it to console "root": { "handlers": ["console"], "level": values.Value( "INFO", environ_name="LOGGING_LEVEL_LOGGERS_ROOT", environ_prefix=None ), }, "loggers": { "core": { "handlers": ["console"], "level": values.Value( "INFO", environ_name="LOGGING_LEVEL_LOGGERS_APP", environ_prefix=None, ), "propagate": True, }, "docs.security": { "handlers": ["console"], "level": values.Value( "INFO", environ_name="LOGGING_LEVEL_LOGGERS_SECURITY", environ_prefix=None, ), "propagate": True, }, }, } MALWARE_DETECTION = { "BACKEND": values.Value( "lasuite.malware_detection.backends.dummy.DummyBackend", environ_name="MALWARE_DETECTION_BACKEND", environ_prefix=None, ), "PARAMETERS": values.DictValue( default={ "callback_path": "core.malware_detection.malware_detection_callback", }, environ_name="MALWARE_DETECTION_PARAMETERS", environ_prefix=None, ), } API_USERS_LIST_LIMIT = values.PositiveIntegerValue( default=5, environ_name="API_USERS_LIST_LIMIT", environ_prefix=None, ) API_USERS_SEARCH_QUERY_MIN_LENGTH = values.PositiveIntegerValue( default=3, environ_name="API_USERS_SEARCH_QUERY_MIN_LENGTH", environ_prefix=None, ) # Content Security Policy # See https://content-security-policy.com/ for more information. CONTENT_SECURITY_POLICY = { "EXCLUDE_URL_PREFIXES": values.ListValue( ["/admin"], environ_name="CONTENT_SECURITY_POLICY_EXCLUDE_URL_PREFIXES", environ_prefix=None, ), "DIRECTIVES": values.DictValue( default={ "default-src": [NONE], "script-src": [NONE], "style-src": [NONE], "img-src": [NONE], "connect-src": [NONE], "font-src": [NONE], "object-src": [NONE], "media-src": [NONE], "frame-src": [NONE], "child-src": [NONE], "form-action": [NONE], "frame-ancestors": [NONE], "base-uri": [NONE], "worker-src": [NONE], "manifest-src": [NONE], "prefetch-src": [NONE], }, environ_name="CONTENT_SECURITY_POLICY_DIRECTIVES", environ_prefix=None, ), } # User accounts management USER_RECONCILIATION_FORM_URL = values.Value( None, environ_name="USER_RECONCILIATION_FORM_URL", environ_prefix=None ) USER_ONBOARDING_DOCUMENTS = values.ListValue( [], environ_name="USER_ONBOARDING_DOCUMENTS", environ_prefix=None ) USER_ONBOARDING_SANDBOX_DOCUMENT = values.Value( None, environ_name="USER_ONBOARDING_SANDBOX_DOCUMENT", environ_prefix=None ) # Marketing and communication settings SIGNUP_NEW_USER_TO_MARKETING_EMAIL = values.BooleanValue( False, environ_name="SIGNUP_NEW_USER_TO_MARKETING_EMAIL", environ_prefix=None, help_text=( "When enabled, new users are automatically added to mailing list " "for product updates, marketing communications, and customized emails. " ), ) LASUITE_MARKETING = { "BACKEND": values.Value( "lasuite.marketing.backends.dummy.DummyBackend", environ_name="LASUITE_MARKETING_BACKEND", environ_prefix=None, ), "PARAMETERS": values.DictValue( default={}, environ_name="LASUITE_MARKETING_PARAMETERS", environ_prefix=None, ), } CONTENT_METADATA_CACHE_TIMEOUT = values.IntegerValue( 60 * 60 * 24, environ_name="CONTENT_METADATA_CACHE_TIMEOUT", environ_prefix=None ) TREEBEARD_PATH_COMPUTE_RETRY_MAX_ATTEMPTS = values.IntegerValue( 10, environ_name="TREEBEARD_PATH_COMPUTE_RETRY_MAX_ATTEMPTS", environ_prefix=None, ) # -- Profiling (django-silk) --------------------------------------------- # Opt-in request/SQL/cProfile profiler, OFF by default. Turn it on in a # given environment with SILK_ENABLED=1 (typically a throwaway staging pod # loaded via `generate_volumetry`, or local dev) to record, per request, # the SQL it ran with timing + originating stack, and an optional cProfile # you can download as a binary `.prof`. When enabled, `silk` is appended to # INSTALLED_APPS, `SilkyMiddleware` is wired near the top of MIDDLEWARE, and # the UI is served at /silk/ (see impress/urls.py and post_setup below). # # NEVER enable against production with real users: silk persists request # metadata to the database. Request/response BODIES are deliberately never # stored (the two MAX_*_BODY_SIZE = 0 below) so document content, titles and # emails cannot leak into the silk tables — only method, path, headers-free # metadata, SQL and timings are kept. SILK_ENABLED = values.BooleanValue( False, environ_name="SILK_ENABLED", environ_prefix=None ) # Per-request cProfile. Binary output lets you download a `.prof` and open # it in snakeviz / pstats / tuna offline for a full call graph. The binary # is written through the SILKY_STORAGE backend (S3, see STORAGES above), not # the local filesystem, so it works on read-only/ephemeral pods. SILKY_PYTHON_PROFILER = values.BooleanValue( True, environ_name="SILK_PYTHON_PROFILER", environ_prefix=None ) SILKY_PYTHON_PROFILER_BINARY = values.BooleanValue( True, environ_name="SILK_PYTHON_PROFILER_BINARY", environ_prefix=None ) # Under load, record only a sample of requests to bound silk's own overhead # and storage (100 = every request; drop it for a thundering-herd repro). SILKY_INTERCEPT_PERCENT = values.IntegerValue( 100, environ_name="SILK_INTERCEPT_PERCENT", environ_prefix=None ) # Ring-buffer the stored requests so a long load run cannot fill the disk. SILKY_MAX_RECORDED_REQUESTS = values.IntegerValue( 10000, environ_name="SILK_MAX_RECORDED_REQUESTS", environ_prefix=None ) SILKY_MAX_RECORDED_REQUESTS_CHECK_PERCENT = 10 # Record silk's own per-request overhead so you can subtract it. SILKY_META = True # Lock the /silk/ UI behind an authenticated staff session. SILKY_AUTHENTICATION = True SILKY_AUTHORISATION = True # RGPD: never persist request/response bodies (0 bytes kept). SILKY_MAX_REQUEST_BODY_SIZE = 0 SILKY_MAX_RESPONSE_BODY_SIZE = 0 # pylint: disable=invalid-name @property def ENVIRONMENT(self): """Environment in which the application is launched.""" return self.__class__.__name__.lower() # pylint: disable=invalid-name @property def RELEASE(self): """ Return the release information. Delegate to the module function to enable easier testing. """ return get_release() # pylint: disable=invalid-name @property def PARLER_LANGUAGES(self): """ Return languages for Parler computed from the LANGUAGES and LANGUAGE_CODE settings. """ return { self.SITE_ID: tuple({"code": code} for code, _name in self.LANGUAGES), "default": { "fallbacks": [self.LANGUAGE_CODE], "hide_untranslated": False, }, } @classmethod def post_setup(cls): """Post setup configuration. This is the place where you can configure settings that require other settings to be loaded. """ super().post_setup() # The SENTRY_DSN setting should be available to activate sentry for an environment if cls.SENTRY_DSN is not None: sentry_sdk.init( dsn=cls.SENTRY_DSN, environment=cls.__name__.lower(), release=get_release(), integrations=[DjangoIntegration()], ) sentry_sdk.set_tag("application", "backend") # Ignore the logs added by the DockerflowMiddleware ignore_logger("request.summary") if ( cls.OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION and cls.OIDC_ALLOW_DUPLICATE_EMAILS ): raise ValueError( "Both OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION and " "OIDC_ALLOW_DUPLICATE_EMAILS cannot be set to True simultaneously. " ) psycopg_pool_enabled = values.BooleanValue( False, environ_name="DB_PSYCOPG_POOL_ENABLED", environ_prefix="" ) if psycopg_pool_enabled: cls.DATABASES["default"].update( { "OPTIONS": { # https://www.psycopg.org/psycopg3/docs/api/pool.html#psycopg_pool.ConnectionPool "pool": { "min_size": values.IntegerValue( 4, environ_name="DB_PSYCOPG_POOL_MIN_SIZE", environ_prefix=None, ), "max_size": values.IntegerValue( None, environ_name="DB_PSYCOPG_POOL_MAX_SIZE", environ_prefix=None, ), "timeout": values.IntegerValue( 3, environ_name="DB_PSYCOPG_POOL_TIMEOUT", environ_prefix=None, ), } }, } ) if cls.OPENAI_SDK_API_KEY and cls.MISTRAL_SDK_API_KEY: raise ValueError( "Both OPENAI_SDK and MISTRAL_SDK parameters can not be set simultaneously." ) if cls.POSTHOG_KEY is not None: posthog.api_key = cls.POSTHOG_KEY posthog.host = cls.POSTHOG_HOST if cls.SILK_ENABLED: # Activate django-silk only when explicitly turned on for this # environment. Appending here (rather than in INSTALLED_APPS) keeps # silk absent from every environment that does not opt in, including # production. Guards make re-entry (post_setup can run per subclass) # idempotent. if "silk" not in cls.INSTALLED_APPS: cls.INSTALLED_APPS.append("silk") # SilkyMiddleware must be high enough to time the whole request but # after AuthenticationMiddleware so it can attribute request.user; # process_response runs inner-to-outer, so index 1 (just after # SecurityMiddleware) satisfies both. if "silk.middleware.SilkyMiddleware" not in cls.MIDDLEWARE: cls.MIDDLEWARE.insert(1, "silk.middleware.SilkyMiddleware") class Build(Base): """Settings used when the application is built. This environment should not be used to run the application. Just to build it with non-blocking settings. """ SECRET_KEY = values.Value("DummyKey") STORAGES = { "default": { "BACKEND": "django.core.files.storage.FileSystemStorage", }, "staticfiles": { "BACKEND": values.Value( "whitenoise.storage.CompressedManifestStaticFilesStorage", environ_name="STORAGES_STATICFILES_BACKEND", ), }, } class Development(Base): """ Development environment settings We set DEBUG to True and configure the server to respond from all hosts. """ ALLOWED_HOSTS = ["*"] CORS_ALLOW_ALL_ORIGINS = True CSRF_TRUSTED_ORIGINS = values.ListValue( ["http://localhost:8072", "http://localhost:3000"], environ_name="DJANGO_CSRF_TRUSTED_ORIGINS", environ_prefix=None, ) CORS_ALLOW_HEADERS = ( *default_headers, "if-none-match", "if-modified-since", ) CORS_EXPOSE_HEADERS = ["ETag"] DEBUG = True USE_SWAGGER = True CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": values.Value( "redis://redis:6379/2", environ_name="REDIS_URL", environ_prefix=None, ), "TIMEOUT": values.IntegerValue( 30, # timeout in seconds environ_name="CACHES_DEFAULT_TIMEOUT", environ_prefix=None, ), "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", }, }, } def __init__(self): # pylint: disable=invalid-name self.INSTALLED_APPS += ["django_extensions", "drf_spectacular_sidecar"] self.CONTENT_SECURITY_POLICY["EXCLUDE_URL_PREFIXES"] += [ f"/api/{self.API_VERSION}/swagger", f"/api/{self.API_VERSION}/redoc", "/silk", ] class Test(Base): """Test environment settings""" PASSWORD_HASHERS = [ "django.contrib.auth.hashers.MD5PasswordHasher", ] USE_SWAGGER = True # Static files are not used in the test environment # Tests are raising warnings because the /data/static directory does not exist STATIC_ROOT = None CELERY_TASK_ALWAYS_EAGER = values.BooleanValue(True) def __init__(self): # pylint: disable=invalid-name self.INSTALLED_APPS += ["drf_spectacular_sidecar"] class ContinuousIntegration(Test): """ Continuous Integration environment settings nota bene: it should inherit from the Test environment. """ class Production(Base): """ Production environment settings You must define the ALLOWED_HOSTS environment variable in Production configuration (and derived configurations): ALLOWED_HOSTS=["foo.com", "foo.fr"] """ # Security # Add allowed host from environment variables. # The machine hostname is added by default, # it makes the application pingable by a load balancer on the same machine by example ALLOWED_HOSTS = [ *values.ListValue([], environ_name="ALLOWED_HOSTS"), gethostbyname(gethostname()), ] CSRF_TRUSTED_ORIGINS = values.ListValue([]) SECURE_BROWSER_XSS_FILTER = True SECURE_CONTENT_TYPE_NOSNIFF = True # SECURE_PROXY_SSL_HEADER allows to fix the scheme in Django's HttpRequest # object when your application is behind a reverse proxy. # # Keep this SECURE_PROXY_SSL_HEADER configuration only if : # - your Django app is behind a proxy. # - your proxy strips the X-Forwarded-Proto header from all incoming requests # - Your proxy sets the X-Forwarded-Proto header and sends it to Django # # In other cases, you should comment the following line to avoid security issues. # SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") SECURE_HSTS_SECONDS = 60 SECURE_HSTS_PRELOAD = True SECURE_HSTS_INCLUDE_SUBDOMAINS = True SECURE_SSL_REDIRECT = True SECURE_REDIRECT_EXEMPT = [ "^__lbheartbeat__", "^__heartbeat__", ] # Modern browsers require to have the `secure` attribute on cookies with `Samesite=none` CSRF_COOKIE_SECURE = True SESSION_COOKIE_SECURE = True SESSION_CACHE_ALIAS = "session" # Privacy SECURE_REFERRER_POLICY = "same-origin" # Conversion API: Always verify SSL in production CONVERSION_API_SECURE = True # Cache CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": values.Value( "redis://redis:6379/0", environ_name="REDIS_URL", environ_prefix=None, ), "TIMEOUT": values.IntegerValue( 30, # timeout in seconds environ_name="CACHES_DEFAULT_TIMEOUT", environ_prefix=None, ), "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", }, "KEY_PREFIX": values.Value( "docs", environ_name="CACHES_DEFAULT_KEY_PREFIX", environ_prefix=None, ), }, "session": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": values.Value( "redis://redis:6379/0", environ_name="REDIS_URL", environ_prefix=None, ), "TIMEOUT": values.IntegerValue( 30, # timeout in seconds environ_name="CACHES_SESSION_TIMEOUT", environ_prefix=None, ), "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", }, }, } class Feature(Production): """ Feature environment settings nota bene: it should inherit from the Production environment. """ class Staging(Production): """ Staging environment settings nota bene: it should inherit from the Production environment. """ class PreProduction(Production): """ Pre-production environment settings nota bene: it should inherit from the Production environment. """ class Demo(Production): """ Demonstration environment settings nota bene: it should inherit from the Production environment. """ STORAGES = { "default": { "BACKEND": "django.core.files.storage.FileSystemStorage", }, "staticfiles": { "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", }, }