loko/streetup/assets/views/structures.py
2026-07-22 14:48:40 +02:00

1181 lines
No EOL
45 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ----------------
# 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")})