Coverage for backend/django/CoreRoot/settings.py: 88%
125 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-07-22 05:22 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-07-22 05:22 +0000
1"""
2Django settings for CoreRoot project.
4Generated by 'django-admin startproject' using Django 4.2.7.
6For more information on this file, see
7https://docs.djangoproject.com/en/4.2/topics/settings/
9For the full list of settings and their values, see
10https://docs.djangoproject.com/en/4.2/ref/settings/
11"""
12import logging
13from pathlib import Path
14from django.db.backends.postgresql.psycopg_any import IsolationLevel
15import os
16import warnings
17from dotenv import load_dotenv
19# Build paths inside the project like this: BASE_DIR / 'subdir'.
20BASE_DIR = Path(__file__).resolve().parent.parent
22load_dotenv(dotenv_path=BASE_DIR / ".env") # optional base
24DEBUG = os.getenv("PRODUCTION", "False").lower() in ('false', '0')
26if DEBUG: 26 ↛ 29line 26 didn't jump to line 29 because the condition on line 26 was always true
27 load_dotenv(dotenv_path=BASE_DIR / ".env.development.local")
29DIAGNOSTICS_RULES_PATH = os.getenv(
30 "DIAGNOSTICS_RULES_PATH",
31 str((BASE_DIR / "diagnostics" / "rules" / "validation_rules.jdm").resolve()),
32)
34# Quick-start development settings - unsuitable for production
35# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
37# SECURITY WARNING: keep the secret key used in production secret!
38SECRET_KEY = 'django-insecure-6zboi37mw#!#6e^qx2gk#0t@wqv5_3*yh8u@^^smbzgk6enq^('
40# SECURITY WARNING: don't run with debug turned on in production!
43DAPR_APP_API_TOKEN = os.getenv("DAPR_APP_API_TOKEN", None)
44if DEBUG and DAPR_APP_API_TOKEN is None:
45 DAPR_APP_API_TOKEN = "BlYMxKQgDWt+NDVa7NsNBw==" # This must be the same as defined in django-dapr's env variable in the docker-compose file
48def _get_seaweed_s3_settings() -> dict[str, str | bool | None]:
49 debug_defaults = {
50 "endpoint": "http://127.0.0.1:8333",
51 "public_endpoint": "http://127.0.0.1:8333",
52 "access_key": "seaweedfs",
53 "secret_key": "seaweedfs-secret",
54 "bucket": "ahuora-csv-uploads",
55 } if DEBUG else {}
57 return {
58 "endpoint": os.getenv("SEAWEED_S3_ENDPOINT", debug_defaults.get("endpoint")),
59 "public_endpoint": os.getenv(
60 "SEAWEED_S3_PUBLIC_ENDPOINT",
61 debug_defaults.get("public_endpoint"),
62 ),
63 "access_key": os.getenv("SEAWEED_S3_ACCESS_KEY", debug_defaults.get("access_key")),
64 "secret_key": os.getenv("SEAWEED_S3_SECRET_KEY", debug_defaults.get("secret_key")),
65 "bucket": os.getenv("SEAWEED_S3_BUCKET", debug_defaults.get("bucket")),
66 "region": os.getenv("SEAWEED_S3_REGION", "us-east-1"),
67 "force_path_style": os.getenv("SEAWEED_S3_FORCE_PATH_STYLE", "true").lower() in ('true', '1'),
68 }
71SEAWEED_S3 = _get_seaweed_s3_settings()
72SEAWEED_S3_ENDPOINT = SEAWEED_S3["endpoint"]
73SEAWEED_S3_PUBLIC_ENDPOINT = SEAWEED_S3["public_endpoint"]
74SEAWEED_S3_ACCESS_KEY = SEAWEED_S3["access_key"]
75SEAWEED_S3_SECRET_KEY = SEAWEED_S3["secret_key"]
76SEAWEED_S3_BUCKET = SEAWEED_S3["bucket"]
77SEAWEED_S3_REGION = SEAWEED_S3["region"]
78SEAWEED_S3_FORCE_PATH_STYLE = SEAWEED_S3["force_path_style"]
80PROFILING_ENABLED = os.getenv("PROFILING_ENABLED", "False").lower() in ('true', '1')
81SEAWEED_ML_UPLOAD_RETENTION_DAYS = int(os.getenv("SEAWEED_ML_UPLOAD_RETENTION_DAYS", "7"))
82SEAWEED_STALE_MULTIPART_UPLOAD_HOURS = int(os.getenv("SEAWEED_STALE_MULTIPART_UPLOAD_HOURS", "24"))
84# Get string list from comma-separated list of allowed hosts,
85# e.g. "localhost,api.ahuora.co.nz,127.0.0.1" turns into ["localhost", "api.ahuora.co.nz", "127.0.0.1"]
86ALLOWED_HOSTS = list(filter(None, os.getenv("ALLOWED_HOSTS", "").split(",")))
87if DEBUG: 87 ↛ 90line 87 didn't jump to line 90 because the condition on line 87 was always true
88 ALLOWED_HOSTS += ["host.docker.internal", "localhost", "127.0.0.1"]
90CODE_COVERAGE_ENABLED = os.getenv("CODE_COVERAGE_ENABLED", "False").lower() in ('true', '1')
91if CODE_COVERAGE_ENABLED:
92 # Code coverage can be run for Django in two ways:
93 # 1. Using `coverage run ...` to start the server
94 # 2. By setting the COVERAGE_PROCESS_START environment variable and manually starting coverage.
95 # Number 1 is used when running tests, while number 2 is used when running the server via Granian,
96 # as `coverage run ...` does not seem to be able to discover Python subprocesses started by a non-Python program.
98 direct_code_coverage_active = os.getenv("COVERAGE_PROCESS_CONFIG")
99 indirect_code_coverage_active = os.getenv("COVERAGE_PROCESS_START")
101 if direct_code_coverage_active: 101 ↛ 103line 101 didn't jump to line 103 because the condition on line 101 was always true
102 logging.info("Detected code coverage running via `coverage run ...`")
103 elif indirect_code_coverage_active:
104 logging.info("Detected code coverage running via COVERAGE_PROCESS_START environment variable (likely sitepackages script injection)")
105 else:
106 import coverage
108 # Set default coverage config file name
109 os.environ.setdefault("COVERAGE_PROCESS_START", ".coveragerc")
111 coverage.process_startup()
112 logging.info("Manually started code coverage measurement")
114 if not DEBUG: 114 ↛ 115line 114 didn't jump to line 115 because the condition on line 114 was never true
115 warnings.warn("Code coverage measurement running in production mode. This may impact performance.")
117# Application definition
119INSTALLED_APPS = [
120 'django.contrib.auth',
121 'django.contrib.contenttypes',
122 'django.contrib.sessions',
123 'django.contrib.messages',
124 'django.contrib.staticfiles',
125 'django_extensions',
126 'rest_framework',
127 'drf_spectacular',
128 'corsheaders',
129 'core',
130 'core.auxiliary',
131 'authentication',
132 'authentication.user',
133 'flowsheetInternals',
134 'flowsheetInternals.unitops',
135 'flowsheetInternals.graphicData',
136 'flowsheetInternals.propertyPackages',
137 'PinchAnalysis',
138 'Economics',
139 'django_bleach',
140 'diagnostics',
141]
143CHANNEL_LAYERS = {
144 "default": {
145 "BACKEND": "channels_redis.core.RedisChannelLayer",
146 "CONFIG": {
147 "hosts": [(
148 os.getenv("CHANNELS_REDIS_HOST", "localhost"),
149 os.getenv("CHANNELS_REDIS_PORT", 6379))
150 ],
151 },
152 },
153}
155REST_FRAMEWORK = {
156 'DEFAULT_AUTHENTICATION_CLASSES': (
157 'authentication.custom_drf_authentication.AhuoraRemoteUserAuthentication',
158 ),
159 'EXCEPTION_HANDLER': 'core.exceptions.otel_trace_exception_handler',
160 'DEFAULT_RENDERER_CLASSES': (
161 'rest_framework.renderers.JSONRenderer',
162 ),
163 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
164 'DEFAULT_PERMISSION_CLASSES': [
165 'rest_framework.permissions.IsAuthenticated',
166 'authentication.permissions.HasHumanUserAccess',
167 ],
168 'DEFAULT_PARSER_CLASSES': [
169 'rest_framework.parsers.JSONParser',
170 'core.parsers.CloudEventsParser'
171 ]
172}
174SPECTACULAR_SETTINGS = {
175 'TITLE': 'Ahuora API',
176 'DESCRIPTION': 'Your project description',
177 'VERSION': '1.0.0',
178 'SERVE_INCLUDE_SCHEMA': False,
179}
181MIDDLEWARE = [
182 'django.middleware.security.SecurityMiddleware',
183 'django.contrib.sessions.middleware.SessionMiddleware',
184 'corsheaders.middleware.CorsMiddleware',
185 'django.middleware.common.CommonMiddleware',
186 'django.middleware.csrf.CsrfViewMiddleware',
187 'django.contrib.auth.middleware.AuthenticationMiddleware',
188 'authentication.middleware.AhuoraRemoteUserMiddleware',
189 'django.contrib.messages.middleware.MessageMiddleware',
190 'django.middleware.clickjacking.XFrameOptionsMiddleware',
191]
193SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
195CORS_ALLOWED_ORIGINS = [
196 "http://localhost:19006",# Dev server
197 "http://127.0.0.1:19006",
198 "http://localhost:19005",# E2E Test server
199 "http://127.0.0.1:19005",
200 "http://localhost:3000",
201 "http://front-end:19006",
202 "http://172.29.171.74:19006",
203 "https://ahuora.org.nz", # Production server
204 "https://www.ahuora.org.nz",
205 "http://ahuora.org.nz",
206 "http://www.ahuora.org.nz"
207]
209CORS_ALLOW_CREDENTIALS = True
211CORS_EXPOSE_HEADERS = ["X-Flowsheet-Edit-Operation"]
213ROOT_URLCONF = 'CoreRoot.urls'
215TEMPLATES = [
216 {
217 'BACKEND': 'django.template.backends.django.DjangoTemplates',
218 'DIRS': [],
219 'APP_DIRS': True,
220 'OPTIONS': {
221 'context_processors': [
222 'django.template.context_processors.debug',
223 'django.template.context_processors.request',
224 'django.contrib.auth.context_processors.auth',
225 'django.contrib.messages.context_processors.messages',
226 ],
227 },
228 },
229]
231OPEN_TELEMETRY_TRACER_NAME = "ahuora-api"
233EMAIL_BACKEND = os.getenv(
234 "EMAIL_BACKEND",
235 "django.core.mail.backends.console.EmailBackend"
236 if DEBUG
237 else "django.core.mail.backends.smtp.EmailBackend",
238)
239EMAIL_HOST = os.getenv("EMAIL_HOST", "localhost")
240EMAIL_PORT = int(os.getenv("EMAIL_PORT", "25"))
241EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER", "")
242EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD", "")
243EMAIL_USE_TLS = os.getenv("EMAIL_USE_TLS", "false").lower() in ("true", "1")
244EMAIL_USE_SSL = os.getenv("EMAIL_USE_SSL", "false").lower() in ("true", "1")
245DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL", "Ahuora <no-reply@ahuora.local>")
246AHUORA_APP_BASE_URL = os.getenv("AHUORA_APP_BASE_URL", "http://localhost:19006")
248WSGI_APPLICATION = 'CoreRoot.wsgi.application'
250FIXTURE_DIRS = [
251 BASE_DIR / '/fixtures'
252]
253#Renewable Ninja Token
255RENEWABLES_NINJA_TOKEN = os.getenv("RENEWABLES_NINJA_TOKEN")
256if not RENEWABLES_NINJA_TOKEN: 256 ↛ 263line 256 didn't jump to line 263 because the condition on line 256 was always true
257 warnings.warn("RENEWABLES_NINJA_TOKEN is not set in environment variables.", UserWarning)
260# Database
261# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
263DATABASES = {
264 'default': {
265 'ENGINE': 'django.db.backends.postgresql',
266 'NAME': os.getenv("POSTGRES_DB", "postgres"),
267 'HOST': os.getenv("POSTGRES_HOST", "localhost"),
268 'USER': os.getenv("POSTGRES_USER", "postgres"),
269 'PASSWORD': os.getenv("POSTGRES_PASSWORD", "postgres"),
270 'OPTIONS': {
271 'isolation_level': IsolationLevel.READ_COMMITTED,
272 }
273 }
274}
276LOGGING = {
277 "version": 1,
278 "disable_existing_loggers": False,
279 "formatters": {
280 "trace_formatter": {
281 'format': '[%(asctime)s] %(levelname)s [%(filename)s:%(lineno)d] [trace_id=%(otelTraceID)s span_id=%(otelSpanID)s] [%(funcName)s] %(message)s', # optional, default is logging.BASIC_FORMAT
282 'datefmt': '%Y-%m-%d %H:%M:%S', # optional, default is '%Y-%m-%d %H:%M:%S'
283 },
284 },
285 "handlers": {
286 "console": {
287 "class": "logging.StreamHandler",
288 "formatter": "trace_formatter",
289 },
290 },
291 "root": {
292 "handlers": ["console"],
293 "level": "INFO",
294 },
295 "loggers": {
296 "django": {
297 "handlers": ["console"],
298 "level": "INFO",
299 "propagate": False,
300 }
301 },
302}
304# Password validation
305# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
307AUTH_PASSWORD_VALIDATORS = [
308 {
309 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
310 },
311 {
312 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
313 },
314 {
315 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
316 },
317 {
318 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
319 },
320]
322# Password hashers/KDFs to enable.
323# The first entry will be used when creating new passwords.
324PASSWORD_HASHERS = [
325 "django.contrib.auth.hashers.Argon2PasswordHasher"
326]
328# Internationalization
329# https://docs.djangoproject.com/en/4.2/topics/i18n/
331LANGUAGE_CODE = 'en-us'
333TIME_ZONE = 'UTC'
335USE_I18N = True
337USE_TZ = True
339# Static files (CSS, JavaScript, Images)
340# https://docs.djangoproject.com/en/4.2/howto/static-files/
342STATIC_URL = 'static/'
344SILKY_META = True
346# Default primary key field type
347# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
349DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
351AUTH_USER_MODEL = 'authentication_user.User'
353AUTHENTICATION_BACKENDS = [
354 "authentication.remote_user_backend.RemoteUserBackendWithEmail",
355]
357PLATFORM_TEST_EMAIL = os.getenv("PLATFORM_TEST_EMAIL", "test@ahuoratech.app")
359PLATFORM_ADMINISTRATORS_GROUP = os.getenv("PLATFORM_ADMINISTRATORS_GROUP", "/PlatformAdministrators")
360PLATFORM_TESTERS_GROUP = os.getenv("PLATFORM_TESTERS_GROUP", "/PlatformTesters")
361AUTH_GENERAL_SCOPE_KEY = os.getenv(
362 "AUTH_GENERAL_SCOPE_KEY",
363 "human-user",
364)
365AUTH_EXCEL_SCOPE_KEY = os.getenv(
366 "AUTH_EXCEL_SCOPE_KEY",
367 "excel-user"
368)
369KEYCLOAK_ALLOW_MISSING_ACCESS_TOKEN = os.getenv(
370 "KEYCLOAK_ALLOW_MISSING_ACCESS_TOKEN",
371 "false",
372).lower() in ("true", "1")
373KEYCLOAK_TOKEN_EXCHANGE_ENDPOINT = os.getenv("KEYCLOAK_TOKEN_EXCHANGE_ENDPOINT", "")
374KEYCLOAK_TOKEN_EXCHANGE_CLIENT_ID = os.getenv("KEYCLOAK_TOKEN_EXCHANGE_CLIENT_ID", "")
375KEYCLOAK_TOKEN_EXCHANGE_CLIENT_SECRET = os.getenv("KEYCLOAK_TOKEN_EXCHANGE_CLIENT_SECRET", "")
376KEYCLOAK_TOKEN_EXCHANGE_AUDIENCE = os.getenv("KEYCLOAK_TOKEN_EXCHANGE_AUDIENCE", "")
377KEYCLOAK_TOKEN_EXCHANGE_TIMEOUT_SECONDS = int(os.getenv("KEYCLOAK_TOKEN_EXCHANGE_TIMEOUT_SECONDS", "10"))
379REMOTE_USER_HEADER = "HTTP_X_AUTH_REQUEST_USER"
380ASGI_REMOTE_USER_HEADER = "x-auth-request-user"
381REMOTE_USER_EMAIL_HEADER = "HTTP_X_AUTH_REQUEST_EMAIL"
382ASGI_REMOTE_USER_EMAIL_HEADER = "x-auth-request-email"
383REMOTE_USER_GROUPS_HEADER = "HTTP_X_AUTH_REQUEST_GROUPS"
384ASGI_REMOTE_USER_GROUPS_HEADER = "x-auth-request-groups"
385REMOTE_USER_ACCESS_TOKEN_HEADER = "HTTP_X_AUTH_REQUEST_ACCESS_TOKEN"
386ASGI_REMOTE_USER_ACCESS_TOKEN_HEADER = "x-auth-request-access-token"
388def __insert_middleware(middleware_name: str, before_middleware_name: str):
389 index = MIDDLEWARE.index(before_middleware_name)
390 MIDDLEWARE.insert(index, middleware_name)
392def set_dapr_endpoints():
393 # Monkey-patch Dapr SDK config to avoid the need to set environment variables externally when
394 # running the API server. We still check for environment variables to allow for overriding.
395 from dapr.conf import settings
397 if os.getenv("DAPR_HTTP_ENDPOINT") is None:
398 settings.DAPR_HTTP_ENDPOINT = "http://localhost:3501"
400 if os.getenv("DAPR_GRPC_ENDPOINT") is None:
401 settings.DAPR_GRPC_ENDPOINT = "localhost:50001"
403if DEBUG: 403 ↛ 420line 403 didn't jump to line 420 because the condition on line 403 was always true
404 set_dapr_endpoints()
406 # Add the dummy auth header middleware and the remote user middleware to the middleware list
407 __insert_middleware(
408 'authentication.middleware.dummy_auth_header_middleware',
409 'authentication.middleware.AhuoraRemoteUserMiddleware'
410 )
412 # Only allow profiling with Silk if we're both in debug mode and profiling is enabled
413 if PROFILING_ENABLED: 413 ↛ 414line 413 didn't jump to line 414 because the condition on line 413 was never true
414 __insert_middleware(
415 'silk.middleware.SilkyMiddleware',
416 'django.contrib.auth.middleware.AuthenticationMiddleware'
417 )
418 INSTALLED_APPS.append('silk')
420BLEACH_ALLOWED_TAGS = [
421 'a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'strong', 'ul', 'p', 'br', 'u'
422]
423BLEACH_ALLOWED_ATTRIBUTES = {
424 '*': ['class', 'id', 'style'],
425 'a': ['href', 'rel'],
426}
427BLEACH_ALLOWED_STYLES = [
428 'color', 'font-weight', 'text-decoration',
429]
430BLEACH_STRIP_TAGS = True
431BLEACH_STRIP_COMMENTS = True