# ---------------- # Structures Views # ---------------- from django.shortcuts import render, get_object_or_404, redirect from django.http import JsonResponse, Http404 from django.views.decorators.http import require_GET, require_POST from django.contrib.gis.db.models.functions import Transform, Distance from django.contrib.gis.geos import GEOSGeometry, Point from django.contrib.contenttypes.models import ContentType from django.db.models import Q, Case, When, IntegerField, CharField from django.db.models.functions import Lower from django.db.models import Func, Prefetch from django.db import connection from django.template.loader import render_to_string from django.utils.translation import get_language, gettext as _ import unicodedata import json from collections import defaultdict from common.utils import check_thematic_access from common.models import Thematic, UserConfig, UserContractAccess, UserThematics from assets.models import Structure, StructureLocation, StructureGeoAsset, StructureAssetModel, StructureDocument, StructureContract, AssetCategory from interventions.models import InterventionLocation from interventions.views.drafts import InterventionDraftManager from assets.forms import StructureDocumentForm from documents.models import DocumentTag, DocumentFolder from documents.services import DocumentManagementService @check_thematic_access('structures') def structures_index(request): thematic = 'structures' lang = get_language() # Get filters from URL statuses = request.GET.getlist('status') selected_structure_types = request.GET.getlist('structure_type') selected_is_main_location = request.GET.get('is_main_location') selected_categories = request.GET.getlist('category') selected_models = request.GET.getlist('model') # Get preventive draft ID (if any) for selecting assets preventive_draft_id = request.GET.get('preventive_draft_id') if preventive_draft_id: drafts = InterventionDraftManager(request.session) draft_data = drafts.get(preventive_draft_id) form_data = draft_data.get('form_data', {}) preventive_element_type = form_data.get('type_of_selected_elements') if preventive_element_type == 'assets': selected_assets = draft_data.get('assets', []) or [] else: selected_locations_ids = draft_data.get('locations', []) thematic_choices_qs = ( UserThematics.objects .filter(user_config=request.user.config, can_view_assets=True) .select_related('thematic') .order_by(f'thematic__{"name_nl" if lang == "nl" else "name_fr"}') ) thematics = [tc.thematic for tc in thematic_choices_qs] selected_thematic = { "code": thematic, "name": next((t.name for t in thematics if t.code == thematic), None), "icon": next((t.icon for t in thematics if t.code == thematic), None), } categories = list(AssetCategory.objects.filter(thematic__code = 'structures')) categories.sort(key=lambda m: m.get_name().lower()) category_choices = [(m.id, m.get_name()) for m in categories] models = list(StructureAssetModel.objects.all()) models.sort(key=lambda m: (m.get_name() or "").lower()) model_choices = [(m.id, m.get_name()) for m in models] category_model_map = defaultdict(list) models = StructureAssetModel.objects.all() for model in models: if model.category: display_name = model.get_name() category_model_map[str(model.category_id)].append({ "key": str(model.id), "label": display_name, }) context = { 'thematics': thematics, 'structure_type_choices': Structure._meta.get_field('structure_type').choices, 'category_choices': category_choices, 'model_choices': model_choices, 'category_model_map': dict(category_model_map), 'status_choices': StructureGeoAsset._meta.get_field('status').choices, 'selected_thematic': selected_thematic, 'selected_structure_types': selected_structure_types, 'selected_is_main_location': selected_is_main_location, 'selected_categories': selected_categories, 'selected_models': selected_models, 'selected_statuses': statuses, 'preventive_draft_id': preventive_draft_id, 'preventive_element_type': preventive_element_type if 'preventive_element_type' in locals() else '', 'selected_locations_ids': selected_locations_ids if 'selected_locations_ids' in locals() else [], "selected_assets_json": json.dumps(selected_assets) if 'selected_assets' in locals() else [], 'locations_tab_title': _("Localisations"), 'locations_assets_title': "Assets", } return render(request, "assets/structures_index.html", context) def structures_locations(request): pass def structures_assets(request): pass def ensure_list(value): if isinstance(value, list): return value if value is None: return [] return [value] @check_thematic_access('structures') def structures_locations_geojson(request): if request.method == "POST": try: data = json.loads(request.body) except json.JSONDecodeError: return JsonResponse({"error": "Invalid JSON"}, status=400) structure_types = ensure_list(data.get('structure_type')) is_main_location = data.get('is_main_location') filter_geom_raw = data.get("filter_geom") if filter_geom_raw: try: filter_geom = GEOSGeometry(json.dumps(filter_geom_raw['geometry']), srid=4326) filter_geom.transform(3812) except Exception as e: return JsonResponse({"error": f"Invalid geometry: {str(e)}"}, status=400) else: filter_geom = None else: structure_types = request.GET.getlist('structure_type') is_main_location = request.GET.get('is_main_location') filter_geom = None # --------- Point d'entrée du queryset structure_locations = StructureLocation.objects.select_related('structure').all() # --------- Filtre par type de structure if structure_types: structure_locations = structure_locations.filter(structure__structure_type__in=structure_types) # --------- Filtre par localisation principale if is_main_location: val = is_main_location.lower() if val == 'true': structure_locations = structure_locations.filter(is_main_location=True) elif val == 'false': structure_locations = structure_locations.filter(is_main_location=False) # --------- Filtre géométrique if filter_geom: structure_locations = structure_locations.filter(geom__intersects=filter_geom) # --------- Limitation pour utilisateurs externes user = request.user if user.is_authenticated: try: user_config = user.config # OneToOne UserConfig except UserConfig.DoesNotExist: user_config = None # externe = pas interne ; on tient aussi compte du flag limit_assets_to_contracts is_external = (user_config is None) or (not user_config.is_intern) must_limit = is_external and (not user_config or user_config.limit_assets_to_contracts) if must_limit: # contrats accessibles à l'utilisateur (idéalement avec droit "assets") accessible_contract_ids = UserContractAccess.objects.filter( user_config=user_config, can_view_assets=True # champ prévu dans la table d'accès contrat ).values_list('contract_id', flat=True) # Ne garder que les locations dont la structure a un contrat actif lié à ces contrats structure_locations = structure_locations.filter( structure__contracts__status='active', # StructureContract.status structure__contracts__contract_id__in=accessible_contract_ids # FK contract ).distinct() else: # Non authentifié : ne rien exposer (option sûre) structure_locations = structure_locations.none() # Recherche plein texte if request.method == 'POST': from assets.views.core import apply_search structure_locations = apply_search(structure_locations, data) # --------- Conversion en GeoJSON def obj_to_feature(obj): geometry = json.loads(obj.geojson) if obj.geojson else None # AbstractLocation.geojson # :contentReference[oaicite:16]{index=16} return { "type": "Feature", "id": obj.id, "geometry": geometry, "properties": { "code": obj.code, "structure": obj.structure.code if obj.structure else "-", "name": obj.get_name(), "structure_type": obj.structure.get_structure_type_display() if (obj.structure and obj.structure.structure_type) else None, "lon": obj.lon, "lat": obj.lat, "detail_url": obj.get_absolute_url() if hasattr(obj, 'get_absolute_url') else None, "element_type": "Ouvrage d'art", "thematic": "structures", "is_location": True, } } features = [] for location in structure_locations.order_by('structure__code', 'code'): features.append(obj_to_feature(location)) geojson_data = { "type": "FeatureCollection", "features": features, } return JsonResponse(geojson_data, safe=False) MAX_ASSET_FEATURES = 5000 # limite imposée @check_thematic_access('structures') def structures_assets_geojson(request): if request.method == "POST": try: data = json.loads(request.body) except json.JSONDecodeError: return JsonResponse({"error": "Invalid JSON"}, status=400) structure_location_ids = data.get('locations_ids', []) statuses = data.get('asset_status', []) begin_date = data.get('asset_begin_date') categories = ensure_list(data.get('category')) models = ensure_list(data.get('model')) structure_types = ensure_list(data.get('structure_type')) is_main_location = data.get('is_main_location') include_no_geom = data.get('include_no_geom', False) validation_statuses = ensure_list(data.get('asset_validation_status')) # Récupération et parsing de la géométrie filter_geom_raw = data.get("filter_geom") if filter_geom_raw: try: filter_geom = GEOSGeometry(json.dumps(filter_geom_raw['geometry']), srid=4326) filter_geom.transform(3812) except Exception as e: return JsonResponse({"error": f"Invalid geometry: {str(e)}"}, status=400) else: filter_geom = None else: structure_location_id = request.GET.get('location') structure_location_ids = [structure_location_id] if structure_location_id else [] statuses = request.GET.getlist('asset_status') validation_statuses = request.GET.getlist('asset_validation_status') begin_date = request.GET.get('asset_begin_date') categories = request.GET.getlist('category', []) models = request.GET.getlist('model', []) structure_types = request.GET.getlist('structure_type') is_main_location = request.GET.get('is_main_location') include_no_geom = request.GET.get('include_no_geom') == 'true' filter_geom = None # Pas de paramètres = pas de carte (comportement existant conservé) _search_q = (data.get('q') or '').strip() if request.method == 'POST' else '' if not request.GET.get('unified') and not structure_location_ids and not categories and not models and not filter_geom and not _search_q and not structure_types and not is_main_location: geojson_data = {"type": "FeatureCollection", "features": []} return JsonResponse(geojson_data) from assets.views.core import get_element_type_label def build_geometry_and_coords(obj): geometry = None if obj.geojson: try: geometry = json.loads(obj.geojson) except (TypeError, ValueError, json.JSONDecodeError): geometry = None lon = obj.lon lat = obj.lat if lon is None or lat is None: location = getattr(obj, 'location', None) if location is not None: if lon is None: lon = getattr(location, 'lon', None) if lat is None: lat = getattr(location, 'lat', None) if geometry is None and lon is not None and lat is not None: geometry = { "type": "Point", "coordinates": [lon, lat], } return geometry, lon, lat def obj_to_feature(obj, asset_type=None): geometry, lon, lat = build_geometry_and_coords(obj) return { "type": "Feature", "id": obj.id, "geometry": geometry, "properties": { "code": obj.code, "name": obj.get_name(), "category": obj.category.get_name() if obj.category else None, "model": obj.model.get_name() if obj.model else None, "status": obj.status, "validation_status": obj.validation_status, "lon": lon, "lat": lat, "asset_type": asset_type, "detail_url": obj.get_absolute_url() if hasattr(obj, 'get_absolute_url') else None, "element_type": get_element_type_label(asset_type or ''), "thematic": "structures", "is_location": False, }, } def add_assets_to_features(queryset, asset_type, features, feature_count): for obj in queryset: if feature_count >= MAX_ASSET_FEATURES: return feature_count, True # early stop features.append(obj_to_feature(obj, asset_type=asset_type)) feature_count += 1 return feature_count, False # --------- Query de base + optimisations structure_asset_qs = ( StructureGeoAsset.objects .select_related('location', 'location__structure', 'category', 'model') .all() ) # --------- Filtres métier existants if structure_location_ids: # Étendre pour inclure tous les descendants des localisations sélectionnées all_location_ids = [] for loc_id in structure_location_ids: try: descendant_ids = get_descendant_location_ids(int(loc_id)) all_location_ids.extend(descendant_ids) except Exception: # En cas d'erreur, on ajoute juste l'ID original all_location_ids.append(loc_id) # Dédupliquer la liste all_location_ids = list(set(all_location_ids)) structure_asset_qs = structure_asset_qs.filter(location__id__in=all_location_ids) # Filtres de localisation appliqués aux assets (mode unifié et page dédiée). if structure_types: structure_asset_qs = structure_asset_qs.filter(location__structure__structure_type__in=structure_types) if is_main_location: val = is_main_location.lower() if val == 'true': structure_asset_qs = structure_asset_qs.filter(location__is_main_location=True) elif val == 'false': structure_asset_qs = structure_asset_qs.filter(location__is_main_location=False) if models: structure_asset_qs = structure_asset_qs.filter(model__id__in=models) if categories: structure_asset_qs = structure_asset_qs.filter(category__id__in=categories) if validation_statuses: structure_asset_qs = structure_asset_qs.filter(validation_status__in=validation_statuses) if filter_geom: from assets.views.core import filter_queryset_by_geom structure_asset_qs = filter_queryset_by_geom( structure_asset_qs, filter_geom, include_no_geom, geom_field='geom', location_field='location' ) # Recherche plein texte if request.method == 'POST': from assets.views.core import apply_search structure_asset_qs = apply_search(structure_asset_qs, data) # --------- Limitation pour utilisateurs externes user = request.user if user.is_authenticated: try: user_config = user.config # OneToOne except UserConfig.DoesNotExist: user_config = None is_external = (user_config is None) or (not user_config.is_intern) must_limit = is_external and (not user_config or user_config.limit_assets_to_contracts) if must_limit: accessible_contract_ids = UserContractAccess.objects.filter( user_config=user_config, can_view_assets=True ).values_list('contract_id', flat=True) # Ne garder que les assets dont la structure (via la location) a un contrat actif # auquel l'utilisateur a accès structure_asset_qs = structure_asset_qs.filter( location__structure__contracts__status='active', location__structure__contracts__contract_id__in=accessible_contract_ids ).distinct() else: # Non authentifié : ne rien exposer (plus sûr) structure_asset_qs = structure_asset_qs.none() # --------- Cap de sécurité structure_asset_qs = structure_asset_qs[0:MAX_ASSET_FEATURES+1] if len(structure_asset_qs) >= MAX_ASSET_FEATURES: return JsonResponse({"error": f"{_("Trop d'objets à afficher")} (> {MAX_ASSET_FEATURES})"}, status=413) # --------- Construction GeoJSON features = [] feature_count = 0 for qs, asset_type in [ (structure_asset_qs, "structuregeoasset"), ]: feature_count, limit_reached = add_assets_to_features(qs, asset_type, features, feature_count) if limit_reached: return JsonResponse({ "error": f"{_("Trop d'objets à afficher")} (> {MAX_ASSET_FEATURES}). {_("Limitez votre sélection.")}" }, status=413) features = sorted(features, key=lambda f: f["properties"]["code"]) geojson_data = { "type": "FeatureCollection", "features": features, } return JsonResponse(geojson_data, safe=False) @check_thematic_access('structures') def structures_locations_detail(request, location_id): show_inactive_assets = (request.GET.get('include_inactive') or '').lower() in ('1', 'true', 'yes', 'on') toggle_params = request.GET.copy() if show_inactive_assets: toggle_params.pop('include_inactive', None) else: toggle_params['include_inactive'] = '1' toggle_query = toggle_params.urlencode() toggle_inactive_url = f"{request.path}?{toggle_query}" if toggle_query else request.path asset_tree_qs = StructureGeoAsset.objects.all().order_by('code') if not show_inactive_assets: asset_tree_qs = asset_tree_qs.filter(status='active') # Préfetch (réduction N+1) : ASSETS + enfants (2 niveaux) location = ( StructureLocation.objects .prefetch_related( Prefetch( 'assets', queryset=asset_tree_qs ), Prefetch('child_locations__assets', queryset=asset_tree_qs), Prefetch('child_locations__child_locations__assets', queryset=asset_tree_qs), 'child_locations__child_locations', ) .select_related('structure') .get(pk=location_id) ) # 1) Construire la liste des ids (location + enfants) def collect_ids(loc): ids = [loc.id] for child in loc.child_locations.all(): ids.extend(collect_ids(child)) return ids all_loc_ids = collect_ids(location) # 2) Récupérer en bloc tous les liens GFK pour ces ids ct_sl = ContentType.objects.get_for_model(StructureLocation) links = ( InterventionLocation.objects .filter(content_type=ct_sl, object_id__in=all_loc_ids) .select_related('intervention') ) # 3) Indexer par location_id → [interventions...] by_loc = defaultdict(list) for link in links: by_loc[link.object_id].append(link.intervention) # 4) Arbre def build_tree(loc): return { 'loc': loc, 'assets': list(loc.assets.all()), 'interventions': by_loc.get(loc.id, []), 'children': [build_tree(child) for child in loc.child_locations.all()], } tree = build_tree(location) # 5) Aplatir les interventions de l’arbre (sans doublons) + tri global desc. par code def flatten_interventions(node): seen = set() out = [] def walk(n): for itv in n['interventions']: if itv.pk not in seen: seen.add(itv.pk) out.append(itv) for c in n['children']: walk(c) walk(node) return out interventions = sorted( flatten_interventions(tree), key=lambda itv: itv.creation_time, reverse=True ) # 6) Assets + documents assets = location.get_all_assets() if show_inactive_assets else location.get_active_assets() # --- NOUVELLE GESTION DOCUMENTAIRE --- # Récupérer les documents depuis le système de gestion documentaire doc_data = DocumentManagementService.get_documents_for_asset(location.structure) managed_main_documents = doc_data.get("main_documents", []) managed_other_documents = doc_data.get("other_documents", []) document_folder = doc_data.get("folder") # Fallback vers l'ancien système si pas de répertoire configuré if document_folder is None: if location.structure: # Ancien système: récupérer depuis StructureDocument as_built_doc = location.structure.documents.filter(document_type='as_built', status='active').order_by('-id').first() cme_doc = location.structure.documents.filter(document_type='cme', status='active').order_by('-id').first() pis_doc = location.structure.documents.filter(document_type='pis', status='active').order_by('-id').first() inspection_gc_doc = location.structure.documents.filter(document_type='inspection_ce', status='active').order_by('-id').first() inspection_em_doc = location.structure.documents.filter(document_type='inspection_em', status='active').order_by('-id').first() other_docs = location.structure.documents.filter(document_type='other', status='active').order_by('-id') main_documents = [] if as_built_doc: main_documents.append(as_built_doc) if cme_doc: main_documents.append(cme_doc) if pis_doc: main_documents.append(pis_doc) if inspection_gc_doc: main_documents.append(inspection_gc_doc) if inspection_em_doc: main_documents.append(inspection_em_doc) else: main_documents = [] other_docs = [] use_managed_documents = False else: main_documents = managed_main_documents other_docs = managed_other_documents use_managed_documents = True # --- FIN NOUVELLE GESTION DOCUMENTAIRE --- user = request.user user_config = getattr(user, "config", None) is_delete_document_allowed = False can_add_unique_tag_documents = False can_create_assets = False if user_config and user_config.is_intern: if user_config.roles.filter(name__in=["admin", "manager", "controller"]).exists(): is_delete_document_allowed = True can_create_assets = True can_add_unique_tag_documents = True elif user_config: can_add_unique_tag_documents = user_config.can_add_unique_tag_documents if user_config.roles.filter(name__in=["external_manager", "technician"]).exists(): can_create_assets = True # Récupérer les tags disponibles pour la thématique "structures" available_tags = DocumentManagementService.get_available_tags_for_thematic('structures') # Récupérer les sous-répertoires disponibles pour le choix de l'utilisateur available_folders = DocumentManagementService.get_subfolders_for_object(location.structure) if location.structure else [] # 7) Géométrie 4326 (main_location fallback) if location.geom: location_geom_4326 = location.geom.transform(4326, clone=True) elif location.structure and location.structure.main_location: location_geom_4326 = location.structure.main_location.geom.transform(4326, clone=True) else: location_geom_4326 = None geom = json.loads(location_geom_4326.geojson) if location_geom_4326 else None return render(request, "assets/structures_location_detail.html", { "location": location, "interventions": interventions, # toutes les interventions de l'arbre "assets": assets, "main_documents": main_documents, "other_documents": other_docs, "add_document_form": StructureDocumentForm(), "geom": geom, "location_tree": tree, # contient interventions par nœud "show_inactive_assets": show_inactive_assets, "toggle_inactive_url": toggle_inactive_url, "is_delete_document_allowed": is_delete_document_allowed, "can_add_unique_tag_documents": can_add_unique_tag_documents, "can_create_assets": can_create_assets, "available_tags": available_tags, "available_folders": available_folders, "document_folder": document_folder, "use_managed_documents": use_managed_documents if 'use_managed_documents' in dir() else False, }) @check_thematic_access('structures') def structures_assets_detail(request, asset_model, asset_id): try: # Récupérer le ContentType pour le modèle fourni content_type = ContentType.objects.get(model=asset_model) model_class = content_type.model_class() except ContentType.DoesNotExist: raise Http404(_("Modèle %(asset_model)s inconnu.") % (asset_model)) # Récupérer l’objet asset asset = get_object_or_404(model_class, id=asset_id) # Choisir dynamiquement le template à utiliser template_name = f"structures/detail_{asset_model}.html" # Si le template spécifique n’existe pas, tu peux fallback vers un template générique : from django.template.loader import select_template try: template = select_template([template_name, "assets/detail_generic.html"]) except: raise Http404(_("Aucun template trouvé pour ce type d’asset.")) # Get edit context from assets.views.core import get_asset_edit_context context = { "asset": asset, "content_type": content_type, } context.update(get_asset_edit_context(request, asset, content_type)) # Override thematic_code for structures context['thematic_code'] = 'structures' return render(request, template.template.name, context) # Appelle la fonction PostgreSQL unaccent(...) class Unaccent(Func): function = 'public.unaccent_immutable' output_field = CharField() def strip_accents_lower(s: str) -> str: if not s: return '' s = ''.join(c for c in unicodedata.normalize('NFD', s) if unicodedata.category(c) != 'Mn') return s.lower() # @require_GET # def structures_locations_autocomplete(request): # raw_query = (request.GET.get('q') or '').strip() # if len(raw_query) < 2: # return JsonResponse([], safe=False) # # Normalise la requête (équivalent de unaccent+lower côté Python) # qn = strip_accents_lower(raw_query) # # Prépare des colonnes "recherchables" unaccentées + minuscules # qs = ( # StructureLocation.objects # .annotate( # code_u=Lower(Unaccent('code')), # name_fr_u=Lower(Unaccent('name_fr')), # name_nl_u=Lower(Unaccent('name_nl')), # ) # .filter( # Q(code_u__contains=qn) | # __contains car on a déjà des champs en lower # Q(name_fr_u__contains=qn) | # Q(name_nl_u__contains=qn) # ) # ) # # Priorité : exact > startswith > nom startswith > contient # priority = Case( # When(code_u=qn, then=0), # When(code_u__startswith=qn, then=1), # When(name_fr_u__startswith=qn, then=2), # When(name_nl_u__startswith=qn, then=3), # When(code_u__contains=qn, then=4), # When(name_fr_u__contains=qn, then=5), # When(name_nl_u__contains=qn, then=6), # default=7, # output_field=IntegerField(), # ) # qs = qs.annotate(_prio=priority).order_by('_prio', 'code')[:50] # data = [{ # "label": f"{loc.code} - {loc.get_name()}", # "location_id": loc.id, # "location_label": f"{loc.get_name()} ({loc.code})", # "lon": ( # loc.lon if loc.lon is not None # else (loc.structure.main_location.lon # if getattr(loc, "structure", None) and getattr(loc.structure, "main_location", None) # else None) # ), # "lat": ( # loc.lat if loc.lat is not None # else (loc.structure.main_location.lat # if getattr(loc, "structure", None) and getattr(loc.structure, "main_location", None) # else None) # ), # "structure_id": loc.structure.id if getattr(loc, "structure", None) else None, # "structure_label": ( # f"{loc.structure.get_name()} ({loc.structure.code})" # if getattr(loc, "structure", None) else None # ), # } for loc in qs] # return JsonResponse(data, safe=False) def get_descendant_location_ids(root_location_id: int) -> list[int]: """ Récupère l'id de la localisation racine + tous ses descendants (FK parent_location) via un CTE récursif. Nécessite PostgreSQL. """ # Récupérer dynamiquement le nom de la table depuis le modèle Django table_name = StructureLocation._meta.db_table with connection.cursor() as cur: cur.execute(f""" WITH RECURSIVE loc_tree AS ( SELECT id FROM {table_name} WHERE id = %s UNION ALL SELECT sl.id FROM {table_name} sl JOIN loc_tree lt ON sl.parent_location_id = lt.id ) SELECT id FROM loc_tree; """, [root_location_id]) rows = cur.fetchall() return [r[0] for r in rows] @require_GET def structures_assets_autocomplete(request): raw_query_original = (request.GET.get('q') or '') raw_query = raw_query_original.strip() # CT ids (calculés une fois) equipment_ct_id = ContentType.objects.get_for_model(StructureGeoAsset).id location_ct_id = ContentType.objects.get_for_model(StructureLocation).id qn = strip_accents_lower(raw_query) has_search_term = len(qn) > 0 # filtres optionnels # Supporte à la fois location_id (singulier) et location_ids (pluriel via getlist) location_id = request.GET.get('location_id') location_ids_param = request.GET.getlist('location_ids') or request.GET.getlist('location_id') structure_id = request.GET.get('structure_id') category_id = request.GET.get('asset_category') try: category_id = int(category_id) except (TypeError, ValueError): category_id = None # si location(s) fournie(s), calcule l'ensemble {locations + tous les descendants} location_ids = None input_location_ids = [] # Collecter tous les IDs de localisations fournis if location_id: input_location_ids.append(location_id) if location_ids_param: input_location_ids.extend(location_ids_param) # Dédupliquer input_location_ids = list(set(input_location_ids)) if input_location_ids: all_location_ids = [] for loc_id in input_location_ids: try: descendant_ids = get_descendant_location_ids(int(loc_id)) all_location_ids.extend(descendant_ids) except Exception: # en cas d'ID invalide ou autre, on ignore cet ID pass # Dédupliquer la liste finale location_ids = list(set(all_location_ids)) if all_location_ids else None # Base queryset accent-insensible + actifs uniquement qs = ( StructureGeoAsset.objects .annotate( code_u=Lower(Unaccent('code')), name_fr_u=Lower(Unaccent('name_fr')), name_nl_u=Lower(Unaccent('name_nl')), ) ) # Si un terme de recherche est fourni, filtrer dessus if has_search_term: qs = qs.filter( Q(code_u__contains=qn) | Q(name_fr_u__contains=qn) | Q(name_nl_u__contains=qn) ) qs = qs.exclude(status='removed') # <-- exclure les supprimés qs = qs.select_related( 'location', 'location__structure', 'structure', 'structure__main_location', 'model', 'category', ) if location_ids: qs = qs.filter(location_id__in=location_ids) if structure_id: qs = qs.filter(structure_id=structure_id) if category_id is not None: qs = qs.filter( Q(category_id=category_id) | Q(category__isnull=True, model__category_id=category_id) ) # Priorité : exact > code startswith > nom startswith > contient if has_search_term: priority = Case( When(code_u=qn, then=0), When(code_u__startswith=qn, then=1), When(name_fr_u__startswith=qn, then=2), When(name_nl_u__startswith=qn, then=3), When(code_u__contains=qn, then=4), When(name_fr_u__contains=qn, then=5), When(name_nl_u__contains=qn, then=6), default=7, output_field=IntegerField(), ) qs = qs.annotate(_prio=priority).order_by('_prio', 'code')[:100] else: # Sans terme de recherche, tri par code uniquement qs = qs.order_by('code')[:100] data = [] for a in qs: # equipment_type depuis model/category equipment_type = None if getattr(a, 'model', None): equipment_type = getattr(a.model, 'name', None) or getattr(a.model, 'code', None) if not equipment_type and getattr(a, 'category', None): equipment_type = getattr(a.category, 'name', None) # coordonnées: asset > geom(4326) > location > structure.main_location lon = a.lon lat = a.lat if (lon is None or lat is None) and getattr(a, 'geom', None): try: g = a.geom if g.srid and g.srid != 4326: g = a.geom.transform(4326, clone=True) coords = getattr(g, 'coords', None) if coords: if lon is None: lon = coords[0] if lat is None: lat = coords[1] except Exception: pass if (lon is None or lat is None) and getattr(a, 'location', None): if lon is None and getattr(a.location, 'lon', None) is not None: lon = a.location.lon if lat is None and getattr(a.location, 'lat', None) is not None: lat = a.location.lat main_loc = (a.structure.main_location if getattr(a, 'structure', None) and getattr(a.structure, 'main_location', None) else (a.location.structure.main_location if getattr(a, 'location', None) and getattr(a.location, 'structure', None) and getattr(a.location.structure, 'main_location', None) else None)) if (lon is None or lat is None) and main_loc: if lon is None: lon = getattr(main_loc, 'lon', None) if lat is None: lat = getattr(main_loc, 'lat', None) equipment_label = f"{a.get_name()} ({a.code})" if a.code else a.get_name() # label = f"{a.code} - {equipment_label}" if a.code else equipment_label label = equipment_label location_label = ( f"{a.location.get_name()} ({a.location.code})" if getattr(a, "location", None) else None ) struct = a.structure or (a.location.structure if getattr(a, "location", None) else None) structure_id_val = struct.id if struct else None structure_label = (f"{struct.get_name()} ({struct.code})" if struct else None) data.append({ "id": a.id, "label": label, "equipment_id": a.id, "equipment_label": equipment_label, "equipment_type": equipment_type, "equipment_ct": equipment_ct_id, "location_id": a.location.id if getattr(a, "location", None) else None, "location_label": location_label, "location_ct": location_ct_id, "parent_ref_id": structure_id_val, "parent_ref_label": structure_label, "lon": lon, "lat": lat, }) return JsonResponse(data, safe=False) @check_thematic_access('structures', edit_permission_required=True) @require_POST def structures_add_document(request, structure_id): structure = get_object_or_404(Structure, pk=structure_id) count = int(request.POST.get('file_count', 0)) files = request.FILES.getlist('files') user = request.user user_config = getattr(user, "config", None) # Vérifier si le nouveau système de gestion documentaire est activé document_folder = DocumentManagementService.get_default_folder_for_object(structure) for i in range(count): file = files[i] tag_slug = request.POST.get(f'tag_{i}', '') folder_id = request.POST.get(f'folder_{i}', '') # Nouveau système de gestion documentaire if document_folder is not None and tag_slug: try: tag = DocumentTag.objects.get(slug=tag_slug) except DocumentTag.DoesNotExist: tag = None # Vérifier les permissions pour les tags uniques if tag and tag.is_unique: can_add = DocumentManagementService.can_user_add_unique_tag_document(user, tag) if not can_add: # L'utilisateur n'a pas les permissions, on skip ce fichier continue # Déterminer le répertoire cible target_folder = None if folder_id: try: target_folder = DocumentFolder.objects.get(pk=int(folder_id)) except (DocumentFolder.DoesNotExist, ValueError): target_folder = document_folder else: target_folder = document_folder # Ajouter le document via le service DocumentManagementService.add_document( file=file, tag=tag, folder=document_folder, user=user, title=file.name if hasattr(file, 'name') else None, content_object=structure, target_folder=target_folder ) else: # Fallback vers l'ancien système (StructureDocument) document_type = request.POST.get(f'document_type_{i}', 'other') StructureDocument.objects.create( structure=structure, file=file, document_type=document_type, uploaded_by=user ) return redirect('assets:structures_locations_detail', location_id=structure.main_location.id) @check_thematic_access('structures', edit_permission_required=True) @require_POST def structures_delete_document(request, document_id): document = get_object_or_404(StructureDocument, id=document_id) structure_id = document.structure.id # pour rediriger vers la bonne page # Suppression du fichier si nécessaire (en plus de l'objet DB) if document.file: document.file.delete(save=False) if document.thumbnail: document.thumbnail.delete(save=False) document.delete() #messages.success(request, "Le document a été supprimé avec succès.") return redirect('assets:structures_locations_detail', location_id=document.structure.main_location.id) @check_thematic_access('structures') def fetch_document_history(request): structure_id = request.GET.get('structure_id') document_type = request.GET.get('document_type') tag_slug = request.GET.get('tag_slug') user = request.user user_config = getattr(user, "config", None) is_delete_document_allowed = False if user_config and user_config.is_intern: if user_config.roles.filter(name__in=["admin", "manager"]).exists(): is_delete_document_allowed = True # Vérifier si on utilise le nouveau système de gestion documentaire structure = Structure.objects.filter(pk=structure_id).first() if structure: document_folder = DocumentManagementService.get_default_folder_for_object(structure) else: document_folder = None if document_folder is not None and tag_slug: # Nouveau système: récupérer l'historique des versions versions = DocumentManagementService.get_document_history_by_tag(structure, tag_slug) html = render_to_string('assets/document_version_history_list.html', { 'versions': versions, 'is_delete_document_allowed': is_delete_document_allowed, 'use_managed_documents': True }) else: # Ancien système: récupérer depuis StructureDocument documents = StructureDocument.objects.filter( structure_id=structure_id, document_type=document_type ).order_by('-id') html = render_to_string('assets/document_history_list.html', { 'documents': documents, 'is_delete_document_allowed': is_delete_document_allowed }) return JsonResponse({'html': html}) @check_thematic_access('structures') @require_GET def get_document_config(request, structure_id): """ API pour récupérer la configuration des documents (tags et répertoires disponibles). Utilisé par le JavaScript pour le formulaire d'ajout de document. """ structure = get_object_or_404(Structure, pk=structure_id) user = request.user user_config = getattr(user, "config", None) # Vérifier les permissions can_add_unique_tag_documents = False if user_config and user_config.is_intern: can_add_unique_tag_documents = True elif user_config: can_add_unique_tag_documents = user_config.can_add_unique_tag_documents # Vérifier si le nouveau système est activé document_folder = DocumentManagementService.get_default_folder_for_object(structure) if document_folder is None: return JsonResponse({ 'use_managed_documents': False, 'can_add_unique_tag_documents': can_add_unique_tag_documents, 'tags': [], 'folders': [], }) # Récupérer les tags disponibles pour la thématique "structures" tags = DocumentManagementService.get_available_tags_for_thematic('structures') tags_data = [ { 'slug': tag.slug, 'name': tag.name, 'is_unique': tag.is_unique, 'show_in_asset_detail': tag.show_in_asset_detail, } for tag in tags ] # Récupérer les sous-répertoires disponibles folders = DocumentManagementService.get_subfolders_for_object(structure) return JsonResponse({ 'use_managed_documents': True, 'can_add_unique_tag_documents': can_add_unique_tag_documents, 'tags': tags_data, 'folders': folders, }) @require_GET def get_nearest_structure_location(request): try: lon = float(request.GET.get('lon')) lat = float(request.GET.get('lat')) except (TypeError, ValueError): return JsonResponse({"error": "Coordonnées invalides"}, status=400) user_point = Point(lon, lat, srid=4326) user_point.transform(3812) nearest = ( StructureLocation.objects .filter(geom__isnull=False) .annotate(distance=Distance('geom', user_point)) .filter(distance__lte=50) # filtre à 50 mètres .order_by('distance') .first() ) if nearest: location_ct_id = ContentType.objects.get_for_model(nearest).id return JsonResponse({ "location_id": nearest.id, "location_ct": location_ct_id, "location_label": f"{nearest.code} - {nearest.get_name()}", "parent_ref_label": f"{nearest.structure.get_name()} ({nearest.structure.code})" if nearest.structure else None, "lon": nearest.lon, "lat": nearest.lat }) else: return JsonResponse({"error": _("Aucune localisation structure trouvée")})