loko/streetup/assets/views/buildings.py

461 lines
17 KiB
Python

import json
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.db.models import Q, F
from django.http import JsonResponse, HttpResponseForbidden
from django.urls import reverse
from django.utils.translation import gettext as _, get_language
from django.contrib.gis.geos import GEOSGeometry
from common.models import UserThematics
from common.utils import check_thematic_access, get_regional_terms
from assets.permissions import can_edit_asset
from assets.forms import PublicBuildingForm, BuildingAssetForm
from assets.models.buildings import (
PublicBuilding, BuildingAsset, BuildingEquipment, PublicBuildingDocument,
USAGE_TYPE_CHOICES, BUILDING_STATUS_CHOICES, DPE_RATING_CHOICES, ERP_CATEGORY_CHOICES,
LOCATION_TYPE_CHOICES, BUILDING_ASSET_TYPE_CHOICES
)
def publicbuildings_locations_geojson(request):
"""
Vue GeoJSON renvoyant la collection des localisations de bâtiments publics pour la carte unifiée.
"""
lang = get_language() if get_language() in ('fr', 'nl') else 'fr'
if request.method == "POST":
try:
data = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({"error": "Invalid JSON"}, status=400)
statuses = data.get('asset_status', [])
location_types = data.get('location_type') or []
if isinstance(location_types, str):
location_types = [location_types]
usage_types = data.get('usage_type') or []
if isinstance(usage_types, str):
usage_types = [usage_types]
is_main_loc = data.get('is_main_location')
is_erp_val = data.get('is_erp')
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
search_q = (data.get('q') or '').strip()
else:
statuses = request.GET.getlist('asset_status')
location_types = request.GET.getlist('location_type', [])
usage_types = request.GET.getlist('usage_type', [])
is_main_loc = request.GET.get('is_main_location')
is_erp_val = request.GET.get('is_erp')
filter_geom = None
search_q = request.GET.get('q', '').strip()
qs = PublicBuilding.objects.all()
if location_types:
qs = qs.filter(location_type__in=location_types)
if usage_types:
qs = qs.filter(usage_type__in=usage_types)
if is_main_loc in ['true', '1', True]:
qs = qs.filter(is_main_location=True)
elif is_main_loc in ['false', '0', False]:
qs = qs.filter(is_main_location=False)
if is_erp_val in ['true', '1', True]:
qs = qs.filter(is_erp=True)
elif is_erp_val in ['false', '0', False]:
qs = qs.filter(is_erp=False)
if statuses:
qs = qs.filter(building_status__in=statuses)
if filter_geom:
qs = qs.filter(geom__intersects=filter_geom)
if search_q:
qs = qs.filter(
Q(code__icontains=search_q) |
Q(name_fr__icontains=search_q) |
Q(name_nl__icontains=search_q) |
Q(address__icontains=search_q) |
Q(owner__icontains=search_q)
)
features = []
from assets.views.core import get_element_type_label
for b in qs:
geometry = None
if b.geojson:
try:
geometry = json.loads(b.geojson)
except Exception:
pass
if not geometry and b.geom:
geom_4326 = b.geom.clone()
geom_4326.transform(4326)
centroid = geom_4326.centroid
geometry = {"type": "Point", "coordinates": [centroid.x, centroid.y]}
elif not geometry and b.lon is not None and b.lat is not None:
geometry = {"type": "Point", "coordinates": [b.lon, b.lat]}
detail_url = reverse('assets:publicbuilding_detail', kwargs={'pk': b.pk})
features.append({
"type": "Feature",
"id": b.id,
"geometry": geometry,
"properties": {
"code": b.code,
"name": b.get_name(),
"location_type": b.location_type,
"location_type_display": b.get_location_type_display(),
"status": b.building_status,
"building_status": b.building_status,
"building_status_display": b.get_building_status_display(),
"usage_type": b.usage_type,
"usage_type_display": b.get_usage_type_display(),
"owner": b.owner or '',
"is_erp": b.is_erp,
"erp_category": b.erp_category or '',
"dpe_rating": b.dpe_rating or '',
"lon": b.lon,
"lat": b.lat,
"color": "#2B579A",
"display_color": "#2B579A",
"asset_type": "publicbuilding",
"detail_url": detail_url,
"element_type": get_element_type_label("publicbuilding"),
"thematic": "publicbuildings",
"is_location": True,
}
})
return JsonResponse({"type": "FeatureCollection", "features": features})
def publicbuildings_assets_geojson(request):
"""
Vue GeoJSON renvoyant la collection des assets physiques de bâtiments (Génie civil, Façades, CVC, Ascenseurs...) pour la carte unifiée.
"""
lang = get_language() if get_language() in ('fr', 'nl') else 'fr'
if request.method == "POST":
try:
data = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({"error": "Invalid JSON"}, status=400)
statuses = data.get('asset_status', [])
categories = data.get('category', []) or data.get('categories', [])
if isinstance(categories, (str, int)):
categories = [categories]
asset_types = data.get('asset_type') or []
if isinstance(asset_types, str):
asset_types = [asset_types]
search_q = (data.get('q') or '').strip()
else:
statuses = request.GET.getlist('asset_status')
categories = request.GET.getlist('category')
asset_types = request.GET.getlist('asset_type', [])
search_q = request.GET.get('q', '').strip()
qs = BuildingAsset.objects.select_related('building', 'location', 'parent_asset', 'category').all()
if categories:
qs = qs.filter(category_id__in=categories)
if asset_types:
qs = qs.filter(asset_type__in=asset_types)
if statuses:
qs = qs.filter(status__in=statuses)
if search_q:
qs = qs.filter(
Q(code__icontains=search_q) |
Q(name_fr__icontains=search_q) |
Q(name_nl__icontains=search_q)
)
features = []
from assets.views.core import get_element_type_label
for a in qs:
geometry = None
if a.geojson:
try:
geometry = json.loads(a.geojson)
except Exception:
pass
if not geometry and a.geom:
geom_4326 = a.geom.clone()
geom_4326.transform(4326)
geometry = {"type": "Point", "coordinates": [geom_4326.x, geom_4326.y]}
elif not geometry and a.lon is not None and a.lat is not None:
geometry = {"type": "Point", "coordinates": [a.lon, a.lat]}
elif not geometry and a.building and a.building.lon is not None and a.building.lat is not None:
geometry = {"type": "Point", "coordinates": [a.building.lon, a.building.lat]}
detail_url = reverse('assets:publicbuildings_assets_detail', kwargs={'asset_model': 'buildingasset', 'asset_id': a.pk})
features.append({
"type": "Feature",
"id": a.id,
"geometry": geometry,
"properties": {
"code": a.code,
"name": a.get_name(),
"asset_type": a.asset_type,
"asset_type_display": a.get_asset_type_display(),
"category_id": a.category_id,
"category_name": a.category.get_name() if a.category else '',
"status": a.status,
"status_display": a.get_status_display(),
"building_code": a.building.code if a.building else '',
"building_name": a.building.get_name() if a.building else '',
"parent_asset_id": a.parent_asset_id,
"color": "#17a2b8",
"display_color": "#17a2b8",
"detail_url": detail_url,
"element_type": get_element_type_label("buildingasset"),
"thematic": "publicbuildings",
"is_location": False,
}
})
return JsonResponse({"type": "FeatureCollection", "features": features})
@login_required
def buildings_index(request):
"""
Vue principale (Index / Liste) des Localisations Bâtiments Publics.
"""
lang = get_language()
buildings = PublicBuilding.objects.select_related('parent_location', 'manager').all()
# Search filter
search_query = request.GET.get('q', '').strip()
if search_query:
buildings = buildings.filter(
Q(code__icontains=search_query) |
Q(name_fr__icontains=search_query) |
Q(name_nl__icontains=search_query) |
Q(address__icontains=search_query) |
Q(owner__icontains=search_query)
)
# Usage filter
usage_filter = request.GET.get('usage_type')
if usage_filter:
buildings = buildings.filter(usage_type=usage_filter)
# Status filter
status_filter = request.GET.get('building_status')
if status_filter:
buildings = buildings.filter(building_status=status_filter)
# Location type filter
location_type_filter = request.GET.get('location_type')
if location_type_filter:
buildings = buildings.filter(location_type=location_type_filter)
# ERP filter
erp_filter = request.GET.get('is_erp')
if erp_filter in ['true', '1']:
buildings = buildings.filter(is_erp=True)
elif erp_filter in ['false', '0']:
buildings = buildings.filter(is_erp=False)
# DPE rating filter
dpe_filter = request.GET.get('dpe_rating')
if dpe_filter:
buildings = buildings.filter(dpe_rating=dpe_filter)
building_list = []
for b in buildings:
b.can_edit = can_edit_asset(request.user, b)
building_list.append(b)
context = {
'buildings': building_list,
'search_query': search_query,
'usage_type_choices': USAGE_TYPE_CHOICES,
'building_status_choices': BUILDING_STATUS_CHOICES,
'location_type_choices': LOCATION_TYPE_CHOICES,
'dpe_rating_choices': DPE_RATING_CHOICES,
'selected_usage': usage_filter,
'selected_status': status_filter,
'selected_location_type': location_type_filter,
'selected_erp': erp_filter,
'selected_dpe': dpe_filter,
'active_tab': 'buildings',
}
return render(request, 'assets/buildings/publicbuilding_list.html', context)
def build_asset_node(asset):
return {
'asset': asset,
'children': [build_asset_node(c) for c in asset.child_assets.all()]
}
def build_location_node(loc):
if loc.parent_location is None:
loc_assets = loc.location_assets.filter(parent_asset__isnull=True) | loc.all_building_assets.filter(location__isnull=True, parent_asset__isnull=True)
loc_assets = loc_assets.distinct().prefetch_related('child_assets')
else:
loc_assets = loc.location_assets.filter(parent_asset__isnull=True).prefetch_related('child_assets')
return {
'loc': loc,
'children': [build_location_node(c) for c in loc.child_locations.all().prefetch_related('child_locations', 'location_assets')],
'assets': [build_asset_node(a) for a in loc_assets],
}
@login_required
def building_detail(request, pk):
"""
Vue détaillée d'une Localisation Bâtiment Public avec son arborescence unifiée de localisations et d'assets physiques.
"""
from django.contrib.contenttypes.models import ContentType
from interventions.models import Intervention
from interventions.permissions import filter_viewable_interventions_for_user
building = get_object_or_404(PublicBuilding.objects.select_related('parent_location', 'manager'), pk=pk)
can_edit = can_edit_asset(request.user, building)
regional_terms = get_regional_terms(getattr(building, '_regional_standard', 'BE'))
# Arborescence unifiée (Localisation courante + sous-localisations + assets rattachés)
location_tree = build_location_node(building)
parent_location = building.parent_location
documents = building.documents.all()
content_type = ContentType.objects.get_for_model(PublicBuilding)
# Récupération des interventions associées au bâtiment
base_interventions = filter_viewable_interventions_for_user(request.user)
interventions = base_interventions.filter(
interventionasset__object_id=building.id,
interventionasset__content_type=content_type
).distinct().order_by('-creation_time')
# GeoJSON pour la carte de localisation
building_geojson = None
center_lon, center_lat = None, None
if building.geom:
try:
geom_4326 = building.geom.clone()
geom_4326.transform(4326)
building_geojson = json.loads(geom_4326.json)
centroid = geom_4326.centroid
center_lon, center_lat = centroid.x, centroid.y
except Exception:
pass
elif building.lon is not None and building.lat is not None:
center_lon, center_lat = building.lon, building.lat
context = {
'building': building,
'can_edit': can_edit,
'regional_terms': regional_terms,
'location_tree': location_tree,
'parent_location': parent_location,
'equipments': building.get_all_assets(),
'documents': documents,
'interventions': interventions,
'content_type': content_type,
'building_geojson_json': json.dumps(building_geojson) if building_geojson else 'null',
'center_lon': center_lon,
'center_lat': center_lat,
}
return render(request, 'assets/buildings/publicbuilding_detail.html', context)
@login_required
def publicbuilding_edit(request, pk):
"""
Vue d'édition d'une Localisation Bâtiment Public réservée aux utilisateurs autorisés.
"""
building = get_object_or_404(PublicBuilding, pk=pk)
if not can_edit_asset(request.user, building):
return HttpResponseForbidden(_("Vous n'avez pas la permission de modifier ce bâtiment."))
if request.method == 'POST':
form = PublicBuildingForm(request.POST, instance=building)
if form.is_valid():
saved_building = form.save()
messages.success(request, _("La localisation '%s' a été mise à jour avec succès.") % saved_building.code)
return redirect('assets:publicbuilding_detail', pk=saved_building.pk)
else:
form = PublicBuildingForm(instance=building)
regional_terms = get_regional_terms(getattr(building, '_regional_standard', 'BE'))
center_lon, center_lat = None, None
if building.lon is not None and building.lat is not None:
center_lon, center_lat = building.lon, building.lat
elif building.geom:
try:
geom_4326 = building.geom.clone()
geom_4326.transform(4326)
centroid = geom_4326.centroid
center_lon, center_lat = centroid.x, centroid.y
except Exception:
pass
context = {
'building': building,
'form': form,
'regional_terms': regional_terms,
'center_lon': center_lon,
'center_lat': center_lat,
}
return render(request, 'assets/buildings/publicbuilding_form.html', context)
@login_required
def publicbuildings_assets_detail(request, asset_model, asset_id):
"""
Vue générique pour le détail d'un asset/équipement physique d'un bâtiment public.
"""
from django.contrib.contenttypes.models import ContentType
from assets.views.core import get_asset_edit_context
from interventions.models import Intervention
from interventions.permissions import filter_viewable_interventions_for_user
content_type = get_object_or_404(ContentType, app_label='assets', model=asset_model.lower())
model_class = content_type.model_class()
asset = get_object_or_404(model_class, pk=asset_id)
base_interventions = filter_viewable_interventions_for_user(request.user)
interventions = base_interventions.filter(
interventionasset__object_id=asset.id,
interventionasset__content_type=content_type
).distinct().order_by('-creation_time')
context = {
'asset': asset,
'content_type': content_type,
'interventions': interventions,
}
context.update(get_asset_edit_context(request, asset, content_type))
return render(request, "assets/detail_generic.html", context)