371 lines
12 KiB
Python
371 lines
12 KiB
Python
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,
|
|
"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,
|
|
"scooter_capacity": spot.scooter_capacity,
|
|
"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)
|
|
|
|
|