loko/streetup/assets/views/parking.py

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)