import json from django.shortcuts import render, get_object_or_404, redirect from django.http import JsonResponse, Http404 from django.contrib.gis.geos import GEOSGeometry, Point from django.contrib.gis.db.models.functions import Distance from django.contrib.contenttypes.models import ContentType from django.utils.translation import get_language, gettext as _ from django.db.models import F, Q, Value, CharField from django.db.models.functions import Lower from django.views.decorators.http import require_GET, require_POST from django.urls import reverse from common.utils import check_thematic_access, strip_accents_lower, Unaccent from common.models import UserThematics, UserConfig from assets.models import AssetCategory, NatureRWIZ, NatureRWIAsset from interventions.models import InterventionLocation from interventions.views.drafts import InterventionDraftManager from documents.services import DocumentManagementService from documents.permissions import user_can_browse_folder @check_thematic_access('water') def water_index(request): thematic = 'water' lang = get_language() # Get filters from URL statuses = request.GET.getlist('status') selected_categories = request.GET.getlist('category') selected_models = request.GET.getlist('model') selected_location_type = request.GET.get('location_type', '') # Get preventive draft ID (if any) for selecting assets preventive_draft_id = request.GET.get('preventive_draft_id') preventive_element_type = '' selected_assets = [] selected_locations_ids = [] if preventive_draft_id: drafts = InterventionDraftManager(request.session) draft_data = drafts.get(preventive_draft_id) form_data = draft_data.get('form_data', {}) if draft_data else {} preventive_element_type = form_data.get('type_of_selected_elements', '') if preventive_element_type == 'assets': selected_assets = draft_data.get('assets', []) or [] else: selected_locations_ids = draft_data.get('locations', []) or [] 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='water')) categories.sort(key=lambda m: m.get_name().lower()) category_choices = [(m.id, m.get_name()) for m in categories] # NatureRWIAsset does not use models, only structure_type model_choices = [] category_model_map = {} context = { 'thematics': thematics, 'category_choices': category_choices, 'model_choices': model_choices, 'category_model_map': category_model_map, 'selected_thematic': selected_thematic, 'selected_statuses': statuses, 'selected_categories': selected_categories, 'selected_models': selected_models, 'selected_location_type': selected_location_type, 'preventive_draft_id': preventive_draft_id, 'preventive_element_type': preventive_element_type, 'selected_locations_ids': selected_locations_ids, "selected_assets_json": json.dumps(selected_assets), 'locations_tab_title': _("Zones GIEP"), 'locations_assets_title': _("Ouvrages"), } return render(request, "assets/water_index.html", context) @check_thematic_access('water') def water_locations_geojson(request): if request.method == "POST": try: data = json.loads(request.body) except json.JSONDecodeError: return JsonResponse({"error": "Invalid JSON"}, status=400) # 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: filter_geom = None rwiz_locations = NatureRWIZ.objects.all() if filter_geom: rwiz_locations = rwiz_locations.filter(geom__intersects=filter_geom) # Recherche plein texte if request.method == 'POST': from assets.views.core import apply_search rwiz_locations = apply_search(rwiz_locations, data) from assets.views.core import get_element_type_label def obj_to_feature(obj): geometry = json.loads(obj.geojson) if obj.geojson else None return { "type": "Feature", "id": obj.id, "geometry": geometry, "properties": { "code": obj.code, "name": obj.get_name(), "location_type": obj.__class__.__name__, "lon": obj.lon, "lat": obj.lat, "detail_url": obj.get_absolute_url() if hasattr(obj, 'get_absolute_url') else None, "element_type": get_element_type_label(obj.__class__.__name__.lower()), "thematic": "water", "is_location": True, } } features = [obj_to_feature(rwiz) for rwiz in rwiz_locations] return JsonResponse({ "type": "FeatureCollection", "features": features, }, safe=False) MAX_ASSET_FEATURES = 5000 @check_thematic_access('water') def water_assets_geojson(request): 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) sections_ids = data.get('locations_ids', []) statuses = data.get('asset_status', []) categories = data.get('category') or [] if isinstance(categories, str): categories = [categories] # 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) except Exception as e: return JsonResponse({"error": f"Invalid geometry: {str(e)}"}, status=400) else: filter_geom = None else: section_id = request.GET.get('location') sections_ids = [section_id] if section_id else [] statuses = request.GET.getlist('asset_status') categories = request.GET.getlist('category', []) filter_geom = None _search_q = (data.get('q') or '').strip() if request.method == 'POST' else '' if not request.GET.get('unified') and not sections_ids and not categories and not filter_geom and not _search_q: return JsonResponse({ "type": "FeatureCollection", "features": [], }) naturerwiasset_qs = NatureRWIAsset.objects.all() if sections_ids: # Filter by intersecting NatureRWIZ geometries rwiz_geoms = NatureRWIZ.objects.filter(id__in=sections_ids, geom__isnull=False).values_list('geom', flat=True) if rwiz_geoms: query = Q() for g in rwiz_geoms: query |= Q(geom__intersects=g) naturerwiasset_qs = naturerwiasset_qs.filter(query) else: naturerwiasset_qs = naturerwiasset_qs.none() if categories: naturerwiasset_qs = naturerwiasset_qs.filter(category__id__in=categories) if filter_geom: filter_geom_3812 = filter_geom.transform(3812, clone=True) naturerwiasset_qs = naturerwiasset_qs.filter(geom__intersects=filter_geom_3812) if statuses: naturerwiasset_qs = naturerwiasset_qs.filter(status__in=statuses) else: naturerwiasset_qs = naturerwiasset_qs.exclude(status='removed') # Recherche plein texte if request.method == 'POST': from assets.views.core import apply_search naturerwiasset_qs = apply_search(naturerwiasset_qs, data) if naturerwiasset_qs.count() > MAX_ASSET_FEATURES: return JsonResponse({"error": f"{_("Trop d'objets à afficher")} (> {MAX_ASSET_FEATURES})"}, status=413) from django.db.models import FloatField, Value, CharField from assets.views.core import get_element_type_label naturerwiasset_qs = (naturerwiasset_qs.annotate( name=F(f'name_{lang}'), category_name=F(f'category__name_{lang}'), model_name=Value(None, output_field=CharField()), color=Value(None, output_field=CharField()), location_lon=Value(None, output_field=FloatField()), location_lat=Value(None, output_field=FloatField()), ) .values( 'id', 'code', 'name', 'status', 'validation_status', 'lon', 'lat', 'surface_area', 'category_name', 'model_name', 'color', 'geojson', 'category_id', 'location_lon', 'location_lat', 'structure_type' ) .order_by('code') )[:MAX_ASSET_FEATURES] def build_geometry_and_coords(obj): geometry = None if obj.get('geojson'): try: geometry = json.loads(obj['geojson']) except (TypeError, ValueError, json.JSONDecodeError): geometry = None lon = obj.get('lon') lat = obj.get('lat') 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): geometry, lon, lat = build_geometry_and_coords(obj) detail_url = reverse('assets:water_assets_detail', kwargs={'asset_model': asset_type, 'asset_id': obj['id']}) return { "type": "Feature", "id": obj['id'], "geometry": geometry, "properties": { "code": obj['code'], "name": obj['name'], "category": obj['category_name'], "model": obj['model_name'], "category_id": obj.get('category_id'), "model_id": obj.get('model_id'), "status": obj['status'], "validation_status": obj.get('validation_status'), "lon": lon, "lat": lat, "asset_type": asset_type, "area": obj.get('surface_area', ''), "color": '#ccc', "detail_url": detail_url, "element_type": get_element_type_label(asset_type), "thematic": "water", "is_location": False, "structure_type": obj.get('structure_type', ''), } } features = [obj_to_feature(obj, "naturerwiasset") for obj in naturerwiasset_qs] return JsonResponse({ "type": "FeatureCollection", "features": features, }, safe=False) @require_GET def water_assets_autocomplete(request): raw_query = (request.GET.get('q') or '').strip() qn = strip_accents_lower(raw_query) has_search_term = len(qn) > 0 location_id = request.GET.get('location_id') category_id = request.GET.get('asset_category') try: category_id = int(category_id) except (TypeError, ValueError): category_id = None qs = NatureRWIAsset.objects.exclude(status='removed').annotate( code_u=Lower(Unaccent('code')), name_fr_u=Lower(Unaccent('name_fr')), name_nl_u=Lower(Unaccent('name_nl')), ) if has_search_term: qs = qs.filter( Q(code_u__contains=qn) | Q(name_fr_u__contains=qn) | Q(name_nl_u__contains=qn) ) if category_id is not None: qs = qs.filter(category_id=category_id) if location_id: try: loc = NatureRWIZ.objects.get(id=location_id) if loc.geom: qs = qs.filter(geom__intersects=loc.geom) except NatureRWIZ.DoesNotExist: pass items = [] for a in qs.select_related('category')[:100]: code_u = getattr(a, 'code_u', '') or '' name_fr_u = getattr(a, 'name_fr_u', '') or '' name_nl_u = getattr(a, 'name_nl_u', '') or '' prio = 7 if has_search_term: if code_u == qn: prio = 0 elif code_u.startswith(qn): prio = 1 elif name_fr_u.startswith(qn) or name_nl_u.startswith(qn): prio = 2 elif qn in code_u or qn in name_fr_u or qn in name_nl_u: prio = 4 else: prio = 5 equipment_type = a.structure_type parts = [a.code, a.get_name()] equipment_label = " - ".join(p for p in parts if p) equipment_ct_id = ContentType.objects.get_for_model(a).id items.append({ "prio": prio, "code": a.code or "", "id": a.id, "label": equipment_label, "equipment_id": a.id, "equipment_label": equipment_label, "equipment_type": equipment_type, "equipment_ct": equipment_ct_id, "location_id": None, "location_label": None, "location_ct": None, "lon": a.lon, "lat": a.lat, }) items.sort(key=lambda x: (x["prio"], x["code"])) return JsonResponse(items[:100], safe=False) @check_thematic_access('water') def zirw_detail(request, location_id): location = get_object_or_404(NatureRWIZ, id=location_id) content_type = ContentType.objects.get_for_model(location) 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 if show_inactive_assets: all_assets = location.get_all_assets().exclude(status='removed').order_by('code') else: all_assets = location.get_active_assets().order_by('code') all_assets = list(all_assets) # Build spatial hierarchy polygons = [a for a in all_assets if a.geometry_type == 'polygon'] points_and_lines = [a for a in all_assets if a.geometry_type != 'polygon'] polygon_nodes = [] child_asset_ids = set() for poly in polygons: poly_children = [] for pl in points_and_lines: if pl.geom and poly.geom and poly.geom.intersects(pl.geom): poly_children.append({ 'asset': pl, 'children': [], }) child_asset_ids.add(pl.id) polygon_nodes.append({ 'asset': poly, 'children': poly_children, }) top_level_nodes = [] top_level_nodes.extend(polygon_nodes) for pl in points_and_lines: if pl.id not in child_asset_ids: top_level_nodes.append({ 'asset': pl, 'children': [], }) top_level_nodes.sort(key=lambda n: n['asset'].code) # GeoJSON serialization from assets.views.core import get_element_type_label def asset_to_feature(asset): geom_4326 = asset.geom.transform(4326, clone=True) if asset.geom else None geometry = json.loads(geom_4326.geojson) if geom_4326 else None if geometry is None and asset.lon is not None and asset.lat is not None: geometry = { "type": "Point", "coordinates": [asset.lon, asset.lat], } detail_url = reverse('assets:water_assets_detail', kwargs={'asset_model': 'naturerwiasset', 'asset_id': asset.id}) return { "type": "Feature", "id": asset.id, "geometry": geometry, "properties": { "code": asset.code, "name": asset.get_name(), "status": asset.status, "lon": asset.lon, "lat": asset.lat, "asset_type": "naturerwiasset", "detail_url": detail_url, "element_type": get_element_type_label("naturerwiasset"), "thematic": "water", "is_location": False, "structure_type": asset.structure_type or '', "structure_type_label": asset.get_structure_type_display() if asset.structure_type else '', } } geojson_features = [asset_to_feature(asset) for asset in all_assets] assets_geojson = { "type": "FeatureCollection", "features": geojson_features } from assets.models import NatureRWIAssetDocument asset_ids = [a.id for a in all_assets] documents = NatureRWIAssetDocument.objects.filter(rwi_asset_id__in=asset_ids).select_related('rwi_asset').order_by('-upload_date') # GIEP Zone documents doc_data = DocumentManagementService.get_documents_for_asset(location) main_documents = doc_data.get("main_documents", []) document_folder = doc_data.get("folder") use_managed_documents = document_folder is not None user_config = getattr(request.user, "config", None) 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 can_browse_folder = False if document_folder: can_browse_folder = user_can_browse_folder(request.user, document_folder) interventions = [obj.intervention for obj in InterventionLocation.objects.filter(content_type=content_type, object_id=location.id).select_related('intervention').order_by('-intervention__creation_time')] context = { 'location': location, 'interventions': interventions, 'top_level_nodes': top_level_nodes, 'assets_count': len(all_assets), 'assets_geojson': assets_geojson, 'show_inactive_assets': show_inactive_assets, 'toggle_inactive_url': toggle_inactive_url, 'documents': documents, 'main_documents': main_documents, 'document_folder': document_folder, 'use_managed_documents': use_managed_documents, 'can_add_unique_tag_documents': can_add_unique_tag_documents, 'can_browse_folder': can_browse_folder, } return render(request, "assets/naturerwiz_detail.html", context) @check_thematic_access('water') def water_assets_detail(request, asset_model, asset_id): try: 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': asset_model}) asset = get_object_or_404(model_class, id=asset_id) template_name = f"assets/{asset_model}_detail.html" from django.template.loader import select_template template = select_template([template_name, "assets/detail_generic.html"]) 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)) context['thematic_code'] = 'water' return render(request, template.template.name, context) def get_nearest_water_location(request): try: lon = float(request.GET.get('lon')) lat = float(request.GET.get('lat')) except (TypeError, ValueError): return JsonResponse({"error": "Invalid coordinates"}, status=400) user_point = Point(lon, lat, srid=4326) user_point.transform(3812) nearest = ( NatureRWIZ.objects .filter(geom__isnull=False) .annotate(distance=Distance('geom', user_point)) .filter(distance__lte=50) .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()}", "lon": nearest.lon, "lat": nearest.lat, }) else: return JsonResponse({"error": _("Aucune localisation GIEP trouvée à proximité")}) @check_thematic_access('water', edit_permission_required=True) @require_POST def zirw_add_document(request, location_id): location = get_object_or_404(NatureRWIZ, pk=location_id) count = int(request.POST.get('file_count', 0)) files = request.FILES.getlist('files') user = request.user document_folder = DocumentManagementService.get_default_folder_for_object(location) if document_folder is not None: for i in range(count): file = files[i] tag_slug = request.POST.get(f'tag_{i}', None) folder_id = request.POST.get(f'folder_{i}', None) if not tag_slug: continue from documents.models import DocumentTag try: tag = DocumentTag.objects.get(slug=tag_slug) except DocumentTag.DoesNotExist: continue if tag.is_unique: can_add = DocumentManagementService.can_user_add_unique_tag_document(user, tag) if not can_add: continue if tag.is_unique: target_folder = document_folder else: from documents.models import DocumentFolder if folder_id: target_folder = DocumentFolder.objects.filter(id=folder_id).first() or document_folder else: target_folder = document_folder DocumentManagementService.add_document( file=file, tag=tag, folder=target_folder, user=user, content_object=location ) return redirect('assets:zirw_detail', location_id=location_id) @check_thematic_access('water') def fetch_document_history(request): from django.template.loader import render_to_string location_id = request.GET.get('location_id') tag_slug = request.GET.get('tag_slug') user = request.user user_config = getattr(user, "config", None) is_delete_document_allowed = False if user_config: if user_config.roles.filter(name='admin').exists(): is_delete_document_allowed = True elif UserThematics.objects.filter( user_config=user_config, thematic__code='water', can_edit_assets=True ).exists(): is_delete_document_allowed = True location = NatureRWIZ.objects.filter(pk=location_id).first() document_folder = DocumentManagementService.get_default_folder_for_object(location) if location else None if document_folder is not None and tag_slug: versions = DocumentManagementService.get_document_history_by_tag(location, 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: html = '' return JsonResponse({'html': html}) @check_thematic_access('water') @require_GET def get_document_config(request, location_id): location = get_object_or_404(NatureRWIZ, pk=location_id) user = request.user user_config = getattr(user, "config", None) 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 document_folder = DocumentManagementService.get_default_folder_for_object(location) if document_folder is None: return JsonResponse({ 'use_managed_documents': False, 'can_add_unique_tag_documents': can_add_unique_tag_documents, 'tags': [], 'folders': [], }) tags = DocumentManagementService.get_available_tags_for_thematic('water') 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 ] folders = DocumentManagementService.get_subfolders_for_object(location) return JsonResponse({ 'use_managed_documents': True, 'can_add_unique_tag_documents': can_add_unique_tag_documents, 'tags': tags_data, 'folders': folders, })