389 lines
No EOL
16 KiB
Python
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),
|
|
} |