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