995 lines
38 KiB
Python
995 lines
38 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'),
|
|
# --- Espaces plein air ---
|
|
'outdoorlocation': _('Espace public plein air'),
|
|
'outdoorasset': _('Équipement plein air'),
|
|
}
|
|
|
|
|
|
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'),
|
|
},
|
|
'outdoorfacilities': {
|
|
'locations_geojson_url': reverse('assets:outdoor_locations_geojson'),
|
|
'assets_geojson_url': reverse('assets:outdoor_assets_geojson'),
|
|
},
|
|
'outdoor': {
|
|
'locations_geojson_url': reverse('assets:outdoor_locations_geojson'),
|
|
'assets_geojson_url': reverse('assets:outdoor_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,
|
|
})
|
|
|
|
# Public buildings specific choices
|
|
from assets.models.buildings import USAGE_TYPE_CHOICES
|
|
building_usage_options = [
|
|
{'value': str(key), 'label': str(value)}
|
|
for key, value in USAGE_TYPE_CHOICES
|
|
]
|
|
|
|
# Public buildings specific filters
|
|
for thematic_code in ['publicbuildings', 'buildings']:
|
|
if thematic_code in accessible_codes:
|
|
config[thematic_code] = [
|
|
{
|
|
'key': 'usage_type',
|
|
'label': _("Type d'usage (Localisation)"),
|
|
'type': 'multi',
|
|
'options': building_usage_options,
|
|
},
|
|
{
|
|
'key': 'is_main_location',
|
|
'label': _('Localisation principale'),
|
|
'type': 'single',
|
|
'options': [
|
|
{'value': 'true', 'label': _('Oui')},
|
|
{'value': 'false', 'label': _('Non')},
|
|
],
|
|
},
|
|
{
|
|
'key': 'is_erp',
|
|
'label': _('Accueil ERP / PMR'),
|
|
'type': 'single',
|
|
'options': [
|
|
{'value': 'true', 'label': _('Oui')},
|
|
{'value': 'false', 'label': _('Non')},
|
|
],
|
|
},
|
|
]
|
|
|
|
# Outdoor facilities specific choices
|
|
from assets.models.outdoor import SITE_TYPE_CHOICES, NORM_CHOICES
|
|
outdoor_site_type_options = [
|
|
{'value': str(key), 'label': str(value)}
|
|
for key, value in SITE_TYPE_CHOICES
|
|
]
|
|
outdoor_norm_options = [
|
|
{'value': str(key), 'label': str(value)}
|
|
for key, value in NORM_CHOICES
|
|
]
|
|
|
|
for thematic_code in ['outdoorfacilities', 'outdoor']:
|
|
if thematic_code in accessible_codes:
|
|
config[thematic_code] = [
|
|
{
|
|
'key': 'site_type',
|
|
'label': _("Type de site (Localisation)"),
|
|
'type': 'multi',
|
|
'options': outdoor_site_type_options,
|
|
},
|
|
{
|
|
'key': 'norm',
|
|
'label': _("Norme de sécurité (Asset)"),
|
|
'type': 'multi',
|
|
'options': outdoor_norm_options,
|
|
},
|
|
{
|
|
'key': 'pmr_accessible',
|
|
'label': _('Accessible PMR'),
|
|
'type': 'single',
|
|
'options': [
|
|
{'value': 'true', 'label': _('Oui')},
|
|
{'value': 'false', 'label': _('Non')},
|
|
],
|
|
},
|
|
]
|
|
|
|
# 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)
|