Coverage for backend/django/CoreRoot/settings.py: 88%
124 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-05-13 02:47 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-05-13 02:47 +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 'django_bleach',
139 'diagnostics',
140]
142CHANNEL_LAYERS = {
143 "default": {
144 "BACKEND": "channels_redis.core.RedisChannelLayer",
145 "CONFIG": {
146 "hosts": [(
147 os.getenv("CHANNELS_REDIS_HOST", "localhost"),
148 os.getenv("CHANNELS_REDIS_PORT", 6379))
149 ],
150 },
151 },
152}
154REST_FRAMEWORK = {
155 'DEFAULT_AUTHENTICATION_CLASSES': (
156 'authentication.custom_drf_authentication.AhuoraRemoteUserAuthentication',
157 ),
158 'EXCEPTION_HANDLER': 'core.exceptions.otel_trace_exception_handler',
159 'DEFAULT_RENDERER_CLASSES': (
160 'rest_framework.renderers.JSONRenderer',
161 ),
162 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
163 'DEFAULT_PERMISSION_CLASSES': [
164 'rest_framework.permissions.IsAuthenticated',
165 'authentication.permissions.HasHumanUserAccess',
166 ],
167 'DEFAULT_PARSER_CLASSES': [
168 'rest_framework.parsers.JSONParser',
169 'core.parsers.CloudEventsParser'
170 ]
171}
173SPECTACULAR_SETTINGS = {
174 'TITLE': 'Ahuora API',
175 'DESCRIPTION': 'Your project description',
176 'VERSION': '1.0.0',
177 'SERVE_INCLUDE_SCHEMA': False,
178}
180MIDDLEWARE = [
181 'django.middleware.security.SecurityMiddleware',
182 'django.contrib.sessions.middleware.SessionMiddleware',
183 'corsheaders.middleware.CorsMiddleware',
184 'django.middleware.common.CommonMiddleware',
185 'django.middleware.csrf.CsrfViewMiddleware',
186 'django.contrib.auth.middleware.AuthenticationMiddleware',
187 'authentication.middleware.AhuoraRemoteUserMiddleware',
188 'django.contrib.messages.middleware.MessageMiddleware',
189 'django.middleware.clickjacking.XFrameOptionsMiddleware',
190]
192SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
194CORS_ALLOWED_ORIGINS = [
195 "http://localhost:19006",# Dev server
196 "http://127.0.0.1:19006",
197 "http://localhost:19005",# E2E Test server
198 "http://127.0.0.1:19005",
199 "http://localhost:3000",
200 "http://front-end:19006",
201 "http://172.29.171.74:19006",
202 "https://ahuora.org.nz", # Production server
203 "https://www.ahuora.org.nz",
204 "http://ahuora.org.nz",
205 "http://www.ahuora.org.nz"
206]
208CORS_ALLOW_CREDENTIALS = True
210ROOT_URLCONF = 'CoreRoot.urls'
212TEMPLATES = [
213 {
214 'BACKEND': 'django.template.backends.django.DjangoTemplates',
215 'DIRS': [],
216 'APP_DIRS': True,
217 'OPTIONS': {
218 'context_processors': [
219 'django.template.context_processors.debug',
220 'django.template.context_processors.request',
221 'django.contrib.auth.context_processors.auth',
222 'django.contrib.messages.context_processors.messages',
223 ],
224 },
225 },
226]
228OPEN_TELEMETRY_TRACER_NAME = "ahuora-api"
230EMAIL_BACKEND = os.getenv(
231 "EMAIL_BACKEND",
232 "django.core.mail.backends.console.EmailBackend"
233 if DEBUG
234 else "django.core.mail.backends.smtp.EmailBackend",
235)
236EMAIL_HOST = os.getenv("EMAIL_HOST", "localhost")
237EMAIL_PORT = int(os.getenv("EMAIL_PORT", "25"))
238EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER", "")
239EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD", "")
240EMAIL_USE_TLS = os.getenv("EMAIL_USE_TLS", "false").lower() in ("true", "1")
241EMAIL_USE_SSL = os.getenv("EMAIL_USE_SSL", "false").lower() in ("true", "1")
242DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL", "Ahuora <no-reply@ahuora.local>")
243AHUORA_APP_BASE_URL = os.getenv("AHUORA_APP_BASE_URL", "http://localhost:19006")
245WSGI_APPLICATION = 'CoreRoot.wsgi.application'
247FIXTURE_DIRS = [
248 BASE_DIR / '/fixtures'
249]
250#Renewable Ninja Token
252RENEWABLES_NINJA_TOKEN = os.getenv("RENEWABLES_NINJA_TOKEN")
253if not RENEWABLES_NINJA_TOKEN: 253 ↛ 260line 253 didn't jump to line 260 because the condition on line 253 was always true
254 warnings.warn("RENEWABLES_NINJA_TOKEN is not set in environment variables.", UserWarning)
257# Database
258# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
260DATABASES = {
261 'default': {
262 'ENGINE': 'django.db.backends.postgresql',
263 'NAME': os.getenv("POSTGRES_DB", "postgres"),
264 'HOST': os.getenv("POSTGRES_HOST", "localhost"),
265 'USER': os.getenv("POSTGRES_USER", "postgres"),
266 'PASSWORD': os.getenv("POSTGRES_PASSWORD", "postgres"),
267 'OPTIONS': {
268 'isolation_level': IsolationLevel.READ_COMMITTED,
269 }
270 }
271}
273LOGGING = {
274 "version": 1,
275 "disable_existing_loggers": False,
276 "formatters": {
277 "trace_formatter": {
278 '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
279 'datefmt': '%Y-%m-%d %H:%M:%S', # optional, default is '%Y-%m-%d %H:%M:%S'
280 },
281 },
282 "handlers": {
283 "console": {
284 "class": "logging.StreamHandler",
285 "formatter": "trace_formatter",
286 },
287 },
288 "root": {
289 "handlers": ["console"],
290 "level": "INFO",
291 },
292 "loggers": {
293 "django": {
294 "handlers": ["console"],
295 "level": "INFO",
296 "propagate": False,
297 }
298 },
299}
301# Password validation
302# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
304AUTH_PASSWORD_VALIDATORS = [
305 {
306 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
307 },
308 {
309 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
310 },
311 {
312 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
313 },
314 {
315 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
316 },
317]
319# Password hashers/KDFs to enable.
320# The first entry will be used when creating new passwords.
321PASSWORD_HASHERS = [
322 "django.contrib.auth.hashers.Argon2PasswordHasher"
323]
325# Internationalization
326# https://docs.djangoproject.com/en/4.2/topics/i18n/
328LANGUAGE_CODE = 'en-us'
330TIME_ZONE = 'UTC'
332USE_I18N = True
334USE_TZ = True
336# Static files (CSS, JavaScript, Images)
337# https://docs.djangoproject.com/en/4.2/howto/static-files/
339STATIC_URL = 'static/'
341SILKY_META = True
343# Default primary key field type
344# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
346DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
348AUTH_USER_MODEL = 'authentication_user.User'
350AUTHENTICATION_BACKENDS = [
351 "authentication.remote_user_backend.RemoteUserBackendWithEmail",
352]
354PLATFORM_TEST_EMAIL = os.getenv("PLATFORM_TEST_EMAIL", "test@ahuoratech.app")
356PLATFORM_ADMINISTRATORS_GROUP = os.getenv("PLATFORM_ADMINISTRATORS_GROUP", "/PlatformAdministrators")
357PLATFORM_TESTERS_GROUP = os.getenv("PLATFORM_TESTERS_GROUP", "/PlatformTesters")
358AUTH_GENERAL_SCOPE_KEY = os.getenv(
359 "AUTH_GENERAL_SCOPE_KEY",
360 "human-user",
361)
362AUTH_EXCEL_SCOPE_KEY = os.getenv(
363 "AUTH_EXCEL_SCOPE_KEY",
364 "excel-user"
365)
366KEYCLOAK_ALLOW_MISSING_ACCESS_TOKEN = os.getenv(
367 "KEYCLOAK_ALLOW_MISSING_ACCESS_TOKEN",
368 "false",
369).lower() in ("true", "1")
370KEYCLOAK_TOKEN_EXCHANGE_ENDPOINT = os.getenv("KEYCLOAK_TOKEN_EXCHANGE_ENDPOINT", "")
371KEYCLOAK_TOKEN_EXCHANGE_CLIENT_ID = os.getenv("KEYCLOAK_TOKEN_EXCHANGE_CLIENT_ID", "")
372KEYCLOAK_TOKEN_EXCHANGE_CLIENT_SECRET = os.getenv("KEYCLOAK_TOKEN_EXCHANGE_CLIENT_SECRET", "")
373KEYCLOAK_TOKEN_EXCHANGE_AUDIENCE = os.getenv("KEYCLOAK_TOKEN_EXCHANGE_AUDIENCE", "")
374KEYCLOAK_TOKEN_EXCHANGE_TIMEOUT_SECONDS = int(os.getenv("KEYCLOAK_TOKEN_EXCHANGE_TIMEOUT_SECONDS", "10"))
376REMOTE_USER_HEADER = "HTTP_X_AUTH_REQUEST_USER"
377ASGI_REMOTE_USER_HEADER = "x-auth-request-user"
378REMOTE_USER_EMAIL_HEADER = "HTTP_X_AUTH_REQUEST_EMAIL"
379ASGI_REMOTE_USER_EMAIL_HEADER = "x-auth-request-email"
380REMOTE_USER_GROUPS_HEADER = "HTTP_X_AUTH_REQUEST_GROUPS"
381ASGI_REMOTE_USER_GROUPS_HEADER = "x-auth-request-groups"
382REMOTE_USER_ACCESS_TOKEN_HEADER = "HTTP_X_AUTH_REQUEST_ACCESS_TOKEN"
383ASGI_REMOTE_USER_ACCESS_TOKEN_HEADER = "x-auth-request-access-token"
385def __insert_middleware(middleware_name: str, before_middleware_name: str):
386 index = MIDDLEWARE.index(before_middleware_name)
387 MIDDLEWARE.insert(index, middleware_name)
389def set_dapr_endpoints():
390 # Monkey-patch Dapr SDK config to avoid the need to set environment variables externally when
391 # running the API server. We still check for environment variables to allow for overriding.
392 from dapr.conf import settings
394 if os.getenv("DAPR_HTTP_ENDPOINT") is None:
395 settings.DAPR_HTTP_ENDPOINT = "http://localhost:3501"
397 if os.getenv("DAPR_GRPC_ENDPOINT") is None:
398 settings.DAPR_GRPC_ENDPOINT = "localhost:50001"
400if DEBUG: 400 ↛ 417line 400 didn't jump to line 417 because the condition on line 400 was always true
401 set_dapr_endpoints()
403 # Add the dummy auth header middleware and the remote user middleware to the middleware list
404 __insert_middleware(
405 'authentication.middleware.dummy_auth_header_middleware',
406 'authentication.middleware.AhuoraRemoteUserMiddleware'
407 )
409 # Only allow profiling with Silk if we're both in debug mode and profiling is enabled
410 if PROFILING_ENABLED: 410 ↛ 411line 410 didn't jump to line 411 because the condition on line 410 was never true
411 __insert_middleware(
412 'silk.middleware.SilkyMiddleware',
413 'django.contrib.auth.middleware.AuthenticationMiddleware'
414 )
415 INSTALLED_APPS.append('silk')
417BLEACH_ALLOWED_TAGS = [
418 'a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'strong', 'ul', 'p', 'br', 'u'
419]
420BLEACH_ALLOWED_ATTRIBUTES = {
421 '*': ['class', 'id', 'style'],
422 'a': ['href', 'rel'],
423}
424BLEACH_ALLOWED_STYLES = [
425 'color', 'font-weight', 'text-decoration',
426]
427BLEACH_STRIP_TAGS = True
428BLEACH_STRIP_COMMENTS = True