Coverage for backend/django/CoreRoot/settings.py: 88%
124 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-06-23 21:51 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-06-23 21:51 +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
211ROOT_URLCONF = 'CoreRoot.urls'
213TEMPLATES = [
214 {
215 'BACKEND': 'django.template.backends.django.DjangoTemplates',
216 'DIRS': [],
217 'APP_DIRS': True,
218 'OPTIONS': {
219 'context_processors': [
220 'django.template.context_processors.debug',
221 'django.template.context_processors.request',
222 'django.contrib.auth.context_processors.auth',
223 'django.contrib.messages.context_processors.messages',
224 ],
225 },
226 },
227]
229OPEN_TELEMETRY_TRACER_NAME = "ahuora-api"
231EMAIL_BACKEND = os.getenv(
232 "EMAIL_BACKEND",
233 "django.core.mail.backends.console.EmailBackend"
234 if DEBUG
235 else "django.core.mail.backends.smtp.EmailBackend",
236)
237EMAIL_HOST = os.getenv("EMAIL_HOST", "localhost")
238EMAIL_PORT = int(os.getenv("EMAIL_PORT", "25"))
239EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER", "")
240EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD", "")
241EMAIL_USE_TLS = os.getenv("EMAIL_USE_TLS", "false").lower() in ("true", "1")
242EMAIL_USE_SSL = os.getenv("EMAIL_USE_SSL", "false").lower() in ("true", "1")
243DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL", "Ahuora <no-reply@ahuora.local>")
244AHUORA_APP_BASE_URL = os.getenv("AHUORA_APP_BASE_URL", "http://localhost:19006")
246WSGI_APPLICATION = 'CoreRoot.wsgi.application'
248FIXTURE_DIRS = [
249 BASE_DIR / '/fixtures'
250]
251#Renewable Ninja Token
253RENEWABLES_NINJA_TOKEN = os.getenv("RENEWABLES_NINJA_TOKEN")
254if not RENEWABLES_NINJA_TOKEN: 254 ↛ 261line 254 didn't jump to line 261 because the condition on line 254 was always true
255 warnings.warn("RENEWABLES_NINJA_TOKEN is not set in environment variables.", UserWarning)
258# Database
259# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
261DATABASES = {
262 'default': {
263 'ENGINE': 'django.db.backends.postgresql',
264 'NAME': os.getenv("POSTGRES_DB", "postgres"),
265 'HOST': os.getenv("POSTGRES_HOST", "localhost"),
266 'USER': os.getenv("POSTGRES_USER", "postgres"),
267 'PASSWORD': os.getenv("POSTGRES_PASSWORD", "postgres"),
268 'OPTIONS': {
269 'isolation_level': IsolationLevel.READ_COMMITTED,
270 }
271 }
272}
274LOGGING = {
275 "version": 1,
276 "disable_existing_loggers": False,
277 "formatters": {
278 "trace_formatter": {
279 '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
280 'datefmt': '%Y-%m-%d %H:%M:%S', # optional, default is '%Y-%m-%d %H:%M:%S'
281 },
282 },
283 "handlers": {
284 "console": {
285 "class": "logging.StreamHandler",
286 "formatter": "trace_formatter",
287 },
288 },
289 "root": {
290 "handlers": ["console"],
291 "level": "INFO",
292 },
293 "loggers": {
294 "django": {
295 "handlers": ["console"],
296 "level": "INFO",
297 "propagate": False,
298 }
299 },
300}
302# Password validation
303# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
305AUTH_PASSWORD_VALIDATORS = [
306 {
307 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
308 },
309 {
310 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
311 },
312 {
313 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
314 },
315 {
316 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
317 },
318]
320# Password hashers/KDFs to enable.
321# The first entry will be used when creating new passwords.
322PASSWORD_HASHERS = [
323 "django.contrib.auth.hashers.Argon2PasswordHasher"
324]
326# Internationalization
327# https://docs.djangoproject.com/en/4.2/topics/i18n/
329LANGUAGE_CODE = 'en-us'
331TIME_ZONE = 'UTC'
333USE_I18N = True
335USE_TZ = True
337# Static files (CSS, JavaScript, Images)
338# https://docs.djangoproject.com/en/4.2/howto/static-files/
340STATIC_URL = 'static/'
342SILKY_META = True
344# Default primary key field type
345# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
347DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
349AUTH_USER_MODEL = 'authentication_user.User'
351AUTHENTICATION_BACKENDS = [
352 "authentication.remote_user_backend.RemoteUserBackendWithEmail",
353]
355PLATFORM_TEST_EMAIL = os.getenv("PLATFORM_TEST_EMAIL", "test@ahuoratech.app")
357PLATFORM_ADMINISTRATORS_GROUP = os.getenv("PLATFORM_ADMINISTRATORS_GROUP", "/PlatformAdministrators")
358PLATFORM_TESTERS_GROUP = os.getenv("PLATFORM_TESTERS_GROUP", "/PlatformTesters")
359AUTH_GENERAL_SCOPE_KEY = os.getenv(
360 "AUTH_GENERAL_SCOPE_KEY",
361 "human-user",
362)
363AUTH_EXCEL_SCOPE_KEY = os.getenv(
364 "AUTH_EXCEL_SCOPE_KEY",
365 "excel-user"
366)
367KEYCLOAK_ALLOW_MISSING_ACCESS_TOKEN = os.getenv(
368 "KEYCLOAK_ALLOW_MISSING_ACCESS_TOKEN",
369 "false",
370).lower() in ("true", "1")
371KEYCLOAK_TOKEN_EXCHANGE_ENDPOINT = os.getenv("KEYCLOAK_TOKEN_EXCHANGE_ENDPOINT", "")
372KEYCLOAK_TOKEN_EXCHANGE_CLIENT_ID = os.getenv("KEYCLOAK_TOKEN_EXCHANGE_CLIENT_ID", "")
373KEYCLOAK_TOKEN_EXCHANGE_CLIENT_SECRET = os.getenv("KEYCLOAK_TOKEN_EXCHANGE_CLIENT_SECRET", "")
374KEYCLOAK_TOKEN_EXCHANGE_AUDIENCE = os.getenv("KEYCLOAK_TOKEN_EXCHANGE_AUDIENCE", "")
375KEYCLOAK_TOKEN_EXCHANGE_TIMEOUT_SECONDS = int(os.getenv("KEYCLOAK_TOKEN_EXCHANGE_TIMEOUT_SECONDS", "10"))
377REMOTE_USER_HEADER = "HTTP_X_AUTH_REQUEST_USER"
378ASGI_REMOTE_USER_HEADER = "x-auth-request-user"
379REMOTE_USER_EMAIL_HEADER = "HTTP_X_AUTH_REQUEST_EMAIL"
380ASGI_REMOTE_USER_EMAIL_HEADER = "x-auth-request-email"
381REMOTE_USER_GROUPS_HEADER = "HTTP_X_AUTH_REQUEST_GROUPS"
382ASGI_REMOTE_USER_GROUPS_HEADER = "x-auth-request-groups"
383REMOTE_USER_ACCESS_TOKEN_HEADER = "HTTP_X_AUTH_REQUEST_ACCESS_TOKEN"
384ASGI_REMOTE_USER_ACCESS_TOKEN_HEADER = "x-auth-request-access-token"
386def __insert_middleware(middleware_name: str, before_middleware_name: str):
387 index = MIDDLEWARE.index(before_middleware_name)
388 MIDDLEWARE.insert(index, middleware_name)
390def set_dapr_endpoints():
391 # Monkey-patch Dapr SDK config to avoid the need to set environment variables externally when
392 # running the API server. We still check for environment variables to allow for overriding.
393 from dapr.conf import settings
395 if os.getenv("DAPR_HTTP_ENDPOINT") is None:
396 settings.DAPR_HTTP_ENDPOINT = "http://localhost:3501"
398 if os.getenv("DAPR_GRPC_ENDPOINT") is None:
399 settings.DAPR_GRPC_ENDPOINT = "localhost:50001"
401if DEBUG: 401 ↛ 418line 401 didn't jump to line 418 because the condition on line 401 was always true
402 set_dapr_endpoints()
404 # Add the dummy auth header middleware and the remote user middleware to the middleware list
405 __insert_middleware(
406 'authentication.middleware.dummy_auth_header_middleware',
407 'authentication.middleware.AhuoraRemoteUserMiddleware'
408 )
410 # Only allow profiling with Silk if we're both in debug mode and profiling is enabled
411 if PROFILING_ENABLED: 411 ↛ 412line 411 didn't jump to line 412 because the condition on line 411 was never true
412 __insert_middleware(
413 'silk.middleware.SilkyMiddleware',
414 'django.contrib.auth.middleware.AuthenticationMiddleware'
415 )
416 INSTALLED_APPS.append('silk')
418BLEACH_ALLOWED_TAGS = [
419 'a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'strong', 'ul', 'p', 'br', 'u'
420]
421BLEACH_ALLOWED_ATTRIBUTES = {
422 '*': ['class', 'id', 'style'],
423 'a': ['href', 'rel'],
424}
425BLEACH_ALLOWED_STYLES = [
426 'color', 'font-weight', 'text-decoration',
427]
428BLEACH_STRIP_TAGS = True
429BLEACH_STRIP_COMMENTS = True