1181 lines
No EOL
45 KiB
Python
1181 lines
No EOL
45 KiB
Python
# ----------------
|
||
# 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")}) |