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

1""" 

2Django settings for CoreRoot project. 

3 

4Generated by 'django-admin startproject' using Django 4.2.7. 

5 

6For more information on this file, see 

7https://docs.djangoproject.com/en/4.2/topics/settings/ 

8 

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 

18 

19# Build paths inside the project like this: BASE_DIR / 'subdir'. 

20BASE_DIR = Path(__file__).resolve().parent.parent 

21 

22load_dotenv(dotenv_path=BASE_DIR / ".env") # optional base 

23 

24DEBUG = os.getenv("PRODUCTION", "False").lower() in ('false', '0') 

25 

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") 

28 

29DIAGNOSTICS_RULES_PATH = os.getenv( 

30 "DIAGNOSTICS_RULES_PATH", 

31 str((BASE_DIR / "diagnostics" / "rules" / "validation_rules.jdm").resolve()), 

32) 

33 

34# Quick-start development settings - unsuitable for production 

35# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ 

36 

37# SECURITY WARNING: keep the secret key used in production secret! 

38SECRET_KEY = 'django-insecure-6zboi37mw#!#6e^qx2gk#0t@wqv5_3*yh8u@^^smbzgk6enq^(' 

39 

40# SECURITY WARNING: don't run with debug turned on in production! 

41 

42 

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 

46 

47 

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 {} 

56 

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 } 

69 

70 

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"] 

79 

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")) 

83 

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"] 

89 

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. 

97 

98 direct_code_coverage_active = os.getenv("COVERAGE_PROCESS_CONFIG") 

99 indirect_code_coverage_active = os.getenv("COVERAGE_PROCESS_START") 

100 

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 

107 

108 # Set default coverage config file name 

109 os.environ.setdefault("COVERAGE_PROCESS_START", ".coveragerc") 

110 

111 coverage.process_startup() 

112 logging.info("Manually started code coverage measurement") 

113 

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.") 

116 

117# Application definition 

118 

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] 

141 

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} 

153 

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} 

172 

173SPECTACULAR_SETTINGS = { 

174 'TITLE': 'Ahuora API', 

175 'DESCRIPTION': 'Your project description', 

176 'VERSION': '1.0.0', 

177 'SERVE_INCLUDE_SCHEMA': False, 

178} 

179 

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] 

191 

192SESSION_ENGINE = 'django.contrib.sessions.backends.cache' 

193 

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] 

207 

208CORS_ALLOW_CREDENTIALS = True 

209 

210ROOT_URLCONF = 'CoreRoot.urls' 

211 

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] 

227 

228OPEN_TELEMETRY_TRACER_NAME = "ahuora-api" 

229 

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") 

244 

245WSGI_APPLICATION = 'CoreRoot.wsgi.application' 

246 

247FIXTURE_DIRS = [ 

248 BASE_DIR / '/fixtures' 

249] 

250#Renewable Ninja Token 

251 

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) 

255 

256 

257# Database 

258# https://docs.djangoproject.com/en/4.2/ref/settings/#databases 

259 

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} 

272 

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} 

300 

301# Password validation 

302# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators 

303 

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] 

318 

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] 

324 

325# Internationalization 

326# https://docs.djangoproject.com/en/4.2/topics/i18n/ 

327 

328LANGUAGE_CODE = 'en-us' 

329 

330TIME_ZONE = 'UTC' 

331 

332USE_I18N = True 

333 

334USE_TZ = True 

335 

336# Static files (CSS, JavaScript, Images) 

337# https://docs.djangoproject.com/en/4.2/howto/static-files/ 

338 

339STATIC_URL = 'static/' 

340 

341SILKY_META = True 

342 

343# Default primary key field type 

344# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field 

345 

346DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 

347 

348AUTH_USER_MODEL = 'authentication_user.User' 

349 

350AUTHENTICATION_BACKENDS = [ 

351 "authentication.remote_user_backend.RemoteUserBackendWithEmail", 

352] 

353 

354PLATFORM_TEST_EMAIL = os.getenv("PLATFORM_TEST_EMAIL", "test@ahuoratech.app") 

355 

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")) 

375 

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" 

384 

385def __insert_middleware(middleware_name: str, before_middleware_name: str): 

386 index = MIDDLEWARE.index(before_middleware_name) 

387 MIDDLEWARE.insert(index, middleware_name) 

388 

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 

393 

394 if os.getenv("DAPR_HTTP_ENDPOINT") is None: 

395 settings.DAPR_HTTP_ENDPOINT = "http://localhost:3501" 

396 

397 if os.getenv("DAPR_GRPC_ENDPOINT") is None: 

398 settings.DAPR_GRPC_ENDPOINT = "localhost:50001" 

399 

400if DEBUG: 400 ↛ 417line 400 didn't jump to line 417 because the condition on line 400 was always true

401 set_dapr_endpoints() 

402 

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 ) 

408 

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') 

416 

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