832 lines
No EOL
31 KiB
Python
832 lines
No EOL
31 KiB
Python
from django.shortcuts import render, get_object_or_404, redirect
|
||
from django.http import JsonResponse, Http404
|
||
from django.contrib.contenttypes.models import ContentType
|
||
from django.contrib.gis.db.models.functions import Transform
|
||
from django.contrib.gis.geos import Point, GEOSGeometry
|
||
from django.contrib.gis.db.models.functions import Distance
|
||
from django.views.decorators.http import require_GET, require_POST
|
||
from django.template.loader import render_to_string
|
||
from django.db.models import Value, OuterRef, Subquery, Q, Case, When
|
||
from django.db.models import IntegerField, CharField, BooleanField
|
||
from django.db.models import Func
|
||
from django.db.models.functions import Lower
|
||
from django.utils.translation import gettext as _, get_language
|
||
|
||
import json
|
||
import unicodedata
|
||
|
||
from common.models import UserThematics, Municipality
|
||
from common.utils import check_thematic_access
|
||
|
||
from assets.models import PublicLightingStreet, PublicLightingStreetDocument, PublicLightingPole, PublicLightingLightFixture, AssetCategory, PublicLightingPoleModel, PublicLightingLightFixtureModel, PublicLightingElectricalCabinet, PublicLightingElectricalCabinetModel
|
||
from assets.forms import StreetDocumentForm
|
||
|
||
from interventions.models import InterventionLocation
|
||
from interventions.views.drafts import InterventionDraftManager
|
||
|
||
|
||
@check_thematic_access('publiclighting')
|
||
def publiclighting_index(request):
|
||
thematic = 'publiclighting'
|
||
lang = get_language()
|
||
|
||
# Get filters from URL
|
||
statuses = request.GET.getlist('status')
|
||
selected_municipalities = request.GET.getlist('municipality')
|
||
selected_categories = request.GET.getlist('category')
|
||
selected_models = request.GET.getlist('model')
|
||
|
||
# Get preventive draft ID (if any) for selecting assets
|
||
preventive_draft_id = request.GET.get('preventive_draft_id')
|
||
if preventive_draft_id:
|
||
drafts = InterventionDraftManager(request.session)
|
||
draft_data = drafts.get(preventive_draft_id)
|
||
form_data = draft_data.get('form_data', {})
|
||
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', [])
|
||
|
||
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),
|
||
}
|
||
|
||
from assets.permissions import get_user_accessible_municipalities
|
||
municipalities = list(get_user_accessible_municipalities(request.user))
|
||
municipalities.sort(key=lambda m: m.name.lower())
|
||
municipality_choices = [(m.id, m.name) for m in municipalities] if len(municipalities) > 1 else []
|
||
|
||
categories = list(AssetCategory.objects.filter(thematic__code = 'publiclighting'))
|
||
categories.sort(key=lambda m: m.get_name().lower())
|
||
category_choices = [(m.id, m.get_name()) for m in categories]
|
||
|
||
models = list(PublicLightingPoleModel.objects.all())
|
||
models += list(PublicLightingLightFixtureModel.objects.all())
|
||
models += list(PublicLightingElectricalCabinetModel.objects.all())
|
||
models.sort(key=lambda m: (m.get_name() or "").lower())
|
||
model_choices = [(m.id, m.get_name()) for m in models]
|
||
|
||
context = {
|
||
'thematics': thematics,
|
||
'municipality_choices':municipality_choices,
|
||
'category_choices':category_choices,
|
||
'model_choices':model_choices,
|
||
'selected_thematic': selected_thematic,
|
||
'selected_statuses': statuses,
|
||
'selected_municipalities': selected_municipalities,
|
||
'selected_categories': selected_categories,
|
||
'selected_models': selected_models,
|
||
'preventive_draft_id': preventive_draft_id,
|
||
'preventive_element_type': preventive_element_type if 'preventive_element_type' in locals() else '',
|
||
'selected_locations_ids': selected_locations_ids if 'selected_locations_ids' in locals() else [],
|
||
"selected_assets_json": json.dumps(selected_assets) if 'selected_assets' in locals() else [],
|
||
'locations_tab_title': _("Voiries"),
|
||
'locations_assets_title': "Assets",
|
||
}
|
||
|
||
return render(request, "assets/publiclighting_index.html", context)
|
||
|
||
|
||
|
||
def publiclighting_streets(request):
|
||
pass
|
||
|
||
@check_thematic_access('publiclighting')
|
||
def publiclighting_streets_detail(request, street_id):
|
||
street = get_object_or_404(PublicLightingStreet, pk=street_id)
|
||
|
||
ct_pls = ContentType.objects.get_for_model(PublicLightingStreet)
|
||
interventions = [obj.intervention for obj in InterventionLocation.objects.filter(content_type=ct_pls, object_id=street.id).select_related('intervention').order_by('-intervention__creation_time')]
|
||
|
||
assets = street.get_active_assets()
|
||
|
||
as_built_doc = street.documents.filter(document_type='as_built').filter(status='active').order_by('-id').first()
|
||
other_docs = street.documents.filter(document_type='other').filter(status='active').order_by('-id')
|
||
|
||
street_geom_4326 = street.geom.transform(4326, clone=True)
|
||
geom = json.loads(street_geom_4326.geojson)
|
||
|
||
return render(request, "assets/publiclighting_street_detail.html",
|
||
{
|
||
"street":street,
|
||
"interventions":interventions,
|
||
"assets":assets,
|
||
'main_documents': [as_built_doc,],
|
||
"other_documents": other_docs,
|
||
'add_document_form': StreetDocumentForm(),
|
||
"geom": geom
|
||
})
|
||
|
||
|
||
@check_thematic_access('publiclighting', edit_permission_required=True)
|
||
@require_POST
|
||
def publiclighting_streets_add_document(request, street_id):
|
||
street = get_object_or_404(PublicLightingStreet, pk=street_id)
|
||
count = int(request.POST.get('file_count', 0))
|
||
files = request.FILES.getlist('files')
|
||
|
||
for i in range(count):
|
||
file = files[i]
|
||
document_type = request.POST.get(f'document_type_{i}', 'other')
|
||
|
||
PublicLightingStreetDocument.objects.create(
|
||
street=street,
|
||
file=file,
|
||
document_type=document_type,
|
||
uploaded_by = request.user
|
||
)
|
||
return redirect('assets:publiclighting_streets_detail', street_id=street_id)
|
||
|
||
@check_thematic_access('publiclighting', edit_permission_required=True)
|
||
@require_POST
|
||
def publiclighting_streets_delete_document(request, document_id):
|
||
document = get_object_or_404(PublicLightingStreetDocument, id=document_id)
|
||
street_id = document.street.id # pour rediriger vers la bonne page
|
||
|
||
# Suppression du fichier si nécessaire (en plus de l'objet DB)
|
||
if document.file:
|
||
document.file.delete(save=False)
|
||
if document.thumbnail:
|
||
document.thumbnail.delete(save=False)
|
||
|
||
document.delete()
|
||
#messages.success(request, "Le document a été supprimé avec succès.")
|
||
return redirect('assets:publiclighting_streets_detail', street_id=street_id)
|
||
|
||
|
||
@check_thematic_access('publiclighting')
|
||
def fetch_document_history(request):
|
||
street_id = request.GET.get('street_id')
|
||
document_type = request.GET.get('document_type')
|
||
|
||
documents = PublicLightingStreetDocument.objects.filter(
|
||
street_id=street_id,
|
||
document_type=document_type
|
||
).order_by('-id')
|
||
|
||
html = render_to_string('assets/document_history_list.html', {'documents': documents})
|
||
return JsonResponse({'html': html})
|
||
|
||
|
||
|
||
@check_thematic_access('publiclighting')
|
||
def publiclighting_streets_geojson(request):
|
||
if request.method == "POST":
|
||
try:
|
||
data = json.loads(request.body)
|
||
except json.JSONDecodeError:
|
||
return JsonResponse({"error": "Invalid JSON"}, status=400)
|
||
|
||
municipalities = ensure_list(data.get('municipality'))
|
||
|
||
# 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:
|
||
municipalities = request.GET.getlist('municipality')
|
||
filter_geom = None
|
||
|
||
|
||
publiclighting_streets = PublicLightingStreet.objects.all()
|
||
|
||
if municipalities:
|
||
publiclighting_streets = publiclighting_streets.filter(municipality__in=municipalities)
|
||
|
||
if filter_geom:
|
||
publiclighting_streets = publiclighting_streets.filter(geom__intersects=filter_geom)
|
||
|
||
# Recherche plein texte
|
||
if request.method == 'POST':
|
||
from assets.views.core import apply_search
|
||
publiclighting_streets = apply_search(publiclighting_streets, data)
|
||
|
||
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(),
|
||
"municipality": obj.municipality.name if obj.municipality else None,
|
||
"lon": obj.lon,
|
||
"lat": obj.lat,
|
||
"detail_url": obj.get_absolute_url() if hasattr(obj, 'get_absolute_url') else None,
|
||
"element_type": "Rue EP",
|
||
"thematic": "publiclighting",
|
||
"is_location": True,
|
||
}
|
||
}
|
||
|
||
features = []
|
||
|
||
for street in publiclighting_streets:
|
||
features.append(obj_to_feature(street))
|
||
|
||
geojson_data = {
|
||
"type": "FeatureCollection",
|
||
"features": features,
|
||
}
|
||
|
||
return JsonResponse(geojson_data, safe=False)
|
||
|
||
|
||
|
||
def publiclighting_assets(request):
|
||
pass
|
||
|
||
|
||
@check_thematic_access('publiclighting')
|
||
def publiclighting_assets_detail(request, asset_model, asset_id):
|
||
content_type = ContentType.objects.filter(app_label='assets', model=asset_model.lower()).first() or ContentType.objects.filter(model=asset_model.lower()).first()
|
||
if not content_type:
|
||
raise Http404(_("Modèle %(asset_model)s inconnu.") % {'asset_model': asset_model})
|
||
model_class = content_type.model_class()
|
||
if not model_class:
|
||
raise Http404(_("Modèle %(asset_model)s inconnu.") % {'asset_model': asset_model})
|
||
|
||
# Récupérer l’objet asset
|
||
asset = get_object_or_404(model_class, id=asset_id)
|
||
|
||
# Choisir dynamiquement le template à utiliser
|
||
template_name = f"publiclighting/assets/detail_{asset_model}.html"
|
||
|
||
# Si le template spécifique n’existe pas, tu peux fallback vers un template générique :
|
||
from django.template.loader import select_template
|
||
try:
|
||
template = select_template([template_name, "assets/detail_generic.html"])
|
||
except:
|
||
raise Http404(_("Aucun template trouvé pour ce type d’asset."))
|
||
|
||
# Get edit context
|
||
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))
|
||
|
||
# Override thematic_code for publiclighting
|
||
context['thematic_code'] = 'publiclighting'
|
||
|
||
return render(request, template.template.name, context)
|
||
|
||
|
||
def ensure_list(value):
|
||
if isinstance(value, list):
|
||
return value
|
||
if value is None:
|
||
return []
|
||
return [value]
|
||
|
||
MAX_ASSET_FEATURES = 5000 # limite imposée
|
||
|
||
@check_thematic_access('publiclighting')
|
||
def publiclighting_assets_geojson(request):
|
||
if request.method == "POST":
|
||
try:
|
||
data = json.loads(request.body)
|
||
except json.JSONDecodeError:
|
||
return JsonResponse({"error": "Invalid JSON"}, status=400)
|
||
|
||
street_ids = data.get('locations_ids', [])
|
||
statuses = data.get('asset_status', [])
|
||
begin_date = data.get('asset_begin_date')
|
||
categories = ensure_list(data.get('category'))
|
||
models = ensure_list(data.get('model'))
|
||
municipalities = ensure_list(data.get('municipality'))
|
||
|
||
# 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:
|
||
street_id = request.GET.get('location')
|
||
street_ids = [street_id] if street_id else []
|
||
statuses = request.GET.getlist('asset_status')
|
||
begin_date = request.GET.get('asset_begin_date')
|
||
categories = request.GET.getlist('category', [])
|
||
models = request.GET.getlist('model', [])
|
||
municipalities = request.GET.getlist('municipality', [])
|
||
filter_geom = None
|
||
|
||
_search_q = (data.get('q') or '').strip() if request.method == 'POST' else ''
|
||
if not request.GET.get('unified') and not street_ids and not categories and not models and not municipalities and not filter_geom and not _search_q: # return empty geojson if no param
|
||
geojson_data = {
|
||
"type": "FeatureCollection",
|
||
"features": [],
|
||
}
|
||
return JsonResponse(geojson_data)
|
||
|
||
|
||
from assets.views.core import get_element_type_label
|
||
|
||
def build_geometry_and_coords(obj):
|
||
geometry = None
|
||
if obj.geojson:
|
||
try:
|
||
geometry = json.loads(obj.geojson)
|
||
except (TypeError, ValueError, json.JSONDecodeError):
|
||
geometry = None
|
||
|
||
lon = obj.lon
|
||
lat = obj.lat
|
||
|
||
if lon is None or lat is None:
|
||
street = getattr(obj, 'street', None)
|
||
if street is not None:
|
||
if lon is None:
|
||
lon = getattr(street, 'lon', None)
|
||
if lat is None:
|
||
lat = getattr(street, 'lat', None)
|
||
|
||
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=None):
|
||
geometry, lon, lat = build_geometry_and_coords(obj)
|
||
ct_id = ContentType.objects.get_for_model(obj).id
|
||
return {
|
||
"type": "Feature",
|
||
"id": obj.id,
|
||
"geometry": geometry,
|
||
"properties": {
|
||
"id": obj.id,
|
||
"code": obj.code,
|
||
"content_type_id": ct_id,
|
||
"name": obj.get_name(),
|
||
"category": obj.category.get_name() if obj.category else None,
|
||
"model": obj.model.get_name() if obj.model else None,
|
||
"status": obj.status,
|
||
"validation_status": obj.validation_status,
|
||
"lon": lon,
|
||
"lat": lat,
|
||
"asset_type": asset_type,
|
||
"detail_url": obj.get_absolute_url() if hasattr(obj, 'get_absolute_url') else None,
|
||
"element_type": get_element_type_label(asset_type or ''),
|
||
"thematic": "publiclighting",
|
||
"is_location": False,
|
||
},
|
||
}
|
||
|
||
def add_assets_to_features(queryset, asset_type, features, feature_count):
|
||
for obj in queryset:
|
||
if feature_count >= MAX_ASSET_FEATURES:
|
||
return feature_count, True # early stop
|
||
features.append(obj_to_feature(obj, asset_type=asset_type))
|
||
feature_count += 1
|
||
return feature_count, False
|
||
|
||
features = []
|
||
feature_count = 0
|
||
|
||
pole_qs = PublicLightingPole.objects.select_related('street', 'category', 'model').all()
|
||
|
||
if street_ids:
|
||
pole_qs = pole_qs.filter(street__id__in=street_ids)
|
||
|
||
if municipalities:
|
||
pole_qs = pole_qs.filter(street__municipality_id__in=municipalities)
|
||
|
||
if models:
|
||
pole_qs = pole_qs.filter(model__id__in=models)
|
||
|
||
if categories:
|
||
pole_qs = pole_qs.filter(category__id__in=categories)
|
||
|
||
if filter_geom:
|
||
pole_qs = pole_qs.filter(
|
||
Q(geom__intersects=filter_geom) |
|
||
Q(geom__isnull=True, street__geom__intersects=filter_geom)
|
||
)
|
||
|
||
# Recherche plein texte
|
||
if request.method == 'POST':
|
||
from assets.views.core import apply_search
|
||
pole_qs = apply_search(pole_qs, data)
|
||
electrical_cabinet_pre_qs = PublicLightingElectricalCabinet.objects.select_related('street', 'category', 'model').all()
|
||
if street_ids:
|
||
electrical_cabinet_pre_qs = electrical_cabinet_pre_qs.filter(street__id__in=street_ids)
|
||
if municipalities:
|
||
electrical_cabinet_pre_qs = electrical_cabinet_pre_qs.filter(street__municipality_id__in=municipalities)
|
||
if models:
|
||
electrical_cabinet_pre_qs = electrical_cabinet_pre_qs.filter(model__id__in=models)
|
||
if categories:
|
||
electrical_cabinet_pre_qs = electrical_cabinet_pre_qs.filter(category__id__in=categories)
|
||
if filter_geom:
|
||
electrical_cabinet_pre_qs = electrical_cabinet_pre_qs.filter(
|
||
Q(geom__intersects=filter_geom) |
|
||
Q(geom__isnull=True, street__geom__intersects=filter_geom)
|
||
)
|
||
electrical_cabinet_pre_qs = apply_search(electrical_cabinet_pre_qs, data)
|
||
|
||
pole_qs = pole_qs[0:MAX_ASSET_FEATURES+1]
|
||
|
||
if len(pole_qs) >= MAX_ASSET_FEATURES:
|
||
return JsonResponse({"error": f"{_("Trop d'objets à afficher")} (> {MAX_ASSET_FEATURES})"}, status=413)
|
||
|
||
lightfixture_qs = PublicLightingLightFixture.objects.select_related('street', 'category', 'model').all()
|
||
if street_ids:
|
||
lightfixture_qs = lightfixture_qs.filter(street__id__in=street_ids)
|
||
if municipalities:
|
||
lightfixture_qs = lightfixture_qs.filter(street__municipality_id__in=municipalities)
|
||
if models:
|
||
lightfixture_qs = lightfixture_qs.filter(model__id__in=models)
|
||
if categories:
|
||
lightfixture_qs = lightfixture_qs.filter(category__id__in=categories)
|
||
if filter_geom:
|
||
lightfixture_qs = lightfixture_qs.filter(
|
||
Q(geom__intersects=filter_geom) |
|
||
Q(geom__isnull=True, street__geom__intersects=filter_geom)
|
||
)
|
||
if request.method == 'POST':
|
||
from assets.views.core import apply_search
|
||
lightfixture_qs = apply_search(lightfixture_qs, data)
|
||
lightfixture_qs = lightfixture_qs[0:MAX_ASSET_FEATURES+1]
|
||
if len(lightfixture_qs) >= MAX_ASSET_FEATURES:
|
||
return JsonResponse({"error": f"{_("Trop d'objets à afficher")} (> {MAX_ASSET_FEATURES})"}, status=413)
|
||
|
||
if request.method == 'POST' and (data.get('q') or '').strip():
|
||
electrical_cabinet_qs = electrical_cabinet_pre_qs[0:MAX_ASSET_FEATURES+1]
|
||
else:
|
||
electrical_cabinet_qs = PublicLightingElectricalCabinet.objects.select_related('street', 'category', 'model').all()
|
||
if street_ids:
|
||
electrical_cabinet_qs = electrical_cabinet_qs.filter(street__id__in=street_ids)
|
||
if municipalities:
|
||
electrical_cabinet_qs = electrical_cabinet_qs.filter(street__municipality_id__in=municipalities)
|
||
if models:
|
||
electrical_cabinet_qs = electrical_cabinet_qs.filter(model__id__in=models)
|
||
if categories:
|
||
electrical_cabinet_qs = electrical_cabinet_qs.filter(category__id__in=categories)
|
||
if filter_geom:
|
||
electrical_cabinet_qs = electrical_cabinet_qs.filter(
|
||
Q(geom__intersects=filter_geom) |
|
||
Q(geom__isnull=True, street__geom__intersects=filter_geom)
|
||
)
|
||
electrical_cabinet_qs = electrical_cabinet_qs[0:MAX_ASSET_FEATURES+1]
|
||
if len(electrical_cabinet_qs) >= MAX_ASSET_FEATURES:
|
||
return JsonResponse({"error": f"{_("Trop d'objets à afficher")} (> {MAX_ASSET_FEATURES})"}, status=413)
|
||
|
||
for qs, asset_type in [
|
||
(pole_qs, "publiclightingpole"),
|
||
(lightfixture_qs, "publiclightinglightfixture"),
|
||
(electrical_cabinet_qs, "publiclightingelectricalcabinet")
|
||
]:
|
||
feature_count, limit_reached = add_assets_to_features(qs, asset_type, features, feature_count)
|
||
if limit_reached:
|
||
return JsonResponse({
|
||
"error": f"{_("Trop d'objets à afficher")} (> {MAX_ASSET_FEATURES}). {_("Limitez votre sélection.")}"
|
||
}, status=413)
|
||
|
||
features = sorted(features, key=lambda f: f["properties"]["code"])
|
||
|
||
geojson_data = {
|
||
"type": "FeatureCollection",
|
||
"features": features,
|
||
}
|
||
|
||
return JsonResponse(geojson_data, safe=False)
|
||
|
||
|
||
@require_GET
|
||
def get_nearest_publiclighting_street(request):
|
||
try:
|
||
lon = float(request.GET.get('lon'))
|
||
lat = float(request.GET.get('lat'))
|
||
except (TypeError, ValueError):
|
||
return JsonResponse({"error": "Coordonnées invalides"}, status=400)
|
||
|
||
user_point = Point(lon, lat, srid=4326)
|
||
user_point.transform(3812)
|
||
|
||
nearest = (
|
||
PublicLightingStreet.objects
|
||
.filter(geom__isnull=False)
|
||
.annotate(distance=Distance('geom', user_point))
|
||
.filter(distance__lte=50) # filtre à 50 mètres
|
||
.order_by('distance')
|
||
.first()
|
||
)
|
||
|
||
location_ct_id = ContentType.objects.get_for_model(nearest).id
|
||
|
||
if nearest:
|
||
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 rue trouvée")})
|
||
|
||
|
||
|
||
|
||
|
||
# Appelle la fonction PostgreSQL unaccent(...)
|
||
class Unaccent(Func):
|
||
function = 'public.unaccent_immutable'
|
||
output_field = CharField()
|
||
|
||
def strip_accents_lower(s: str) -> str:
|
||
if not s:
|
||
return ''
|
||
s = ''.join(c for c in unicodedata.normalize('NFD', s) if unicodedata.category(c) != 'Mn')
|
||
return s.lower()
|
||
|
||
|
||
@require_GET
|
||
def publiclighting_locations_autocomplete(request):
|
||
raw_query = (request.GET.get('q') or '').strip()
|
||
if len(raw_query) < 2:
|
||
return JsonResponse([], safe=False)
|
||
|
||
# Normalise la requête (équivalent de unaccent+lower côté Python)
|
||
qn = strip_accents_lower(raw_query)
|
||
|
||
# Prépare des colonnes "recherchables" unaccentées + minuscules
|
||
qs = (
|
||
PublicLightingStreet.objects
|
||
.annotate(
|
||
code_u=Lower(Unaccent('code')),
|
||
name_fr_u=Lower(Unaccent('name_fr')),
|
||
name_nl_u=Lower(Unaccent('name_nl')),
|
||
)
|
||
.filter(
|
||
Q(code_u__contains=qn) | # __contains car on a déjà des champs en lower
|
||
Q(name_fr_u__contains=qn) |
|
||
Q(name_nl_u__contains=qn)
|
||
)
|
||
)
|
||
|
||
# Priorité : exact > startswith > nom startswith > contient
|
||
priority = Case(
|
||
When(code_u=qn, then=0),
|
||
When(code_u__startswith=qn, then=1),
|
||
When(name_fr_u__startswith=qn, then=2),
|
||
When(name_nl_u__startswith=qn, then=3),
|
||
When(code_u__contains=qn, then=4),
|
||
When(name_fr_u__contains=qn, then=5),
|
||
When(name_nl_u__contains=qn, then=6),
|
||
default=7,
|
||
output_field=IntegerField(),
|
||
)
|
||
|
||
qs = qs.annotate(_prio=priority).order_by('_prio', 'code')[:50]
|
||
|
||
data = [{
|
||
"label": f"{loc.code} - {loc.get_name()}",
|
||
"location_id": loc.id,
|
||
"location_label": f"{loc.code} - {loc.get_name()}",
|
||
"lon": loc.lon,
|
||
"lat": loc.lat
|
||
} for loc in qs]
|
||
|
||
return JsonResponse(data, safe=False)
|
||
|
||
|
||
|
||
|
||
# --- helpers spécifiques publiclighting ---
|
||
def _equipment_type_from_model(asset):
|
||
m = getattr(asset, 'model', None)
|
||
return (getattr(m, 'name', None) or getattr(m, 'code', None)) if m else None
|
||
|
||
def _street_of(asset):
|
||
"""
|
||
Renvoie la rue associée à un asset, directement ou via le pole (lantern/detector).
|
||
- Pole/Controller/Cable: asset.street
|
||
- Lantern/Detector: asset.pole.street si pole présent
|
||
"""
|
||
# Lantern or Detector via pole
|
||
if hasattr(asset, 'pole') and getattr(asset, 'pole', None):
|
||
return getattr(asset.pole, 'street', None)
|
||
# Others have direct FK 'intersection'
|
||
return getattr(asset, 'street', None)
|
||
|
||
def _coords_of(asset):
|
||
"""
|
||
Détermine lon/lat:
|
||
1) asset.lon/lat (si présents)
|
||
2) si PointField: coords du point (transformé en 4326 si nécessaire)
|
||
si LineString: centroid en 4326
|
||
3) via pole.geom (lantern/detector)
|
||
4) via intersection.geom (point)
|
||
"""
|
||
# 1) lon/lat directs
|
||
lon = getattr(asset, 'lon', None)
|
||
lat = getattr(asset, 'lat', None)
|
||
if lon is not None and lat is not None:
|
||
return lon, lat
|
||
|
||
# 2) géométrie sur l'asset (poles point, cables line)
|
||
geom = getattr(asset, 'geom', None)
|
||
if geom:
|
||
try:
|
||
g = geom
|
||
if getattr(g, 'srid', None) and g.srid != 4326:
|
||
g = geom.transform(4326, clone=True)
|
||
# point
|
||
if hasattr(g, 'coords'):
|
||
c = g.coords
|
||
if isinstance(c, (list, tuple)) and len(c) >= 2:
|
||
return c[0], c[1]
|
||
# line/poly: centroid
|
||
centroid = getattr(g, 'centroid', None)
|
||
if centroid:
|
||
if getattr(centroid, 'srid', None) and centroid.srid != 4326:
|
||
centroid = centroid.transform(4326, clone=True)
|
||
cc = getattr(centroid, 'coords', None)
|
||
if cc and len(cc) >= 2:
|
||
return cc[0], cc[1]
|
||
except Exception:
|
||
pass
|
||
|
||
# 3) pole.geom (lantern/detector)
|
||
pole = getattr(asset, 'pole', None)
|
||
if pole and getattr(pole, 'geom', None):
|
||
try:
|
||
g = pole.geom
|
||
if getattr(g, 'srid', None) and g.srid != 4326:
|
||
g = g.transform(4326, clone=True)
|
||
if hasattr(g, 'coords'):
|
||
c = g.coords
|
||
if c and len(c) >= 2:
|
||
return c[0], c[1]
|
||
except Exception:
|
||
pass
|
||
|
||
# 4) intersection.geom
|
||
street = _street_of(asset)
|
||
if street and getattr(street, 'geom', None):
|
||
try:
|
||
g = street.geom
|
||
if getattr(g, 'srid', None) and g.srid != 4326:
|
||
g = g.transform(4326, clone=True)
|
||
if hasattr(g, 'coords'):
|
||
c = g.coords
|
||
if c and len(c) >= 2:
|
||
return c[0], c[1]
|
||
except Exception:
|
||
pass
|
||
|
||
return None, None
|
||
|
||
@require_GET
|
||
def publiclighting_assets_autocomplete(request):
|
||
raw_query_original = (request.GET.get('q') or '')
|
||
raw_query = raw_query_original.strip()
|
||
|
||
qn = strip_accents_lower(raw_query)
|
||
has_search_term = len(qn) > 0
|
||
|
||
location_id = request.GET.get('location_id') # = id de l'intersection
|
||
category_id = request.GET.get('asset_category')
|
||
try:
|
||
category_id = int(category_id)
|
||
except (TypeError, ValueError):
|
||
category_id = None
|
||
|
||
# Prépare filtre "dans cette rue uniquement" si location_id fourni
|
||
street_filter = {}
|
||
pole_street_filter = {}
|
||
if location_id:
|
||
street_filter = {'street_id': location_id}
|
||
pole_street_filter = {'pole__street_id': location_id}
|
||
|
||
|
||
def base_qs(model, extra_filter=None, via_pole=False):
|
||
ann = {
|
||
'code_u': Lower(Unaccent('code')),
|
||
'name_fr_u': Lower(Unaccent('name_fr')),
|
||
'name_nl_u': Lower(Unaccent('name_nl')),
|
||
}
|
||
q = model.objects.annotate(**ann)
|
||
|
||
# Si un terme de recherche est fourni, filtrer dessus
|
||
if has_search_term:
|
||
q = q.filter(
|
||
Q(code_u__contains=qn) |
|
||
Q(name_fr_u__contains=qn) |
|
||
Q(name_nl_u__contains=qn)
|
||
)
|
||
|
||
# Filtrer uniquement les non supprimés
|
||
q = q.exclude(status='removed')
|
||
|
||
if extra_filter:
|
||
q = q.filter(**extra_filter)
|
||
|
||
if category_id is not None:
|
||
q = q.filter(
|
||
Q(category_id=category_id) |
|
||
Q(category__isnull=True, model__category_id=category_id)
|
||
)
|
||
|
||
# select_related utile selon le modèle
|
||
if via_pole:
|
||
q = q.select_related('pole', 'pole__street', 'model')
|
||
else:
|
||
q = q.select_related('street', 'model')
|
||
|
||
return q
|
||
|
||
# Récupère QS pour chaque type (limités grossièrement pour éviter de charger trop, puis triés/merge côté Python)
|
||
poles_qs = base_qs(PublicLightingPole, extra_filter=street_filter)[:100]
|
||
lanterns_qs = base_qs(PublicLightingLightFixture, extra_filter=pole_street_filter, via_pole=True)[:100]
|
||
|
||
# Concatène en mémoire (on priorise ensuite)
|
||
items = []
|
||
for a in list(poles_qs) + list(lanterns_qs):
|
||
# priorisation locale (recalcule ici car on merge des modèles différents)
|
||
code_u = getattr(a, 'code_u', None)
|
||
name_fr_u = getattr(a, 'name_fr_u', None)
|
||
name_nl_u = getattr(a, 'name_nl_u', None)
|
||
|
||
prio = 7
|
||
# Priorisation uniquement si un terme de recherche est fourni
|
||
if has_search_term:
|
||
if code_u == qn:
|
||
prio = 0
|
||
elif code_u and code_u.startswith(qn):
|
||
prio = 1
|
||
elif (name_fr_u and name_fr_u.startswith(qn)) or (name_nl_u and name_nl_u.startswith(qn)):
|
||
prio = 2
|
||
elif (code_u and qn in code_u) or (name_fr_u and qn in name_fr_u) or (name_nl_u and qn in name_nl_u):
|
||
prio = 4
|
||
else:
|
||
# Sans terme de recherche, on trie par code
|
||
prio = 5
|
||
|
||
street = _street_of(a)
|
||
equipment_type = _equipment_type_from_model(a)
|
||
parts = [a.code, a.get_name()]
|
||
equipment_label = " - ".join(p for p in parts if p)
|
||
code = getattr(a, 'code', '') or ''
|
||
label = equipment_label
|
||
lon, lat = _coords_of(a)
|
||
|
||
location_id_val = street.id if street else None
|
||
location_label = (f"{street.code} - {street.get_name()}" if street else None)
|
||
|
||
equipment_ct_id = ContentType.objects.get_for_model(a).id
|
||
location_ct_id = ContentType.objects.get_for_model(street).id if street else None
|
||
|
||
items.append({
|
||
"prio": prio,
|
||
"code": code or "",
|
||
"id": getattr(a, 'id', None),
|
||
"label": label,
|
||
|
||
# attendus par ton autocomplete équipements
|
||
"equipment_id": getattr(a, 'id', None),
|
||
"equipment_label": equipment_label,
|
||
"equipment_type": equipment_type,
|
||
"equipment_ct": equipment_ct_id,
|
||
|
||
"location_id": location_id_val,
|
||
"location_label": location_label,
|
||
"location_ct": location_ct_id,
|
||
|
||
"lon": lon,
|
||
"lat": lat,
|
||
})
|
||
|
||
# Tri global par priorité puis code, coupe à 100
|
||
items.sort(key=lambda x: (x["prio"], x["code"]))
|
||
data = items[:100]
|
||
|
||
return JsonResponse(data, safe=False) |