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) 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)