loko/streetup/assets/views/core.py

910 lines
34 KiB
Python

import json
import re
from datetime import datetime
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.http import Http404
from django.shortcuts import redirect, render, get_object_or_404
from django.template.loader import select_template
from django.urls import reverse
from django.utils.translation import gettext as _
from common.models import UserConfig, Thematic
from common.utils import check_thematic_access
from assets.models import AssetCategory, ASSET_STATUS_CHOICES
from assets.models.core import ASSET_VALIDATION_STATUS_CHOICES
from assets.permissions import (
get_allowed_update_fields_for_asset,
get_allowed_actions_for_asset,
)
def get_connection_lines_geojson(asset):
"""
Returns a GeoJSON FeatureCollection containing LineStrings representing
only electrical connections. For a TrafficLightRadar, this means connections to
power_publiclighting_pole and/or power_trafficlights_pole when they are set and have geoms.
"""
import json
from django.contrib.gis.geos import LineString
from assets.models.trafficlights import TrafficLightRadar
if not isinstance(asset, TrafficLightRadar):
return None
if not asset.geom:
return None
features = []
# We check both power sources
power_sources = [
getattr(asset, 'power_publiclighting_pole', None),
getattr(asset, 'power_trafficlights_pole', None)
]
for ps in power_sources:
if ps and getattr(ps, 'geom', None):
p1 = asset.geom.clone()
p1.transform(4326)
p2 = ps.geom.clone()
p2.transform(4326)
# Avoid zero-length lines if by any chance they are at the exact same point
if p1.distance(p2) >= 0.00001:
line = LineString(p1, p2)
features.append({
"type": "Feature",
"geometry": json.loads(line.json),
"properties": {
"parent_code": ps.code,
"relation_type": "electrical_connection"
}
})
if not features:
return None
return {
"type": "FeatureCollection",
"features": features
}
def get_asset_edit_context(request, asset, content_type):
"""
Get context variables for asset editing in templates.
Includes the complete replacement chain based on the replaced_by field.
"""
allowed_update_fields = get_allowed_update_fields_for_asset(request.user, asset)
allowed_actions = get_allowed_actions_for_asset(request.user, asset)
# Format coordinates: convert to float then to string with proper decimal separator (dot)
lon_display = None
lat_display = None
if hasattr(asset, 'lon') and asset.lon is not None:
try:
# Convert to float to normalize, then back to string
lon_value = float(str(asset.lon).replace(',', '.'))
lon_display = str(lon_value)
except (ValueError, TypeError):
lon_display = None
if hasattr(asset, 'lat') and asset.lat is not None:
try:
# Convert to float to normalize, then back to string
lat_value = float(str(asset.lat).replace(',', '.'))
lat_display = str(lat_value)
except (ValueError, TypeError):
lat_display = None
# Get status choices
status_choices = list(ASSET_STATUS_CHOICES.items())
validation_status_choices = list(ASSET_VALIDATION_STATUS_CHOICES.items())
# Get categories for the same thematic
# Priority: asset.category.thematic > asset.model.category.thematic > URL path
categories = []
_cat_thematic = None
if hasattr(asset, 'category') and asset.category and asset.category.thematic:
_cat_thematic = asset.category.thematic
elif hasattr(asset, 'model') and asset.model and getattr(asset.model, 'category', None) and asset.model.category.thematic:
_cat_thematic = asset.model.category.thematic
else:
# Fallback: extract thematic code from URL path (/assets/<thematic_code>/...)
url_match = re.match(r'^/assets/([^/]+)/', request.path)
if url_match:
try:
_cat_thematic = Thematic.objects.get(code=url_match.group(1))
except Thematic.DoesNotExist:
pass
if _cat_thematic:
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
asset_content_type = ContentType.objects.get_for_model(asset)
categories = list(AssetCategory.objects.filter(
thematic=_cat_thematic
).filter(
Q(allowed_models=asset_content_type) | Q(id=getattr(asset, 'category_id', None))
).distinct().order_by('name_fr'))
# Get asset models for replacement
asset_models = []
if hasattr(asset, 'model') and asset.model:
model_field = asset._meta.get_field('model')
related_model = model_field.related_model
asset_models = list(related_model.objects.all().order_by('code'))
# Get the original asset (first in the chain) and the complete replacement chain
asset_replacements = []
asset_base_code = None
if hasattr(asset, 'get_original_asset'):
try:
original_asset = asset.get_original_asset()
asset_replacements = original_asset.get_replacement_chain()
asset_base_code = getattr(original_asset, 'code', None)
except Exception:
asset_replacements = [asset]
asset_base_code = getattr(asset, 'code', None)
else:
asset_replacements = [asset]
asset_base_code = getattr(asset, 'code', None)
# Get geometry JSON for JavaScript (transform to WGS84 for MapLibre)
asset_geometry_json = None
if hasattr(asset, 'geom') and asset.geom:
try:
geom_wgs84 = asset.geom.clone()
geom_wgs84.transform(4326)
asset_geometry_json = geom_wgs84.json
except Exception:
pass
# Get thematic code for URL construction
# Priority: asset.category > asset.model.category > None
thematic_code = None
thematic = None
_thematic_source = None
if hasattr(asset, 'category') and asset.category and asset.category.thematic:
_thematic_source = asset.category.thematic
elif hasattr(asset, 'model') and asset.model and getattr(asset.model, 'category', None) and asset.model.category.thematic:
_thematic_source = asset.model.category.thematic
if _thematic_source:
thematic_code = _thematic_source.code
thematic = _thematic_source
# Verbose name of the concrete model (e.g. "Traffic light pole")
asset_verbose_name = str(asset._meta.verbose_name).capitalize()
# Is the current user an admin? (used to display the permissions panel)
is_admin = False
try:
from common.models import UserConfig as _UC
is_admin = _UC.objects.get(user=request.user).roles.filter(name='admin').exists()
except Exception:
pass
return {
'allowed_update_fields': allowed_update_fields,
'allowed_actions': allowed_actions,
'status_choices': status_choices,
'validation_status_choices': validation_status_choices,
'categories': categories,
'asset_models': asset_models,
'asset_model': content_type.model,
'asset_geometry_json': asset_geometry_json,
'thematic_code': thematic_code,
'thematic': thematic,
'asset_replacements': asset_replacements,
'asset_base_code': asset_base_code,
'lon_display': lon_display,
'lat_display': lat_display,
'asset_verbose_name': asset_verbose_name,
'is_admin': is_admin,
'connection_lines_geojson': get_connection_lines_geojson(asset),
}
def index(request):
user_config = UserConfig.objects.filter(user=request.user).first()
default_thematic = user_config.default_thematic.code if user_config and user_config.default_thematic else 'trafficlights'
# Redirection based on default_thematic
if default_thematic == 'trafficlights':
return redirect('assets:trafficlights_index')
elif default_thematic == 'publiclighting':
return redirect('assets:publiclighting_index')
elif default_thematic == 'artworks':
return redirect('assets:artworks_index')
elif default_thematic == 'structures':
return redirect('assets:structures_index')
elif default_thematic == 'nature':
return redirect('assets:nature_index')
elif default_thematic == 'water':
return redirect('assets:water_index')
elif default_thematic == 'roads':
return redirect('assets:roads_index')
elif default_thematic == 'sign':
return redirect('assets:sign_index')
elif default_thematic == 'controlcenters':
return redirect('assets:controlcenters_index')
# Add more conditions for other thematics
else:
return redirect('assets:trafficlights_index') # Fallback URL if no match is found
def locations_geojson(request):
pass
def mobile_asset_detail(request, thematic_code, asset_model, asset_id):
"""Mobile-friendly asset detail using the generic template with the mobile base."""
try:
content_type = ContentType.objects.get(app_label='assets', model=asset_model)
model_class = content_type.model_class()
except ContentType.DoesNotExist:
raise Http404(_("Modèle %(asset_model)s inconnu.") % (asset_model))
asset = get_object_or_404(model_class, id=asset_id)
# Permission gate based on the provided thematic code
permission_check = check_thematic_access(thematic_code)
result = permission_check(lambda req, *a, **kw: None)(request)
if result is not None:
return result
template = select_template([
f"assets/{asset_model}_detail_mobile.html",
"assets/detail_generic.html",
])
# Build back URL from referer or intervention context
back_url = request.GET.get('back')
if not back_url:
referer = request.META.get('HTTP_REFERER', '')
if referer:
# If coming from an intervention, build the back URL
if '/mobile/interventions/' in referer:
import re
match = re.search(r'/interventions/(\d+)', referer)
if match:
back_url = reverse('mobile:intervention_detail_mobile', args=[match.group(1)])
context = {
"asset": asset,
"content_type": content_type,
"base_template": "mobile/mobile_base.html",
"mobile_view": True,
"back_url": back_url,
}
context.update(get_asset_edit_context(request, asset, content_type))
return render(request, template.template.name, context)
def filter_queryset_by_geom(qs, filter_geom, include_no_geom, geom_field='geom', location_field='location'):
"""
Filtre un queryset d'assets selon une géométrie (intersects) et prend en compte l'option include_no_geom.
"""
if not filter_geom:
return qs
from django.db.models import Q
# Standard intersects query
q_intersects = Q(**{f"{geom_field}__intersects": filter_geom})
if location_field:
q_intersects |= Q(**{
f"{geom_field}__isnull": True,
f"{location_field}__geom__intersects": filter_geom
})
if include_no_geom:
q_no_geom = Q(**{f"{geom_field}__isnull": True})
if location_field:
q_no_geom &= (
Q(**{f"{location_field}__isnull": True}) |
Q(**{f"{location_field}__geom__isnull": True})
)
return qs.filter(q_intersects | q_no_geom)
return qs.filter(q_intersects)
# ---------------------
# Recherche plein texte
# ---------------------
def apply_search(qs, data, fields=None):
"""
Filtre un queryset par recherche plein texte (AND par défaut, ou OR).
Paramètres POST attendus : `q` (texte) et `search_mode` ('and'/'or').
Par défaut, cherche dans `code`, `name_fr` et `name_nl`.
"""
q = (data.get('q') or '').strip()
mode = (data.get('search_mode') or 'and').lower()
if not q:
return qs
if fields is None:
fields = ['code', 'name_fr', 'name_nl']
words = q.split()
def _word_q(w):
combined = Q()
for field in fields:
combined |= Q(**{f'{field}__icontains': w})
return combined
combined = Q()
if mode == 'and':
for w in words:
combined &= _word_q(w)
else:
for w in words:
combined |= _word_q(w)
return qs.filter(combined)
# -----------------------------------------------
# Libellés lisibles par type d'élément
# -----------------------------------------------
ELEMENT_TYPE_LABELS = {
# --- Localisations ---
'roadstreet': 'Voirie',
'signstreet': 'Voirie signalisation',
'publiclightingstreet': 'Rue EP',
'trafficlightintersection': 'Carrefour',
'naturelocation': 'Section verte',
# --- Voirie ---
'street': _('Rue / Voie'),
'roadway': _('Chaussée'),
'sidewalk': _('Trottoir'),
'roadfurniture': _('Mobilier voirie'),
'roaddrain': _('Drain'),
'roadconcreteblockset': _('Blocs béton'),
# --- Éclairage public ---
'publiclightingpole': _('Candélabre'),
'publiclightinglightfixture': _('Luminaire'),
'publiclightingelectricalcabinet': _('Armoire électrique EP'),
# --- Signalisation ---
'signpole': _('Poteau signalisation'),
'signpanel': _('Panneau'),
'signparkingspot': _('Place de parking'),
# --- Tricolores ---
'trafficlightpole': _('Poteau tricolore'),
'trafficlightcable': _('Câble TL'),
'trafficlightlantern': _('Lanterne TL'),
'trafficlightcontroller': _('Contrôleur TL'),
'trafficlightcontrollerhardware': _('Hardware contrôleur'),
'trafficlightdetector': _('Détecteur TL'),
'trafficlightnetworkhardware': _('Hardware réseau TL'),
'trafficlightvoltagehardware': _('Hardware tension TL'),
'trafficlightelectricalcabinet': _('Armoire électrique TL'),
'trafficlightelectricalcabinetcontent': _('Contenu armoire TL'),
'trafficlightaccessory': _('Accessoire TL'),
# --- Nature ---
'naturegreensurface': _('Surface verte'),
'naturetree': _('Arbre'),
'natureshrub': _('Arbuste'),
'naturebench': _('Banc'),
'naturerwiasset': _('Ouvrage RWI'),
# --- Ouvrages d'art ---
'structuregeoasset': _("Composant d'ouvrage"),
# --- Œuvres d'art ---
'artworkmonument': _('Monument'),
'artworkfountain': _('Fontaine'),
# --- Centres de contrôle ---
'controlcentergeoasset': _('Équipement CdC'),
# --- ITS ---
'itsgeoasset': _('Équipement ITS'),
# --- Propreté ---
'cleanlitterbin': _('Corbeille'),
'cleanglasscontainer': _('Conteneur verre'),
# --- Bâtiments publics ---
'publicbuilding': _('Bâtiment public'),
'buildingequipment': _('Équipement bâtiment'),
}
def get_element_type_label(model_name):
"""Returns the translated display label for an asset/location model name (lowercase)."""
if not model_name:
return ''
label = ELEMENT_TYPE_LABELS.get((model_name or '').lower(), model_name or '')
return str(_(label)) if label else ''
def build_thematic_config(thematics):
"""
Returns a dict mapping thematic code → {name, icon, locations_geojson_url, assets_geojson_url}.
Used to populate the unified assets page JS config.
"""
_THEMATIC_URLS = {
'roads': {
'locations_geojson_url': reverse('assets:roads_streets_geojson'),
'assets_geojson_url': reverse('assets:roads_assets_geojson'),
},
'publiclighting': {
'locations_geojson_url': reverse('assets:publiclighting_streets_geojson'),
'assets_geojson_url': reverse('assets:publiclighting_assets_geojson'),
},
'sign': {
'locations_geojson_url': reverse('assets:sign_streets_geojson'),
'assets_geojson_url': reverse('assets:sign_assets_geojson'),
},
'trafficlights': {
'locations_geojson_url': reverse('assets:trafficlights_intersections_geojson'),
'assets_geojson_url': reverse('assets:trafficlights_assets_geojson'),
},
'nature': {
'locations_geojson_url': reverse('assets:nature_locations_geojson'),
'assets_geojson_url': reverse('assets:nature_assets_geojson'),
},
'water': {
'locations_geojson_url': reverse('assets:water_locations_geojson'),
'assets_geojson_url': reverse('assets:water_assets_geojson'),
},
'structures': {
'locations_geojson_url': reverse('assets:structures_locations_geojson'),
'assets_geojson_url': reverse('assets:structures_assets_geojson'),
},
'artworks': {
'locations_geojson_url': reverse('assets:artworks_streets_geojson'),
'assets_geojson_url': reverse('assets:artworks_assets_geojson'),
},
'controlcenters': {
'locations_geojson_url': reverse('assets:controlcenters_locations_geojson'),
'assets_geojson_url': reverse('assets:controlcenters_assets_geojson'),
},
'its': {
'locations_geojson_url': reverse('assets:its_locations_geojson'),
'assets_geojson_url': reverse('assets:its_assets_geojson'),
},
'clean': {
'locations_geojson_url': reverse('assets:clean_locations_geojson'),
'assets_geojson_url': reverse('assets:clean_assets_geojson'),
},
'publicbuildings': {
'locations_geojson_url': reverse('assets:publicbuildings_locations_geojson'),
'assets_geojson_url': reverse('assets:publicbuildings_assets_geojson'),
},
'buildings': {
'locations_geojson_url': reverse('assets:publicbuildings_locations_geojson'),
'assets_geojson_url': reverse('assets:publicbuildings_assets_geojson'),
},
}
result = {}
for thematic in thematics:
code = thematic.code
if code in _THEMATIC_URLS:
entry = dict(_THEMATIC_URLS[code])
entry['name'] = thematic.get_name() if hasattr(thematic, 'get_name') else str(thematic)
entry['icon'] = thematic.icon if hasattr(thematic, 'icon') else ''
result[code] = entry
return result
def build_conditional_filter_config(accessible_thematics, user=None):
"""
Returns a dict mapping thematic code -> conditional filters definition
used by the unified assets page.
"""
accessible_codes = {t.code for t in accessible_thematics}
config = {}
# Shared lists used by multiple thematics
from common.models import Municipality
municipalities = Municipality.objects.all().order_by('name_fr')
municipality_options = [
{'value': str(m.id), 'label': m.name}
for m in municipalities
]
# Traffic lights specific choices
from assets.models import TrafficLightIntersection, SignPanelType, TrafficLightContract, Structure
from assets.models.roads import QUALITY_LEVEL_CHOICES
from contracts.models import Contract
voltage_options = [
{'value': str(key), 'label': str(value)}
for key, value in TrafficLightIntersection._meta.get_field('voltage_type').choices
]
trafficlight_status_options = [
{'value': str(key), 'label': str(value)}
for key, value in TrafficLightIntersection._meta.get_field('status').choices
]
if not any(option['value'] == 'archived' for option in trafficlight_status_options):
trafficlight_status_options.append({'value': 'archived', 'label': str(_('Archivé'))})
quality_level_options = [
{'value': str(key), 'label': str(value)}
for key, value in QUALITY_LEVEL_CHOICES
]
structure_type_options = [
{'value': str(key), 'label': str(value)}
for key, value in Structure._meta.get_field('structure_type').choices
]
PANEL_CLASSES = {
'danger': _("Signaux de danger"),
'priority': _("Signaux de priorité"),
'prohibition': _("Signaux d'interdiction"),
'obligation': _("Signaux d'obligation"),
'parking': _("Arrêt / Stationnement"),
'indication': _("Signaux d'indication"),
'zone': _("Signaux de zone"),
'additional': _("Signaux additionnels"),
'other': _("Autres")
}
def get_panel_class_key(code):
code_upper = (code or '').upper()
if code_upper.startswith('A'):
return 'danger'
elif code_upper.startswith('B'):
return 'priority'
elif code_upper.startswith('C'):
return 'prohibition'
elif code_upper.startswith('D'):
return 'obligation'
elif code_upper.startswith('E'):
return 'parking'
elif code_upper.startswith('F'):
return 'indication'
elif code_upper.startswith('Z'):
return 'zone'
else:
return 'additional'
panel_type_options = []
for pt in SignPanelType.objects.all().order_by('code'):
code = (pt.code or '').strip()
if not code:
continue
group_key = get_panel_class_key(code)
group_label = str(PANEL_CLASSES.get(group_key, PANEL_CLASSES['additional']))
panel_type_options.append({
'value': str(pt.id),
'label': code,
'code': code,
'name': pt.get_name(),
'group': group_key,
'group_label': group_label,
'image_url': None,
})
controller_maintenance_provider_options = []
if user is not None:
user_config = getattr(user, 'config', None)
is_intern = bool(user_config and user_config.is_intern)
if is_intern:
contracts = Contract.objects.filter(
id__in=TrafficLightContract.objects.filter(
status='active',
maintenance_type='controller'
).values_list('contract_id', flat=True).distinct()
).select_related('company').order_by('contract_number')
controller_maintenance_provider_options = [
{'value': str(contract.id), 'label': f"{contract.contract_number} - {contract.company.name}"}
for contract in contracts
]
# Roads / public lighting / sign / artworks: municipality on locations
for thematic_code in ['roads', 'publiclighting', 'sign', 'artworks']:
if thematic_code in accessible_codes:
config.setdefault(thematic_code, []).append({
'key': 'municipality',
'label': _('Commune'),
'type': 'multi',
'options': municipality_options,
})
# Traffic lights
if 'trafficlights' in accessible_codes:
config['trafficlights'] = [
{
'key': 'status',
'label': _('Statut des carrefours'),
'type': 'multi',
'options': trafficlight_status_options,
},
{
'key': 'voltage',
'label': _('Voltage'),
'type': 'multi',
'options': voltage_options,
},
{
'key': 'connected',
'label': _('Connexion à la centrale'),
'type': 'single',
'options': [
{'value': 'yes', 'label': _('Oui')},
{'value': 'no', 'label': _('Non')},
],
},
{
'key': 'asset_begin_date',
'label': _('Date de mise en service (à partir de)'),
'type': 'date',
},
]
if controller_maintenance_provider_options:
config['trafficlights'].append({
'key': 'controller_maintenance_provider',
'label': _('Prestataire maintenance contrôleurs'),
'type': 'select',
'options': controller_maintenance_provider_options,
})
# Structures / ITS / control centers: main location flag
for thematic_code in ['structures', 'its', 'controlcenters']:
if thematic_code in accessible_codes:
config[thematic_code] = [{
'key': 'is_main_location',
'label': _('Localisation principale'),
'type': 'single',
'options': [
{'value': 'true', 'label': _('Oui')},
{'value': 'false', 'label': _('Non')},
],
}]
# Structures: structure type (same option set as dedicated page)
if 'structures' in accessible_codes:
config.setdefault('structures', []).insert(0, {
'key': 'structure_type',
'label': _("Type d'ouvrage"),
'type': 'multi',
'options': structure_type_options,
})
# Nature
if 'nature' in accessible_codes:
config['nature'] = [
{
'key': 'location_type',
'label': _('Type de localisation'),
'type': 'single',
'options': [
{'value': '', 'label': _('Tous')},
{'value': 'naturelocation', 'label': _('Voiries')},
],
},
{
'key': 'highway',
'label': _('Ring et autoroutes'),
'type': 'checkbox',
'value': '1',
},
]
# Water
if 'water' in accessible_codes:
config['water'] = [
{
'key': 'location_type',
'label': _('Type de localisation'),
'type': 'single',
'options': [
{'value': '', 'label': _('Tous')},
{'value': 'naturerwiz', 'label': _('GIEP')},
],
},
]
# Roads assets specific filter
if 'roads' in accessible_codes:
config.setdefault('roads', []).append({
'key': 'quality_level',
'label': _('Niveau de qualité'),
'type': 'multi',
'options': quality_level_options,
})
# Sign assets specific filter
if 'sign' in accessible_codes:
config.setdefault('sign', []).append({
'key': 'panel_type',
'label': _('Type de panneau'),
'type': 'multi',
'options': panel_type_options,
})
return config
def build_unified_add_modal_config(accessible_thematics, user=None):
"""
Returns the thematic choices allowed for the unified add modal.
Asset creation uses thematic edit rights. Location creation is limited to
thematics that also expose a generic location form.
"""
from assets.forms import LOCATION_FORM_REGISTRY
from assets.permissions import (
can_edit_location_for_thematic,
get_allowed_actions_for_thematic,
)
location_type_by_thematic = {
entry['thematic_code']: {
'location_type': location_type,
'label': entry.get('label', location_type),
}
for location_type, entry in LOCATION_FORM_REGISTRY.items()
}
asset_thematics = []
location_thematics = []
for thematic in accessible_thematics:
thematic_name = thematic.get_name() if hasattr(thematic, 'get_name') else str(thematic)
thematic_icon = thematic.icon if hasattr(thematic, 'icon') else ''
if 'create' in get_allowed_actions_for_thematic(user, thematic):
asset_entry = {
'code': thematic.code,
'name': thematic_name,
'icon': thematic_icon,
}
location_meta = location_type_by_thematic.get(thematic.code)
if location_meta:
asset_entry['location_type'] = location_meta['location_type']
asset_thematics.append(asset_entry)
location_meta = location_type_by_thematic.get(thematic.code)
if location_meta and can_edit_location_for_thematic(user, thematic.code):
location_thematics.append({
'code': thematic.code,
'name': thematic_name,
'icon': thematic_icon,
'location_type': location_meta['location_type'],
'location_label': location_meta['label'],
})
return {
'asset_thematics': asset_thematics,
'location_thematics': location_thematics,
}
SPECIFIC_TABLES_REGISTRY = [
{
'thematic_code': 'publicbuildings',
'title': _("Bâtiments publics"),
'description': _("Gestion du patrimoine bâti & équipements"),
'icon': 'bi-building',
'url_name': 'assets:buildings_index',
},
]
def get_user_specific_tables(user, accessible_thematics):
"""
Retourne la liste générique des répertoires/tableaux spécifiques accessibles par l'utilisateur.
"""
from django.urls import reverse
accessible_codes = {t.code for t in accessible_thematics}
specific_tables = []
for item in SPECIFIC_TABLES_REGISTRY:
if user.is_superuser or item['thematic_code'] in accessible_codes:
try:
url = reverse(item['url_name'])
specific_tables.append({
'title': item['title'],
'description': item['description'],
'icon': item['icon'],
'url': url,
})
except Exception:
pass
return specific_tables
def unified_assets_index(request):
"""Unified asset/location index page combining all accessible thematics."""
from django.utils.translation import get_language
from common.models import UserThematics
from interventions.views.drafts import InterventionDraftManager
lang = get_language() or 'fr'
user_config, _ = UserConfig.objects.get_or_create(user=request.user)
# Thématiques accessibles à l'utilisateur
if getattr(request.user, 'is_superuser', False):
accessible_thematics = list(Thematic.objects.order_by(f'{"name_nl" if lang == "nl" else "name_fr"}'))
else:
thematic_qs = (
UserThematics.objects
.filter(user_config=user_config, can_view_assets=True)
.select_related('thematic')
.order_by(f'thematic__{"name_nl" if lang == "nl" else "name_fr"}')
)
accessible_thematics = [tc.thematic for tc in thematic_qs]
# Mode intervention préventive
preventive_draft_id = request.GET.get('preventive_draft_id')
locked_thematic_code = request.GET.get('thematic_code', '')
preventive_element_type = ''
selected_assets = []
selected_locations_ids = []
if preventive_draft_id:
drafts = InterventionDraftManager(request.session)
draft_data = drafts.get(preventive_draft_id)
form_data = draft_data.get('form_data', {}) if draft_data else {}
preventive_element_type = form_data.get('type_of_selected_elements', 'locations')
if preventive_element_type == 'assets':
selected_assets = draft_data.get('assets', []) or []
else:
selected_locations_ids = draft_data.get('locations', []) or []
# Thématiques présélectionnées (GET ou fixées par le mode intervention)
selected_thematics = request.GET.getlist('thematic') or (
[locked_thematic_code] if locked_thematic_code else []
)
selected_categories = request.GET.getlist('category')
# Config JS par thématique (URLs GeoJSON, nom, icône)
thematic_config = build_thematic_config(accessible_thematics)
conditional_filter_config = build_conditional_filter_config(accessible_thematics, request.user)
add_modal_config = build_unified_add_modal_config(accessible_thematics, request.user)
# Catégories groupées par thématique
thematic_categories = {}
for thematic in accessible_thematics:
cats = list(AssetCategory.objects.filter(thematic=thematic).order_by('name_fr'))
thematic_categories[thematic.code] = [
{'id': c.id, 'name': c.get_name()} for c in cats
]
link_assets_url = (
reverse(
'interventions:intervention_preventive_link_elements',
kwargs={'thematic_code': locked_thematic_code},
)
if locked_thematic_code else ''
)
user_roles = user_config.roles.values_list('name', flat=True) if user_config else []
is_admin = 'admin' in user_roles or request.user.is_superuser
has_bulk_edit_status_permission = is_admin or UserThematics.objects.filter(user_config=user_config, can_edit_assets=True).exists()
has_bulk_edit_validation_status_permission = is_admin or UserThematics.objects.filter(user_config=user_config, can_validate_assets=True).exists()
specific_tables = get_user_specific_tables(request.user, accessible_thematics)
context = {
'accessible_thematics': accessible_thematics,
'accessible_thematic_codes': [t.code for t in accessible_thematics],
'specific_tables': specific_tables,
'has_bulk_edit_status_permission': has_bulk_edit_status_permission,
'has_bulk_edit_validation_status_permission': has_bulk_edit_validation_status_permission,
'thematic_config_json': json.dumps(thematic_config),
'conditional_filter_config_json': json.dumps(conditional_filter_config),
'thematic_categories_json': json.dumps(thematic_categories),
'asset_status_choices': list(ASSET_STATUS_CHOICES.items()),
'asset_validation_status_choices': list(ASSET_VALIDATION_STATUS_CHOICES.items()),
'add_modal_config_json': json.dumps(add_modal_config),
'selected_thematics': selected_thematics,
'selected_categories_json': json.dumps(selected_categories),
'preventive_draft_id': preventive_draft_id,
'locked_thematic_code': locked_thematic_code,
'preventive_element_type': preventive_element_type,
'selected_assets_json': json.dumps(selected_assets),
'selected_locations_ids': selected_locations_ids,
'link_assets_url': link_assets_url,
'assets_index_prefs_json': json.dumps(user_config.assets_index_preferences or {}),
'default_thematic_code': (user_config.default_thematic.code if user_config.default_thematic_id else ''),
}
return render(request, 'assets/unified_index.html', context)