loko/streetup/streetup/settings.py

409 lines
No EOL
13 KiB
Python

"""
Django settings for streetup project.
Generated by 'django-admin startproject' using Django 5.1.4.
For more information on this file, see
https://docs.djangoproject.com/en/5.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.1/ref/settings/
"""
from pathlib import Path
from os import getenv
from dotenv import load_dotenv
import os
import platform
# Bypass WMI queries on Windows which can hang indefinitely due to OS/WMI service issues
if os.name == 'nt':
platform.win32_ver = lambda: ('10', '10.0.19045', '', 'Multiprocessor Free')
from datetime import timedelta
from django.utils.translation import gettext_lazy as _
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
load_dotenv(BASE_DIR / ".env", override=False)
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = getenv("SECRET_KEY")
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = getenv("DEBUG") == "True"
ROOT_URL = getenv("ROOT_URL", "http://localhost:8000")
REGIONAL_STANDARD = getenv("REGIONAL_STANDARD", "BE")
ALLOWED_HOSTS = getenv("ALLOWED_HOSTS").split(',')
CSRF_TRUSTED_ORIGINS = getenv("CSRF_TRUSTED_ORIGINS").split(',')
SESSION_COOKIE_NAME = 'streetup_core_sessionid'
# Application definition
INSTALLED_APPS = [
'common.apps.CommonConfig',
'home.apps.HomeConfig',
'assets.apps.AssetsConfig',
'interventions.apps.InterventionsConfig',
'stock.apps.StockConfig',
'contracts.apps.ContractsConfig',
'controls.apps.ControlsConfig',
'projects.apps.ProjectsConfig',
'observations.apps.ObservationsConfig',
'documents.apps.DocumentsConfig',
'mobile.apps.MobileConfig',
'api.apps.ApiConfig',
'sign.apps.SignConfig',
'maps.apps.MapsConfig',
'analytics.apps.AnalyticsConfig',
'notifications.apps.NotificationsConfig',
'road_detection.apps.RoadDetectionConfig',
'authorizations.apps.AuthorizationsConfig',
'reporting.apps.ReportingConfig',
'panoramax.apps.PanoramaxConfig',
'bim.apps.BimConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
"django.contrib.gis",
"django.contrib.postgres",
"crispy_forms",
"crispy_bootstrap5",
"rest_framework",
]
MIDDLEWARE = [
'django.middleware.gzip.GZipMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.LoginRequiredMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'common.middleware.AutoLogoutMiddleware', # Déconnexion automatique après X heures
'common.middleware.RequestLoggingMiddleware', # Capture des erreurs dans les logs
'analytics.middleware.AnalyticsMiddleware', # Monitoring de l'utilisation de l'application
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'streetup.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
"DIRS": [BASE_DIR / "templates"],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'common.context_processors.linked_accounts',
'common.context_processors.current_app_view',
'common.context_processors.environment_info',
'common.context_processors.organization_info',
'common.context_processors.regional_terminology',
'notifications.context_processors.unread_notifications_count',
],
},
},
]
WSGI_APPLICATION = 'streetup.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.contrib.gis.db.backends.postgis",
'OPTIONS': {
'options': f'-c search_path={getenv("DB_SCHEMA","public")},public'
},
'NAME': getenv("DB_NAME"),
'USER': getenv("DB_USER"),
'PASSWORD': getenv("DB_PASSWORD"),
'HOST': getenv("DB_HOST"),
'PORT': int(getenv("DB_PORT", 5432)),
},
}
# Password validation
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.1/topics/i18n/
LANGUAGES = [
('nl', 'Nederlands'),
('en', 'English'),
('fr', 'Français'),
]
LANGUAGE_CODE = 'en-us'
USE_TZ = True
# TIME_ZONE = 'UTC'
TIME_ZONE = 'Europe/Brussels'
USE_I18N = True
LOCALE_PATHS = [
BASE_DIR / "locale",
]
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.1/howto/static-files/
STATIC_URL = 'static/'
STATIC_ROOT = BASE_DIR / "static"
STATICFILES_DIRS = []
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"
PRIVATE_MEDIA_ROOT = BASE_DIR / "private_media"
PRIVATE_MEDIA_URL = "/private-media/"
# Durée de validité des tokens signés temporaires pour l'accès aux fichiers privés (ex. documents)
PRIVATE_FILES_TOKEN_MAX_AGE = 172800 # 48 heures
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.ManifestStaticFilesStorage",
},
}
# Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
GDAL_LIBRARY_PATH = getenv("GDAL_LIBRARY_PATH")
GEOS_LIBRARY_PATH = getenv("GEOS_LIBRARY_PATH")
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
CRISPY_TEMPLATE_PACK = "bootstrap5"
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = getenv("EMAIL_HOST")
EMAIL_USE_TLS = False
EMAIL_USE_SSL = True
EMAIL_PORT = getenv("EMAIL_PORT")
EMAIL_HOST_USER = getenv("EMAIL_HOST_USER")
EMAIL_HOST_PASSWORD = getenv("EMAIL_HOST_PASSWORD")
DEFAULT_FROM_EMAIL = 'no-reply@streetup.be'
# Override email recipients for testing (set to None in production)
# If set, all emails will be sent to this address instead of real recipients
EMAIL_OVERRIDE = getenv("EMAIL_OVERRIDE", "kdeterme@sprb.brussels")
# Organization Configuration
PRODUCT_NAME = getenv("PRODUCT_NAME", "StreetUp")
ORGANIZATION_NAME = getenv("ORGANIZATION_NAME", _("Brussels Mobility"))
ORGANIZATION_NAME_FR = getenv("ORGANIZATION_NAME_FR", "Bruxelles Mobilité")
ORGANIZATION_NAME_NL = getenv("ORGANIZATION_NAME_NL", "Brussel Mobiliteit")
ORGANIZATION_LOGO_HEADER = getenv("ORGANIZATION_LOGO_HEADER", "common/android-192.png")
ORGANIZATION_LOGO_PNG = getenv("ORGANIZATION_LOGO_PNG", "common/SPRB-GOB-FR-NL-LOGO-RGB.png")
ORGANIZATION_LOGO_SVG = getenv("ORGANIZATION_LOGO_SVG", "common/sprb.svg")
def _clean_hex(hex_str):
try:
hex_str = hex_str.strip().strip('"').strip("'")
h = hex_str.lstrip('#')
if len(h) == 8:
h = h[:6]
return f"#{h}"
except Exception:
return "#2f3e8b"
ORGANIZATION_PRIMARY_COLOR_HEX = _clean_hex(getenv("ORGANIZATION_PRIMARY_COLOR_HEX", "#2f3e8b"))
def _hex_to_rgb(hex_str):
try:
h = hex_str.lstrip('#')
return tuple(int(h[i:i+2], 16) for i in (0, 2, 4))
except Exception:
return (47, 62, 139)
ORGANIZATION_PRIMARY_COLOR_RGB = _hex_to_rgb(ORGANIZATION_PRIMARY_COLOR_HEX)
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"streetup_file": {
"level": "WARNING",
"class": "logging.FileHandler",
"filename": getenv("LOG_FILE_PATH"),
},
"streetup_file_info": {
"level": "INFO",
"class": "logging.FileHandler",
"filename": getenv("LOG_FILE_PATH"),
},
},
"root": {
"handlers": ["streetup_file"],
"level": "WARNING",
},
"loggers": {
"django": {
"handlers": ["streetup_file"],
"level": "WARNING",
"propagate": False,
},
"home.perf": {
"handlers": ["streetup_file_info"],
"level": "INFO",
"propagate": False,
},
},
}
REST_FRAMEWORK_ENABLED = True
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
#"rest_framework.authentication.SessionAuthentication",
"api.auth.HybridTokenAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
}
# SIMPLE_JWT = {
# "ALGORITHM": "RS256",
# "SIGNING_KEY": open("/etc/keys/jwt-private.pem").read(),
# "VERIFYING_KEY": open("/etc/keys/jwt-public.pem").read(),
# "ACCESS_TOKEN_LIFETIME": timedelta(minutes=20),
# "AUTH_HEADER_TYPES": ("Bearer",),
# "AUDIENCE": "pygeoapi",
# }
# Schools app settings
SCHOOLS_GEOCODING_CHUNK_SIZE = 3 # Concurrent geocoding requests per block
SCHOOLS_GEOCODING_API_LIMIT = 1 # Number of results to request from Photon API per address
SCHOOLS_GEOCODING_INTERNAL_THRESHOLD = 0.5 # Minimum fuzzy match score (0-1) to accept internal DB result
SCHOOLS_GEOCODING_USE_INTERNAL = True # Whether to try internal DB before Photon API
# Road Detection (YOLO11)
# Pour utiliser un modèle fine-tuné sur les nids de poule, définir le chemin ici.
# Exemple: ROAD_DETECTION_MODEL_PATH = BASE_DIR / "media" / "road_detection" / "models" / "pothole_yolo11n.pt"
ROAD_DETECTION_MODEL_PATH = getenv("ROAD_DETECTION_MODEL_PATH", "yolo11n.pt")
# Cartography configuration
MAP_STYLE_LIGHT = getenv("MAP_STYLE_LIGHT")
MAP_STYLE_STANDARD = getenv("MAP_STYLE_STANDARD")
MAP_RASTER_TILE_URL = getenv("MAP_RASTER_TILE_URL")
MAP_ATTRIBUTION = getenv("MAP_ATTRIBUTION")
MAP_DEFAULT_CENTER_LON = float(getenv("MAP_DEFAULT_CENTER_LON", 4.3572))
MAP_DEFAULT_CENTER_LAT = float(getenv("MAP_DEFAULT_CENTER_LAT", 50.8477))
MAP_DEFAULT_ZOOM = int(getenv("MAP_DEFAULT_ZOOM", 11))
def _parse_extent(extent_str):
if not extent_str:
return None
try:
parts = [float(x.strip()) for x in extent_str.split(',')]
if len(parts) == 4:
return [[parts[0], parts[1]], [parts[2], parts[3]]]
return None
except Exception:
return None
MAP_DEFAULT_EXTENT = _parse_extent(getenv("MAP_DEFAULT_EXTENT"))
# Support & Follow-up Contacts
EMAIL_TECH_SUPPORT = getenv("EMAIL_TECH_SUPPORT", "bm.perm.tech@sprb.brussels")
EMAIL_TECH_SUPPORT_PHONE = getenv("EMAIL_TECH_SUPPORT_PHONE", "02/204.22.86")
EMAIL_INTERNAL_FOLLOWUP = getenv("EMAIL_INTERNAL_FOLLOWUP", "kdeterme@sprb.brussels")
EMAIL_AUTHORIZATIONS_BCC = getenv("EMAIL_AUTHORIZATIONS_BCC", "autorisation.voiries@sprb.brussels")
# Orthophoto WMS Layer Configuration
MAP_ORTHO_WMS_URL = getenv("MAP_ORTHO_WMS_URL", "https://geoservices-urbis.irisnet.be/geoserver/urbisgrid/wms?service=WMS&version=1.3.0&request=GetMap&layers=Ortho&styles=&format=image/png&transparent=true&width=256&height=256&crs=EPSG:3857&bbox={bbox-epsg-3857}")
MAP_ORTHO_WMS_NAME = getenv("MAP_ORTHO_WMS_NAME", "Urbis (Bruxelles)")
MAP_ORTHO_WMS_ATTRIBUTION = getenv("MAP_ORTHO_WMS_ATTRIBUTION", "© Brussels UrbIS")
# Panoramax API configuration
PANORAMAX_API_URL = getenv("PANORAMAX_API_URL", "https://panoramax.streetup.be/api")
PANORAMAX_API_KEY = getenv("PANORAMAX_API_KEY", "")
PANORAMAX_IMPORT_JPEG_QUALITY = int(getenv("PANORAMAX_IMPORT_JPEG_QUALITY", "98"))
# AWS S3 Settings for Panoramax private bucket redirects (Option B)
PANORAMAX_AWS_ACCESS_KEY_ID = getenv("PANORAMAX_AWS_ACCESS_KEY_ID")
PANORAMAX_AWS_SECRET_ACCESS_KEY = getenv("PANORAMAX_AWS_SECRET_ACCESS_KEY")
PANORAMAX_AWS_S3_ENDPOINT_URL = getenv("PANORAMAX_AWS_S3_ENDPOINT_URL", "https://s3.rbx.io.cloud.ovh.net")
PANORAMAX_AWS_S3_BUCKET = getenv("PANORAMAX_AWS_S3_BUCKET")
PANORAMAX_AWS_S3_PREFIX = getenv("PANORAMAX_AWS_S3_PREFIX", "photos")
# Optimization for tests: use a fast password hasher and preserve test database by default
import sys
import os
if 'test' in sys.argv:
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.MD5PasswordHasher',
]
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
},
}
if '--recreate-db' in sys.argv:
sys.argv.remove('--recreate-db')
os.environ['RECREATE_DB'] = '1'
TEST_RUNNER = 'streetup.runner.KeepDBDiscoverRunner'