loko/loko/common/context_processors.py

389 lines
No EOL
16 KiB
Python

from __future__ import annotations
import json
import os
from typing import Iterable, List
from django.contrib.auth.models import AnonymousUser
from django.core.cache import cache
from django.urls import resolve
from .models import UserConfig, AppView
SESSION_ALLOWED_IDS_KEY = "account_switch_allowed_user_ids"
SESSION_ORIGIN_ID_KEY = "account_switch_origin_user_id"
SESSION_MAP_BOUNDS_KEY = "user_map_bounds"
from django.conf import settings
def environment_info(request):
"""
Context processor that exposes environment information.
Uses ENVIRONMENT variable which can be 'development', 'test', or 'production'.
Makes 'environment_name' available in all templates.
"""
env = os.getenv('ENVIRONMENT', '').lower()
# Map environment names to display labels
env_labels = {
'development': 'DEV',
'dev': 'DEV',
'test': 'TEST',
'staging': 'STAGING',
}
# Only show label for non-production environments
environment_label = env_labels.get(env, '')
return {
'environment_name': environment_label,
'MAP_STYLE_LIGHT': getattr(settings, 'MAP_STYLE_LIGHT', None) or 'https://api.maptiler.com/maps/streets-v2-light/style.json',
'MAP_STYLE_STANDARD': getattr(settings, 'MAP_STYLE_STANDARD', None) or 'https://api.maptiler.com/maps/streets-v2/style.json',
}
def compute_initial_allowed_ids(config: UserConfig) -> List[int]:
linked_ids: Iterable[int] = config.linked_configs.values_list("user_id", flat=True)
allowed = list(linked_ids)
allowed.append(config.user_id)
return allowed
def current_app_view(request):
"""
Context processor that determines the current active AppView based on the URL namespace.
Makes 'current_view' available in all templates.
Only 'sign' and 'documents' have their own dedicated views.
All other apps (assets, interventions, controls, contracts, etc.) use 'asset_management' view.
Results are cached to avoid hitting the database on every page render.
"""
current_view = None
# Apps that have their own dedicated view
DEDICATED_VIEW_APPS = ['sign', 'documents', 'reporting', 'maps', 'authorizations', 'panoramax']
try:
# Get the namespace from the current URL
resolved = resolve(request.path)
namespace = resolved.namespace # e.g., 'sign', 'home', 'assets'
if namespace:
cache_key = f"appview_for_{namespace}"
cached_view = cache.get(cache_key)
if cached_view is not None:
current_view = None if cached_view is False else cached_view
else:
if namespace in DEDICATED_VIEW_APPS:
# These apps have their own dedicated view
current_view = AppView.objects.filter(url_name__startswith=f"{namespace}:").first()
else:
# All other apps (home, assets, interventions, controls, contracts, etc.)
# should show 'asset_management' view
current_view = AppView.objects.filter(code='asset_management').first()
cache.set(cache_key, current_view if current_view is not None else False, 3600)
except Exception:
pass
return {
'current_view': current_view,
}
def linked_accounts(request):
user = getattr(request, "user", None)
if not user or isinstance(user, AnonymousUser) or not user.is_authenticated:
request.session.pop(SESSION_ALLOWED_IDS_KEY, None)
request.session.pop(SESSION_ORIGIN_ID_KEY, None)
return {
"switchable_accounts": [],
"account_switch_origin_user_id": None,
}
try:
current_config: UserConfig = user.config
except UserConfig.DoesNotExist:
request.session.pop(SESSION_ALLOWED_IDS_KEY, None)
request.session.pop(SESSION_ORIGIN_ID_KEY, None)
return {
"switchable_accounts": [],
"account_switch_origin_user_id": None,
}
session = getattr(request, "session", None)
if session is None:
return {
"switchable_accounts": [],
"account_switch_origin_user_id": None,
}
allowed_ids = session.get(SESSION_ALLOWED_IDS_KEY)
origin_user_id = session.get(SESSION_ORIGIN_ID_KEY)
if allowed_ids is None or origin_user_id is None:
allowed_ids = compute_initial_allowed_ids(current_config)
origin_user_id = user.pk
else:
allowed_ids = list(dict.fromkeys(int(pk) for pk in allowed_ids))
if user.pk not in allowed_ids:
allowed_ids.append(user.pk)
session[SESSION_ALLOWED_IDS_KEY] = allowed_ids
session[SESSION_ORIGIN_ID_KEY] = origin_user_id
switchable_accounts = (
UserConfig.objects.select_related("user")
.filter(user_id__in=allowed_ids)
.exclude(user_id=user.pk)
.order_by("user__first_name", "user__last_name")
)
return {
"switchable_accounts": switchable_accounts,
"account_switch_origin_user_id": origin_user_id,
}
def invalidate_user_map_bounds(user_id: int) -> None:
"""
Invalidates the cached map center/zoom/extent for a given user.
Called when UserConfig or UserContractAccess changes.
"""
if user_id:
cache.delete(f"user_map_bounds_{user_id}")
def get_active_map_backgrounds(request=None) -> list:
"""
Returns list of active basemap layers from MapBackground model, cached.
Fallback to defaults if table does not exist or is empty.
Prioritizes basemaps matching user contract municipality region (WAL, BRU, VLG).
"""
cached = cache.get("active_map_backgrounds")
if cached is not None:
return cached
backgrounds = []
try:
from .models import MapBackground
from django.utils.translation import get_language
lang = get_language() or 'fr'
qs = list(MapBackground.objects.filter(is_active=True).order_by('order', 'id'))
# Detect regional priority if user has an assigned municipality
preferred_region = 'ALL'
if request and hasattr(request, 'user') and request.user.is_authenticated:
try:
user_config = getattr(request.user, 'config', None)
if user_config and user_config.default_contract:
muni = user_config.default_contract.municipalities.exclude(geom=None).first()
if muni and muni.niscode:
code = str(muni.niscode).strip()
if code.startswith('21'):
preferred_region = 'BRU'
elif code[:2] in ('25', '51', '52', '53', '55', '56', '57', '61', '62', '63', '81', '82', '83', '84', '85', '91', '92', '93') or code[0] in ('5', '6', '8', '9'):
preferred_region = 'WAL'
elif code[0] in ('1', '3', '4', '7') or code[:2] in ('23', '24'):
preferred_region = 'VLG'
except Exception:
pass
if preferred_region != 'ALL':
# Stable sort: matching region first, then remaining
qs.sort(key=lambda x: (0 if x.region == preferred_region else (1 if x.region == 'ALL' else 2), x.order))
for bg in qs:
backgrounds.append({
'id': bg.identifier,
'name': bg.name_nl if lang == 'nl' and bg.name_nl else bg.name_fr,
'name_fr': bg.name_fr,
'name_nl': bg.name_nl,
'service_type': bg.service_type,
'url': bg.url,
'layers': bg.layers,
'format': bg.format,
'attribution': bg.attribution,
'region': bg.region,
'category': bg.category,
'tileSize': bg.tile_size,
'minZoom': bg.min_zoom,
'maxZoom': bg.max_zoom,
'is_default': bg.is_default,
})
except Exception:
pass
if not backgrounds:
backgrounds = [
{
'id': 'ortho_wallonie',
'name': 'Ortho Wallonie (SPW)',
'name_fr': 'Ortho Wallonie (SPW)',
'name_nl': 'Ortho Wallonië (SPW)',
'service_type': 'wms',
'url': 'https://geoservices.wallonie.be/arcgis/services/IMAGERIE/ORTHO_LAST/MapServer/WMSServer?service=WMS&version=1.3.0&request=GetMap&layers=0&styles=&format=image/jpeg&crs=EPSG:3857&bbox={bbox-epsg-3857}&width=256&height=256',
'layers': '0',
'format': 'image/jpeg',
'attribution': '© SPW - Orthophoto Wallonie',
'region': 'WAL',
'category': 'ortho',
'tileSize': 256,
},
{
'id': 'urbis_ortho',
'name': 'Urbis Ortho (Bruxelles)',
'name_fr': 'Urbis Ortho (Bruxelles)',
'name_nl': 'Urbis Ortho (Brussel)',
'service_type': 'wms',
'url': getattr(settings, 'MAP_ORTHO_WMS_URL', None) or '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}',
'layers': 'Ortho',
'format': 'image/png',
'attribution': getattr(settings, 'MAP_ORTHO_WMS_ATTRIBUTION', None) or '© Brussels UrbIS',
'region': 'BRU',
'category': 'ortho',
'tileSize': 256,
},
{
'id': 'ortho_vlaanderen',
'name': 'Ortho Vlaanderen',
'name_fr': 'Ortho Vlaanderen',
'name_nl': 'Ortho Vlaanderen',
'service_type': 'wmts',
'url': 'https://geo.api.vlaanderen.be/OMW/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=omwrgb23vl&STYLE=&FORMAT=image/png&TILEMATRIXSET=GoogleMapsVL&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}',
'layers': 'omwrgb23vl',
'format': 'image/png',
'attribution': '© Digitaal Vlaanderen',
'region': 'VLG',
'category': 'ortho',
'tileSize': 256,
},
{
'id': 'plan_wallonie_spw',
'name': 'Plan de base Wallonie (SPW)',
'name_fr': 'Plan de base Wallonie (SPW)',
'name_nl': 'Basiskaart Wallonië (SPW)',
'service_type': 'arcgis_rest',
'url': 'https://geoservices.wallonie.be/arcgis/rest/services/DONNEES_BASE/FDC_SPW/MapServer/export?bbox={bbox-epsg-3857}&bboxSR=3857&imageSR=3857&size=256,256&format=png&f=image',
'layers': '',
'format': 'image/png',
'attribution': '© SPW - Fond de plan Wallonie',
'region': 'WAL',
'category': 'plan',
'tileSize': 256,
},
]
cache.set("active_map_backgrounds", backgrounds, 3600)
return backgrounds
def organization_info(request):
"""
Context processor that exposes organization details from settings.
Dynamically adapts map_default_center, map_default_zoom and map_default_extent
when the authenticated user has an assigned contract associated with a municipality.
Caches calculated bounds in Django cache to avoid heavy GIS and SQL processing on every page.
"""
center = [getattr(settings, 'MAP_DEFAULT_CENTER_LON', 4.3572), getattr(settings, 'MAP_DEFAULT_CENTER_LAT', 50.8477)]
zoom = getattr(settings, 'MAP_DEFAULT_ZOOM', 11)
extent = getattr(settings, 'MAP_DEFAULT_EXTENT', None)
if hasattr(request, 'user') and request.user.is_authenticated:
cache_key = f"user_map_bounds_{request.user.pk}"
cached_bounds = cache.get(cache_key)
if cached_bounds is not None:
center = cached_bounds.get('center', center)
zoom = cached_bounds.get('zoom', zoom)
extent = cached_bounds.get('extent', extent)
else:
try:
user_config = getattr(request.user, 'config', None)
if user_config:
# Récupérer les contrats accessibles ayant des communes associées
contracts_to_check = []
if user_config.default_contract and user_config.default_contract.municipalities.exists():
contracts_to_check.append(user_config.default_contract)
else:
contracts_to_check = list(user_config.contracts.prefetch_related('municipalities').filter(municipalities__isnull=False).distinct())
geoms_4326 = []
for c in contracts_to_check:
for m in c.municipalities.exclude(geom=None):
g = m.geom.transform(4326, clone=True) if m.geom.srid != 4326 else m.geom
geoms_4326.append(g)
if geoms_4326:
min_lon = min(g.extent[0] for g in geoms_4326)
min_lat = min(g.extent[1] for g in geoms_4326)
max_lon = max(g.extent[2] for g in geoms_4326)
max_lat = max(g.extent[3] for g in geoms_4326)
# Léger buffer (5% d'expansion de chaque côté pour aérer le cadre)
delta_lon = (max_lon - min_lon) * 0.05
delta_lat = (max_lat - min_lat) * 0.05
center = [round((min_lon + max_lon) / 2.0, 5), round((min_lat + max_lat) / 2.0, 5)]
extent = [
[round(min_lon - delta_lon, 5), round(min_lat - delta_lat, 5)],
[round(max_lon + delta_lon, 5), round(max_lat + delta_lat, 5)]
]
span = max(max_lon - min_lon, max_lat - min_lat)
if span < 0.08:
zoom = 14
elif span < 0.2:
zoom = 13
elif span < 0.5:
zoom = 12
else:
zoom = 11
cache.set(cache_key, {
'center': center,
'zoom': zoom,
'extent': extent,
}, 86400)
except Exception:
pass
return {
'product_name': getattr(settings, 'PRODUCT_NAME', 'Loko'),
'organization_name': getattr(settings, 'ORGANIZATION_NAME', 'Loko'),
'organization_logo_header': getattr(settings, 'ORGANIZATION_LOGO_HEADER', 'common/android-192.png'),
'organization_primary_color': getattr(settings, 'ORGANIZATION_PRIMARY_COLOR_HEX', '#2f3e8b'),
'map_default_center': center,
'map_default_zoom': zoom,
'map_default_extent': extent,
'map_overlay_wms_url': getattr(settings, 'MAP_OVERLAY_WMS_URL', ''),
'map_ortho_wms_url': getattr(settings, 'MAP_ORTHO_WMS_URL', ''),
'map_ortho_wms_name': getattr(settings, 'MAP_ORTHO_WMS_NAME', ''),
'map_ortho_wms_attribution': getattr(settings, 'MAP_ORTHO_WMS_ATTRIBUTION', ''),
'map_backgrounds': get_active_map_backgrounds(request),
'map_backgrounds_json': json.dumps(get_active_map_backgrounds(request)),
}
def regional_terminology(request):
"""
Context processor providing regional terminology & standard definitions
(e.g., PEB in Belgium vs DPE/GES in France) based on the user's UserConfig or system default.
Makes 'regional_terms' and 'regional_standard' available in all templates.
"""
from .utils import get_regional_terms
standard_code = getattr(settings, 'REGIONAL_STANDARD', 'BE')
if request.user.is_authenticated:
user_config = getattr(request.user, 'config', None)
if user_config and getattr(user_config, 'regional_standard', None):
standard_code = user_config.regional_standard
return {
'regional_standard': standard_code,
'regional_terms': get_regional_terms(standard_code),
}