loko/streetup/assets/views/publiclighting.py
2026-07-22 14:48:40 +02:00

828 lines
No EOL
30 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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),
}
municipalities = list(Municipality.objects.all())
municipalities.sort(key=lambda m: m.name.lower())
municipality_choices = [(m.id, m.name) for m in municipalities]
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):
try:
# Récupérer le ContentType pour le modèle fourni (ex: 'trafficlightpole')
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))
# 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)
return {
"type": "Feature",
"id": obj.id,
"geometry": geometry,
"properties": {
"code": obj.code,
"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)