import json import unicodedata from django.shortcuts import render, get_object_or_404, redirect from django.http import JsonResponse, Http404, HttpResponseForbidden from django.contrib.auth.decorators import login_required from django.db.models import Q from django.contrib.gis.geos import GEOSGeometry from django.contrib.contenttypes.models import ContentType from django.urls import reverse from django.utils.translation import gettext as _, get_language from common.models import Thematic from common.utils import check_thematic_access from assets.models.parking import ( ParkingLocation, ParkingSpot, SPOT_TYPE_CHOICES, STAND_TYPE_CHOICES, MARKING_QUALITY_CHOICES ) from assets.views.core import get_asset_edit_context MAX_ASSET_FEATURES = 5000 @login_required def parking_index(request): """Page d'accueil de la thématique Stationnement & Mobilité.""" check_thematic_access(request.user, 'parking') parking_thematic = Thematic.objects.filter(code='parking').first() context = { 'thematic': parking_thematic, 'spot_types': SPOT_TYPE_CHOICES, 'stand_types': STAND_TYPE_CHOICES, 'marking_qualities': MARKING_QUALITY_CHOICES, } return render(request, 'assets/parking_index.html', context) def parking_assets_geojson(request): """ Vue GeoJSON renvoyant les emplacements de stationnement (ParkingSpot). Prend en charge le filtrage par type d'emplacement, type d'arceau, drop zone, statut, conformité, géométrie et recherche texte. """ if request.method == "POST": try: data = json.loads(request.body) except json.JSONDecodeError: return JsonResponse({"error": "Invalid JSON"}, status=400) spot_types = data.get('spot_type') or [] if isinstance(spot_types, str): spot_types = [spot_types] stand_types = data.get('stand_type') or [] if isinstance(stand_types, str): stand_types = [stand_types] categories = data.get('categories') or data.get('category') or [] if isinstance(categories, (str, int)): categories = [categories] has_drop_zone = data.get('has_scooter_drop_zone') is_compliant = data.get('is_compliant') inspection_status = data.get('inspection_status') status = data.get('status') 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: spot_types = request.GET.getlist('spot_type', []) stand_types = request.GET.getlist('stand_type', []) categories = request.GET.getlist('category', []) has_drop_zone = request.GET.get('has_scooter_drop_zone') is_compliant = request.GET.get('is_compliant') inspection_status = request.GET.get('inspection_status') status = request.GET.get('status') filter_geom = None search_q = request.GET.get('q', '').strip() qs = ParkingSpot.objects.select_related('category', 'location').all() if categories: qs = qs.filter(category_id__in=categories) if spot_types: qs = qs.filter(spot_type__in=spot_types) if stand_types: qs = qs.filter(stand_type__in=stand_types) if has_drop_zone in ['true', '1', True]: qs = qs.filter(has_scooter_drop_zone=True) elif has_drop_zone in ['false', '0', False]: qs = qs.filter(has_scooter_drop_zone=False) if is_compliant in ['true', '1', True]: qs = qs.filter(is_compliant=True) elif is_compliant in ['false', '0', False]: qs = qs.filter(is_compliant=False) if inspection_status: qs = qs.filter(inspection_status=inspection_status) if status: qs = qs.filter(status=status) 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(note__icontains=search_q) ) qs = qs[:MAX_ASSET_FEATURES] features = [] for spot in qs: geometry = None if spot.geojson: try: geometry = json.loads(spot.geojson) except Exception: pass if not geometry and spot.geom: try: geom_wgs84 = spot.geom.clone() geom_wgs84.transform(4326) geometry = json.loads(geom_wgs84.json) except Exception: pass if not geometry: continue detail_url = reverse('assets:parking_assets_detail', kwargs={ 'asset_model': 'parkingspot', 'asset_id': spot.id }) category_name = spot.category.get_name() if spot.category else _("Emplacements de stationnement") category_id = spot.category_id if spot.category else None lon = getattr(spot, 'lon', None) lat = getattr(spot, 'lat', None) if (lon is None or lat is None) and spot.geom: try: centroid = spot.geom.centroid centroid.transform(4326) lon = centroid.x lat = centroid.y except Exception: pass spot_ct_id = ContentType.objects.get_for_model(ParkingSpot).id properties = { "id": spot.id, "code": spot.code, "content_type_id": spot_ct_id, "model_name": "parkingspot", "name_fr": spot.get_name(), "name_nl": spot.name_nl or spot.name_fr or "", "lon": lon, "lat": lat, "thematic": "parking", "asset_type": "parkingspot", "category": category_name, "category_name": category_name, "category_id": category_id, "spot_type": spot.spot_type, "spot_type_display": spot.get_spot_type_display(), "standard_stand_count": spot.standard_stand_count, "cargo_stand_count": spot.cargo_stand_count, "drop_off_stand_count": spot.drop_off_stand_count, "total_stand_count": spot.total_stand_count, "stand_type": spot.stand_type, "stand_type_display": spot.get_stand_type_display(), "has_scooter_drop_zone": spot.has_scooter_drop_zone, "capacity": spot.capacity, "marking_quality": spot.marking_quality, "is_compliant": spot.is_compliant, "inspection_status": spot.inspection_status, "funding_program": spot.funding_program, "status": spot.status, "status_display": spot.get_status_display(), "last_inspection_date": spot.last_inspection_date.isoformat() if spot.last_inspection_date else None, "detail_url": detail_url, "asset_model": "parkingspot", "element_type": "asset" } features.append({ "type": "Feature", "id": spot.id, "geometry": geometry, "properties": properties }) return JsonResponse({ "type": "FeatureCollection", "features": features }) def parking_locations_geojson(request): """ Vue GeoJSON renvoyant les zones/secteurs de stationnement (ParkingLocation). """ qs = ParkingLocation.objects.all() search_q = request.GET.get('q', '').strip() 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) ) features = [] for loc in qs: geometry = None if loc.geojson: try: geometry = json.loads(loc.geojson) except Exception: pass if not geometry and loc.geom: try: geom_wgs84 = loc.geom.clone() geom_wgs84.transform(4326) geometry = json.loads(geom_wgs84.json) except Exception: pass if not geometry: continue loc_lon = getattr(loc, 'lon', None) loc_lat = getattr(loc, 'lat', None) if (loc_lon is None or loc_lat is None) and loc.geom: try: centroid = loc.geom.centroid centroid.transform(4326) loc_lon = centroid.x loc_lat = centroid.y except Exception: pass features.append({ "type": "Feature", "id": loc.id, "geometry": geometry, "properties": { "id": loc.id, "code": loc.code, "name_fr": loc.get_name(), "address": loc.address or "", "lon": loc_lon, "lat": loc_lat, "thematic": "parking", "location_type": "parkinglocation", "element_type": "location" } }) return JsonResponse({ "type": "FeatureCollection", "features": features }) def parking_assets_autocomplete(request): """ API d'autocomplétion et de recherche pour les emplacements de stationnement. """ raw_query = (request.GET.get('q') or '').strip() qn = unicodedata.normalize('NFD', raw_query).encode('ascii', 'ignore').decode('ascii').lower() qs = ParkingSpot.objects.filter(status='active') if qn: qs = qs.filter( Q(code__icontains=qn) | Q(name_fr__icontains=qn) | Q(name_nl__icontains=qn) | Q(note__icontains=qn) ) qs = qs.order_by('code')[:20] results = [] for spot in qs: results.append({ 'id': spot.id, 'code': spot.code, 'name': spot.get_name(), 'spot_type': spot.spot_type, 'spot_type_display': spot.get_spot_type_display(), 'total_stand_count': spot.total_stand_count, 'has_scooter_drop_zone': spot.has_scooter_drop_zone, 'inspection_status': spot.inspection_status, 'detail_url': reverse('assets:parking_assets_detail', kwargs={'asset_model': 'parkingspot', 'asset_id': spot.id}) }) return JsonResponse({'results': results}) @login_required def parking_assets(request): """Vue de la liste et recherche des emplacements de stationnement.""" check_thematic_access(request.user, 'parking') spots = ParkingSpot.objects.all().select_related('location', 'street') search_q = request.GET.get('q', '').strip() if search_q: spots = spots.filter( Q(code__icontains=search_q) | Q(name_fr__icontains=search_q) | Q(name_nl__icontains=search_q) | Q(note__icontains=search_q) ) context = { 'spots': spots, 'spot_types': SPOT_TYPE_CHOICES, 'search_q': search_q, } return render(request, 'assets/parking_assets.html', context) @login_required def parking_assets_detail(request, asset_model, asset_id): """Vue de détail pour un emplacement de stationnement.""" check_thematic_access(request.user, 'parking') spot = get_object_or_404(ParkingSpot, id=asset_id) content_type = ContentType.objects.get_for_model(ParkingSpot) from inspections.models import Inspection inspections = Inspection.objects.filter( asset_content_type=content_type, asset_object_id=spot.id ).order_by('-inspection_date') from interventions.models import Intervention from interventions.permissions import filter_viewable_interventions_for_user base_interventions = filter_viewable_interventions_for_user(request.user) interventions = base_interventions.filter( interventionasset__content_type=content_type, interventionasset__object_id=spot.id ).distinct().order_by('-creation_time') context = { 'asset': spot, 'spot': spot, 'content_type': content_type, 'inspections': inspections, 'interventions': interventions, } context.update(get_asset_edit_context(request, spot, content_type)) return render(request, "assets/detail_generic.html", context)