Compare commits
2 commits
bb8d6f98c0
...
80c54a083d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80c54a083d | ||
|
|
7d5718dec2 |
15 changed files with 1006 additions and 103 deletions
|
|
@ -451,7 +451,10 @@ admin.site.register(ComplementaryPoliceRegulation, AssetAdmin)
|
||||||
admin.site.register(SignParkingSpot, AssetAdmin)
|
admin.site.register(SignParkingSpot, AssetAdmin)
|
||||||
|
|
||||||
|
|
||||||
from .models import ITSAssetModel, ITSGeoAsset, ITSGeoAssetContract
|
from .models import (
|
||||||
|
ITSAssetModel, ITSGeoAsset, ITSGeoAssetContract,
|
||||||
|
ITSBollardSystem, ITSBollardPlot, ITSBollardSystemAsset, ITSBollardSystemContract
|
||||||
|
)
|
||||||
|
|
||||||
admin.site.register(ITSAssetModel, AssetAdmin)
|
admin.site.register(ITSAssetModel, AssetAdmin)
|
||||||
|
|
||||||
|
|
@ -468,6 +471,44 @@ class ITSGeoAssetContractAdmin(AssetAdmin):
|
||||||
list_filter = ("contract",)
|
list_filter = ("contract",)
|
||||||
|
|
||||||
|
|
||||||
|
class ITSBollardPlotInline(admin.TabularInline):
|
||||||
|
model = ITSBollardPlot
|
||||||
|
extra = 0
|
||||||
|
fields = ("code", "position", "bollard_type", "model", "serial_number", "status")
|
||||||
|
show_change_link = True
|
||||||
|
|
||||||
|
|
||||||
|
class ITSBollardSystemAssetInline(admin.TabularInline):
|
||||||
|
model = ITSBollardSystemAsset
|
||||||
|
extra = 0
|
||||||
|
fields = ("asset", "role", "comment")
|
||||||
|
autocomplete_fields = ("asset",)
|
||||||
|
show_change_link = True
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(ITSBollardSystem)
|
||||||
|
class ITSBollardSystemAdmin(AssetGISAdmin, AssetAdmin):
|
||||||
|
search_fields = ("code", "name_fr", "name_nl", "location__code", "location__name_fr")
|
||||||
|
list_display = ("code", "name_fr", "location", "width", "direction", "status", "plots_count")
|
||||||
|
list_filter = ("status", "direction", "location")
|
||||||
|
inlines = [ITSBollardPlotInline, ITSBollardSystemAssetInline]
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(ITSBollardPlot)
|
||||||
|
class ITSBollardPlotAdmin(AssetGISAdmin, AssetAdmin):
|
||||||
|
search_fields = ("code", "name_fr", "system__code", "serial_number")
|
||||||
|
list_display = ("code", "system", "position", "bollard_type", "model", "status")
|
||||||
|
list_filter = ("bollard_type", "status", "system")
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(ITSBollardSystemContract)
|
||||||
|
class ITSBollardSystemContractAdmin(AssetAdmin):
|
||||||
|
search_fields = ("system__code", "contract__contract_number")
|
||||||
|
list_display = ("system", "contract", "status", "maintenance_type")
|
||||||
|
list_filter = ("status", "maintenance_type", "contract")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
from .models import ITSLocation, ITSLocationDocument, ITSLocationContract
|
from .models import ITSLocation, ITSLocationDocument, ITSLocationContract
|
||||||
|
|
||||||
admin.site.register(ITSLocation, AssetAdmin)
|
admin.site.register(ITSLocation, AssetAdmin)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
# Generated by Django 6.0.7 on 2026-10-08 10:19
|
||||||
|
|
||||||
|
import assets.models.core
|
||||||
|
import django.contrib.gis.db.models.fields
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('assets', '0030_managementzone_managementzonedocument_and_more'),
|
||||||
|
('contracts', '0014_contract_color_contract_geom'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='itsgeoasset',
|
||||||
|
name='extra_data',
|
||||||
|
field=models.JSONField(blank=True, default=dict, verbose_name='Métadonnées techniques'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ITSBollardSystem',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('code', models.CharField(max_length=50)),
|
||||||
|
('name_fr', models.CharField(blank=True, max_length=200, null=True)),
|
||||||
|
('name_nl', models.CharField(blank=True, max_length=200, null=True)),
|
||||||
|
('external_reference', models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe')),
|
||||||
|
('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50)),
|
||||||
|
('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)),
|
||||||
|
('installation_date', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('uninstallation_date', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('serial_number', models.CharField(blank=True, max_length=200, null=True)),
|
||||||
|
('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')),
|
||||||
|
('last_inspection_date', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('inspection_status', models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection")),
|
||||||
|
('last_preventive_date', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('last_corrective_date', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('last_survey_date', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('brand', models.CharField(blank=True, max_length=500, null=True)),
|
||||||
|
('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')),
|
||||||
|
('note', models.TextField(blank=True, null=True, verbose_name='Note')),
|
||||||
|
('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')),
|
||||||
|
('funding_program', models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('lon', models.FloatField(blank=True, null=True)),
|
||||||
|
('lat', models.FloatField(blank=True, null=True)),
|
||||||
|
('geojson', models.TextField(blank=True, null=True)),
|
||||||
|
('geom', django.contrib.gis.db.models.fields.LineStringField(blank=True, null=True, srid=3812, verbose_name='Géométrie (Ligne)')),
|
||||||
|
('width', models.FloatField(blank=True, null=True, verbose_name='Largeur de passage (m)')),
|
||||||
|
('direction', models.CharField(blank=True, choices=[('unidirectional', 'Sens unique'), ('bidirectional', 'Double sens')], default='bidirectional', max_length=50, null=True, verbose_name='Sens de circulation')),
|
||||||
|
('extra_data', models.JSONField(blank=True, default=dict, verbose_name='Métadonnées techniques')),
|
||||||
|
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory')),
|
||||||
|
('location', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bollard_systems', to='assets.itslocation', verbose_name='Localisation ITS')),
|
||||||
|
('marked_as_removed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par')),
|
||||||
|
('model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='assets.itsassetmodel', verbose_name='Modèle de système')),
|
||||||
|
('replaced_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.itsbollardsystem', verbose_name='Remplacé par')),
|
||||||
|
('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Système de bornes',
|
||||||
|
'verbose_name_plural': 'Systèmes de bornes',
|
||||||
|
},
|
||||||
|
bases=(assets.models.core.GeoSyncMixin, models.Model),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ITSBollardSystemAsset',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('role', models.CharField(choices=[('camera_entry', 'Caméra entrée (LAPI)'), ('camera_exit', 'Caméra sortie (LAPI)'), ('camera_cctv', 'Caméra de contexte / surveillance'), ('cabinet', 'Armoire électrique / automate'), ('totem', 'Totem de commande'), ('traffic_light', 'Feu de franchissement'), ('loop', 'Boucle de détection'), ('other', 'Autre')], default='other', max_length=50, verbose_name="Rôle de l'équipement")),
|
||||||
|
('comment', models.CharField(blank=True, max_length=255, null=True, verbose_name='Commentaire')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name="Date d'association")),
|
||||||
|
('asset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bollard_system_links', to='assets.itsgeoasset', verbose_name='Équipement ITS')),
|
||||||
|
('bollard_system', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='system_assets', to='assets.itsbollardsystem', verbose_name='Système de bornes')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Équipement associé au système de bornes',
|
||||||
|
'verbose_name_plural': 'Équipements associés au système de bornes',
|
||||||
|
'unique_together': {('bollard_system', 'asset')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='itsbollardsystem',
|
||||||
|
name='associated_assets',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='bollard_systems', through='assets.ITSBollardSystemAsset', to='assets.itsgeoasset', verbose_name='Équipements associés'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ITSBollardSystemContract',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('status', models.CharField(choices=[('active', 'Actif'), ('inactive', 'Inactif'), ('archived', 'Archivé')], default='active', max_length=50)),
|
||||||
|
('maintenance_type', models.CharField(choices=[('equipments', 'Equipements'), ('office', 'Bureautique'), ('system', 'Système'), ('other', 'Autre')], default='other', max_length=50)),
|
||||||
|
('contract', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bollard_systems_contracts', to='contracts.contract')),
|
||||||
|
('system', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bollard_systems_contracts', to='assets.itsbollardsystem')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Contrat de système de bornes',
|
||||||
|
'verbose_name_plural': 'Contrats de systèmes de bornes',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ITSBollardPlot',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('code', models.CharField(max_length=50)),
|
||||||
|
('name_fr', models.CharField(blank=True, max_length=200, null=True)),
|
||||||
|
('name_nl', models.CharField(blank=True, max_length=200, null=True)),
|
||||||
|
('external_reference', models.CharField(blank=True, max_length=100, null=True, verbose_name='Référence externe')),
|
||||||
|
('status', models.CharField(choices=[('to_be_approved', 'À approuver'), ('planned', 'Planifié'), ('active', 'Actif'), ('damaged', 'Endommagé'), ('to_replace', 'A remplacer'), ('voluntarily_out_of_service', 'Mis hors service volontairement'), ('removed', 'Enlevé'), ('archived', 'Archivé')], default='active', max_length=50)),
|
||||||
|
('obsolescence', models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('bad', 'Dégradé'), ('critical', 'Critique'), ('unknown', 'Inconnu')], default='unknown', null=True)),
|
||||||
|
('installation_date', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('uninstallation_date', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('serial_number', models.CharField(blank=True, max_length=200, null=True)),
|
||||||
|
('warranty_duration', models.IntegerField(blank=True, null=True, verbose_name='Duration of the warranty (months)')),
|
||||||
|
('last_inspection_date', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('inspection_status', models.CharField(blank=True, choices=[('to_inspect', 'À inspecter'), ('compliant', 'Conforme'), ('non_compliant', 'Non conforme'), ('updated', 'Mis à jour'), ('unknown', 'Non inspecté')], default='unknown', max_length=50, null=True, verbose_name="Statut d'inspection")),
|
||||||
|
('last_preventive_date', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('last_corrective_date', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('last_survey_date', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('brand', models.CharField(blank=True, max_length=500, null=True)),
|
||||||
|
('marked_as_removed_date', models.DateTimeField(blank=True, null=True, verbose_name='Date de marquage comme retiré')),
|
||||||
|
('note', models.TextField(blank=True, null=True, verbose_name='Note')),
|
||||||
|
('validation_status', models.CharField(blank=True, choices=[('to_be_validated', 'À valider'), ('validated', 'Validé'), ('to_be_completed', 'À compléter'), ('rejected', 'Rejeté')], max_length=50, null=True, verbose_name='Statut de validation')),
|
||||||
|
('funding_program', models.CharField(blank=True, max_length=200, null=True, verbose_name='Programme de financement / Projet')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('lon', models.FloatField(blank=True, null=True)),
|
||||||
|
('lat', models.FloatField(blank=True, null=True)),
|
||||||
|
('geojson', models.TextField(blank=True, null=True)),
|
||||||
|
('geom', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812, verbose_name='Géométrie (Point)')),
|
||||||
|
('position', models.PositiveSmallIntegerField(default=1, help_text='Ordre de gauche à droite (1, 2, 3...)', verbose_name='Position sur la traverse')),
|
||||||
|
('bollard_type', models.CharField(choices=[('automatic', 'Automatique / Escamotable'), ('semi_automatic', 'Semi-automatique'), ('fixed', 'Fixe'), ('removable', 'Amovible')], default='automatic', max_length=50, verbose_name='Type de plot')),
|
||||||
|
('extra_data', models.JSONField(blank=True, default=dict, verbose_name='Métadonnées techniques')),
|
||||||
|
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.assetcategory')),
|
||||||
|
('location', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bollard_plots', to='assets.itslocation', verbose_name='Localisation ITS')),
|
||||||
|
('marked_as_removed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_marked_as_removed', to=settings.AUTH_USER_MODEL, verbose_name='Marqué comme retiré par')),
|
||||||
|
('model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='assets.itsassetmodel', verbose_name='Modèle de plot')),
|
||||||
|
('replaced_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replaces', to='assets.itsbollardplot', verbose_name='Remplacé par')),
|
||||||
|
('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Mis à jour par')),
|
||||||
|
('system', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='plots', to='assets.itsbollardsystem', verbose_name='Système de bornes')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Plot de borne',
|
||||||
|
'verbose_name_plural': 'Plots de bornes',
|
||||||
|
'ordering': ['system', 'position'],
|
||||||
|
'indexes': [models.Index(fields=['code'], name='assets_itsb_code_de6000_idx')],
|
||||||
|
},
|
||||||
|
bases=(assets.models.core.GeoSyncMixin, models.Model),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='itsbollardsystem',
|
||||||
|
index=models.Index(fields=['code'], name='assets_itsb_code_a4c148_idx'),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='itsbollardsystemcontract',
|
||||||
|
unique_together={('system', 'contract')},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -8,7 +8,7 @@ from .structures import Structure, StructureLocation, StructureGeoAsset, Structu
|
||||||
from .controlcenters import ControlCenter, ControlCenterLocation, ControlCenterGeoAsset, ControlCenterAssetModel, ControlCenterDocument, ControlCenterContract, ControlCenterGeoAssetContract, ControlCenterAssetDocument
|
from .controlcenters import ControlCenter, ControlCenterLocation, ControlCenterGeoAsset, ControlCenterAssetModel, ControlCenterDocument, ControlCenterContract, ControlCenterGeoAssetContract, ControlCenterAssetDocument
|
||||||
from .sign import SignStreet, SignStreetDocument, SignPole, SignPoleModel, SignPanel, SignPanelModel, SignPanelType, ComplementaryPoliceRegulation, SignParkingSpot
|
from .sign import SignStreet, SignStreetDocument, SignPole, SignPoleModel, SignPanel, SignPanelModel, SignPanelType, ComplementaryPoliceRegulation, SignParkingSpot
|
||||||
from .roads import RoadStreet, RoadStreetDocument, Roadway, RoadwayModel, RoadSidewalk, RoadSidewalkModel, RoadFurniture, RoadFurnitureModel, RoadwayFunction, RoadConcreteBlockSet, RoadDrainModel, RoadDrain
|
from .roads import RoadStreet, RoadStreetDocument, Roadway, RoadwayModel, RoadSidewalk, RoadSidewalkModel, RoadFurniture, RoadFurnitureModel, RoadwayFunction, RoadConcreteBlockSet, RoadDrainModel, RoadDrain
|
||||||
from .its import ITSLocation, ITSLocationDocument, ITSAssetModel, ITSGeoAsset, ITSGeoAssetContract, ITSLocationContract
|
from .its import ITSLocation, ITSLocationDocument, ITSAssetModel, ITSGeoAsset, ITSGeoAssetContract, ITSLocationContract, ITSBollardSystem, ITSBollardPlot, ITSBollardSystemAsset, ITSBollardSystemContract
|
||||||
from .clean import CleanLocation, CleanLitterBinModel, CleanLitterBin, CleanLitterBinPhoto, CleanLitterBinObservation, CleanGlassContainerModel, CleanGlassContainer, CleanLocationContract
|
from .clean import CleanLocation, CleanLitterBinModel, CleanLitterBin, CleanLitterBinPhoto, CleanLitterBinObservation, CleanGlassContainerModel, CleanGlassContainer, CleanLocationContract
|
||||||
from .buildings import PublicBuilding, BuildingAsset, BuildingEquipment, PublicBuildingDocument
|
from .buildings import PublicBuilding, BuildingAsset, BuildingEquipment, PublicBuildingDocument
|
||||||
from .outdoor import OutdoorLocation, OutdoorAsset, OutdoorLocationDocument
|
from .outdoor import OutdoorLocation, OutdoorAsset, OutdoorLocationDocument
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
from django.contrib.gis.db import models
|
from django.contrib.gis.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
from django.utils.translation import get_language
|
from django.utils.translation import get_language, gettext_lazy as _
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
@ -91,6 +91,7 @@ class ITSGeoAsset(AbstractGeoAsset):
|
||||||
location = models.ForeignKey(ITSLocation, related_name="assets", on_delete=models.SET_NULL, null=True, blank=True)
|
location = models.ForeignKey(ITSLocation, related_name="assets", on_delete=models.SET_NULL, null=True, blank=True)
|
||||||
trafficlights_pole = models.ForeignKey("assets.TrafficLightPole", on_delete=models.SET_NULL, null=True, blank=True, related_name="its_assets", verbose_name="Traffic Light Pole")
|
trafficlights_pole = models.ForeignKey("assets.TrafficLightPole", on_delete=models.SET_NULL, null=True, blank=True, related_name="its_assets", verbose_name="Traffic Light Pole")
|
||||||
lighting_pole = models.ForeignKey("assets.PublicLightingPole", on_delete=models.SET_NULL, null=True, blank=True, related_name="its_assets", verbose_name="Lighting Pole")
|
lighting_pole = models.ForeignKey("assets.PublicLightingPole", on_delete=models.SET_NULL, null=True, blank=True, related_name="its_assets", verbose_name="Lighting Pole")
|
||||||
|
extra_data = models.JSONField(default=dict, blank=True, verbose_name=_("Métadonnées techniques"))
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
@ -119,6 +120,147 @@ class ITSGeoAsset(AbstractGeoAsset):
|
||||||
return reverse('assets:its_assets_detail', kwargs={'asset_model': 'itsgeoasset', 'asset_id':self.id})
|
return reverse('assets:its_assets_detail', kwargs={'asset_model': 'itsgeoasset', 'asset_id':self.id})
|
||||||
|
|
||||||
|
|
||||||
|
BOLLARD_DIRECTION_CHOICES = (
|
||||||
|
('unidirectional', _('Sens unique')),
|
||||||
|
('bidirectional', _('Double sens')),
|
||||||
|
)
|
||||||
|
|
||||||
|
class ITSBollardSystem(AbstractGeoAsset):
|
||||||
|
"""
|
||||||
|
Système / Traverse de bornes escamotables (représentation linéaire).
|
||||||
|
Contrôle l'accès à une voie ou zone piétonne.
|
||||||
|
"""
|
||||||
|
geom = models.LineStringField(srid=3812, null=True, blank=True, verbose_name=_("Géométrie (Ligne)"))
|
||||||
|
model = models.ForeignKey(ITSAssetModel, on_delete=models.PROTECT, null=True, blank=True, verbose_name=_("Modèle de système"))
|
||||||
|
location = models.ForeignKey(ITSLocation, related_name="bollard_systems", on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_("Localisation ITS"))
|
||||||
|
width = models.FloatField(null=True, blank=True, verbose_name=_("Largeur de passage (m)"))
|
||||||
|
direction = models.CharField(max_length=50, choices=BOLLARD_DIRECTION_CHOICES, default='bidirectional', null=True, blank=True, verbose_name=_("Sens de circulation"))
|
||||||
|
extra_data = models.JSONField(default=dict, blank=True, verbose_name=_("Métadonnées techniques"))
|
||||||
|
associated_assets = models.ManyToManyField(
|
||||||
|
'assets.ITSGeoAsset',
|
||||||
|
through='assets.ITSBollardSystemAsset',
|
||||||
|
related_name="bollard_systems",
|
||||||
|
blank=True,
|
||||||
|
verbose_name=_("Équipements associés")
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Système de bornes")
|
||||||
|
verbose_name_plural = _("Systèmes de bornes")
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=["code"]),
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('assets:its_assets_detail', kwargs={'asset_model': 'itsbollardsystem', 'asset_id': self.id})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def plots_count(self):
|
||||||
|
return self.plots.count()
|
||||||
|
|
||||||
|
|
||||||
|
BOLLARD_TYPE_CHOICES = (
|
||||||
|
('automatic', _('Automatique / Escamotable')),
|
||||||
|
('semi_automatic', _('Semi-automatique')),
|
||||||
|
('fixed', _('Fixe')),
|
||||||
|
('removable', _('Amovible')),
|
||||||
|
)
|
||||||
|
|
||||||
|
class ITSBollardPlot(AbstractGeoAsset):
|
||||||
|
"""
|
||||||
|
Plot / borne physique individuel sur un système de bornes.
|
||||||
|
"""
|
||||||
|
geom = models.PointField(srid=3812, null=True, blank=True, verbose_name=_("Géométrie (Point)"))
|
||||||
|
system = models.ForeignKey(ITSBollardSystem, on_delete=models.CASCADE, related_name="plots", verbose_name=_("Système de bornes"))
|
||||||
|
model = models.ForeignKey(ITSAssetModel, on_delete=models.PROTECT, null=True, blank=True, verbose_name=_("Modèle de plot"))
|
||||||
|
position = models.PositiveSmallIntegerField(default=1, verbose_name=_("Position sur la traverse"), help_text=_("Ordre de gauche à droite (1, 2, 3...)"))
|
||||||
|
bollard_type = models.CharField(max_length=50, choices=BOLLARD_TYPE_CHOICES, default='automatic', verbose_name=_("Type de plot"))
|
||||||
|
location = models.ForeignKey(ITSLocation, related_name="bollard_plots", on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_("Localisation ITS"))
|
||||||
|
extra_data = models.JSONField(default=dict, blank=True, verbose_name=_("Métadonnées techniques"))
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
if not self.location_id and self.system and self.system.location_id:
|
||||||
|
self.location = self.system.location
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if not self.location_id and self.system and self.system.location_id:
|
||||||
|
self.location = self.system.location
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Plot de borne")
|
||||||
|
verbose_name_plural = _("Plots de bornes")
|
||||||
|
ordering = ["system", "position"]
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=["code"]),
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('assets:its_assets_detail', kwargs={'asset_model': 'itsbollardplot', 'asset_id': self.id})
|
||||||
|
|
||||||
|
|
||||||
|
class ITSBollardSystemAsset(models.Model):
|
||||||
|
"""
|
||||||
|
Table de liaison ManyToMany entre un ITSBollardSystem et des ITSGeoAsset
|
||||||
|
(caméras LAPI, totems, armoires électriques, feux bicolores, boucles...).
|
||||||
|
"""
|
||||||
|
class AssetRole(models.TextChoices):
|
||||||
|
CAMERA_ENTRY = "camera_entry", _("Caméra entrée (LAPI)")
|
||||||
|
CAMERA_EXIT = "camera_exit", _("Caméra sortie (LAPI)")
|
||||||
|
CAMERA_CCTV = "camera_cctv", _("Caméra de contexte / surveillance")
|
||||||
|
CABINET = "cabinet", _("Armoire électrique / automate")
|
||||||
|
TOTEM = "totem", _("Totem de commande")
|
||||||
|
TRAFFIC_LIGHT = "traffic_light", _("Feu de franchissement")
|
||||||
|
LOOP = "loop", _("Boucle de détection")
|
||||||
|
OTHER = "other", _("Autre")
|
||||||
|
|
||||||
|
bollard_system = models.ForeignKey(
|
||||||
|
ITSBollardSystem,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="system_assets",
|
||||||
|
verbose_name=_("Système de bornes")
|
||||||
|
)
|
||||||
|
asset = models.ForeignKey(
|
||||||
|
ITSGeoAsset,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="bollard_system_links",
|
||||||
|
verbose_name=_("Équipement ITS")
|
||||||
|
)
|
||||||
|
role = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
choices=AssetRole.choices,
|
||||||
|
default=AssetRole.OTHER,
|
||||||
|
verbose_name=_("Rôle de l'équipement")
|
||||||
|
)
|
||||||
|
comment = models.CharField(max_length=255, blank=True, null=True, verbose_name=_("Commentaire"))
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Date d'association"))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Équipement associé au système de bornes")
|
||||||
|
verbose_name_plural = _("Équipements associés au système de bornes")
|
||||||
|
unique_together = ('bollard_system', 'asset')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.bollard_system.code} - {self.asset.code} ({self.get_role_display()})"
|
||||||
|
|
||||||
|
|
||||||
|
class ITSBollardSystemContract(models.Model):
|
||||||
|
system = models.ForeignKey(ITSBollardSystem, on_delete=models.CASCADE, related_name="bollard_systems_contracts")
|
||||||
|
contract = models.ForeignKey("contracts.Contract", on_delete=models.CASCADE, related_name="bollard_systems_contracts")
|
||||||
|
status = models.CharField(max_length=50, choices=[('active', 'Actif'), ('inactive', 'Inactif'), ('archived', 'Archivé')], default='active')
|
||||||
|
maintenance_type = models.CharField(max_length=50, choices=[('equipments', 'Equipements'), ('office', 'Bureautique'), ('system', 'Système'), ('other', 'Autre')], default='other')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.system.code} - {self.contract.contract_number}"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Contrat de système de bornes")
|
||||||
|
verbose_name_plural = _("Contrats de systèmes de bornes")
|
||||||
|
unique_together = ('system', 'contract')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ITSLocationContract(models.Model):
|
class ITSLocationContract(models.Model):
|
||||||
location = models.ForeignKey(ITSLocation, on_delete=models.CASCADE, related_name="its_locations_contracts")
|
location = models.ForeignKey(ITSLocation, on_delete=models.CASCADE, related_name="its_locations_contracts")
|
||||||
|
|
|
||||||
|
|
@ -427,6 +427,33 @@
|
||||||
glyphCodepoint: 'f82b',
|
glyphCodepoint: 'f82b',
|
||||||
mapImage: 'icon-sign',
|
mapImage: 'icon-sign',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ITS
|
||||||
|
itsgeoasset: {
|
||||||
|
thematic: 'its',
|
||||||
|
color: '#0288d1',
|
||||||
|
iconUrl: null,
|
||||||
|
iconClass: 'bi-broadcast-pin',
|
||||||
|
glyphCodepoint: 'f1d5',
|
||||||
|
mapImage: 'icon-its',
|
||||||
|
},
|
||||||
|
itsbollardsystem: {
|
||||||
|
thematic: 'its',
|
||||||
|
color: '#0288d1',
|
||||||
|
iconUrl: null,
|
||||||
|
iconClass: 'bi-border-width',
|
||||||
|
glyphCodepoint: 'f1ca',
|
||||||
|
mapImage: 'icon-itsbollardsystem',
|
||||||
|
},
|
||||||
|
itsbollardplot: {
|
||||||
|
thematic: 'its',
|
||||||
|
color: '#0288d1',
|
||||||
|
iconUrl: '/static/assets/its_bollard_plot_icon.svg?v=20261008',
|
||||||
|
iconSize: 42,
|
||||||
|
iconClass: 'bi-cone-striped',
|
||||||
|
glyphCodepoint: 'f2aa',
|
||||||
|
mapImage: 'icon-itsbollardplot',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// ─── 3. Specific Parking Spot Overrides by spot_type ─────────────────────────
|
// ─── 3. Specific Parking Spot Overrides by spot_type ─────────────────────────
|
||||||
|
|
@ -633,6 +660,7 @@
|
||||||
natureplanter: injected.natureplanter || (ASSET_TYPE_OVERRIDES.natureplanter && ASSET_TYPE_OVERRIDES.natureplanter.iconUrl),
|
natureplanter: injected.natureplanter || (ASSET_TYPE_OVERRIDES.natureplanter && ASSET_TYPE_OVERRIDES.natureplanter.iconUrl),
|
||||||
artworkfountain: injected.artworkfountain || ASSET_TYPE_OVERRIDES.artworkfountain.iconUrl,
|
artworkfountain: injected.artworkfountain || ASSET_TYPE_OVERRIDES.artworkfountain.iconUrl,
|
||||||
artworkmonument: injected.artworkmonument || ASSET_TYPE_OVERRIDES.artworkmonument.iconUrl,
|
artworkmonument: injected.artworkmonument || ASSET_TYPE_OVERRIDES.artworkmonument.iconUrl,
|
||||||
|
itsbollardplot: injected.itsbollardplot || (ASSET_TYPE_OVERRIDES.itsbollardplot && ASSET_TYPE_OVERRIDES.itsbollardplot.iconUrl),
|
||||||
};
|
};
|
||||||
return urls;
|
return urls;
|
||||||
}
|
}
|
||||||
|
|
@ -656,6 +684,9 @@
|
||||||
{ name: 'icon-roaddrain', codepoint: 'f617', color: '#1e88e5' },
|
{ name: 'icon-roaddrain', codepoint: 'f617', color: '#1e88e5' },
|
||||||
{ name: 'icon-structures', codepoint: 'f876', color: '#6d4c41' },
|
{ name: 'icon-structures', codepoint: 'f876', color: '#6d4c41' },
|
||||||
{ name: 'icon-its', codepoint: 'f1d5', color: '#0288d1' },
|
{ name: 'icon-its', codepoint: 'f1d5', color: '#0288d1' },
|
||||||
|
{ name: 'icon-itsgeoasset', codepoint: 'f1d5', color: '#0288d1' },
|
||||||
|
{ name: 'icon-itsbollardsystem', codepoint: 'f1ca', color: '#0288d1' },
|
||||||
|
{ name: 'icon-itsbollardplot', codepoint: 'f2aa', color: '#0288d1' },
|
||||||
{ name: 'icon-clean', codepoint: 'f78a', color: '#5e5e5e' },
|
{ name: 'icon-clean', codepoint: 'f78a', color: '#5e5e5e' },
|
||||||
{ name: 'icon-cleanglasscontainer', codepoint: 'f78a', color: '#16a34a' },
|
{ name: 'icon-cleanglasscontainer', codepoint: 'f78a', color: '#16a34a' },
|
||||||
{ name: 'icon-artworks', codepoint: 'f4b0', color: '#f24141' },
|
{ name: 'icon-artworks', codepoint: 'f4b0', color: '#f24141' },
|
||||||
|
|
@ -749,6 +780,9 @@
|
||||||
['==', ['get', 'asset_type'], 'roadfurniture'], 'icon-speedbump',
|
['==', ['get', 'asset_type'], 'roadfurniture'], 'icon-speedbump',
|
||||||
['==', ['get', 'asset_type'], 'roadconcreteblockset'], 'icon-roadconcreteblockset',
|
['==', ['get', 'asset_type'], 'roadconcreteblockset'], 'icon-roadconcreteblockset',
|
||||||
['==', ['get', 'asset_type'], 'naturerwiasset'], 'icon-naturerwiasset',
|
['==', ['get', 'asset_type'], 'naturerwiasset'], 'icon-naturerwiasset',
|
||||||
|
['==', ['get', 'asset_type'], 'itsbollardplot'], 'icon-itsbollardplot',
|
||||||
|
['==', ['get', 'asset_type'], 'itsbollardsystem'], 'icon-itsbollardsystem',
|
||||||
|
['==', ['get', 'asset_type'], 'itsgeoasset'], 'icon-its',
|
||||||
['==', ['get', 'spot_type'], 'scooter_drop_zone'], 'icon-parkingspot-scooter_drop_zone',
|
['==', ['get', 'spot_type'], 'scooter_drop_zone'], 'icon-parkingspot-scooter_drop_zone',
|
||||||
['==', ['get', 'spot_type'], 'pmr'], 'icon-parkingspot-pmr',
|
['==', ['get', 'spot_type'], 'pmr'], 'icon-parkingspot-pmr',
|
||||||
['==', ['get', 'spot_type'], 'ev_charging'], 'icon-parkingspot-ev_charging',
|
['==', ['get', 'spot_type'], 'ev_charging'], 'icon-parkingspot-ev_charging',
|
||||||
|
|
@ -924,14 +958,20 @@
|
||||||
'#0288d1'
|
'#0288d1'
|
||||||
],
|
],
|
||||||
['==', ['get', 'thematic'], 'nature'], '#2e7d32',
|
['==', ['get', 'thematic'], 'nature'], '#2e7d32',
|
||||||
|
['==', ['get', 'asset_type'], 'itsbollardsystem'], '#0288d1',
|
||||||
['match', ['get', 'asset_type'], 'roadway', '#334155', 'sidewalk', '#64748b', getThematicColorExpr()]
|
['match', ['get', 'asset_type'], 'roadway', '#334155', 'sidewalk', '#64748b', getThematicColorExpr()]
|
||||||
],
|
],
|
||||||
'line-width': [
|
'line-width': [
|
||||||
'case',
|
'case',
|
||||||
['any', ['boolean', ['feature-state', 'highlighted'], false], ['boolean', ['feature-state', 'selected'], false]], 6,
|
['any', ['boolean', ['feature-state', 'highlighted'], false], ['boolean', ['feature-state', 'selected'], false]], 6,
|
||||||
|
['==', ['get', 'asset_type'], 'itsbollardsystem'], 5,
|
||||||
3.5
|
3.5
|
||||||
],
|
],
|
||||||
'line-opacity': 0.88,
|
'line-opacity': [
|
||||||
|
'case',
|
||||||
|
['==', ['get', 'asset_type'], 'itsbollardsystem'], 0.95,
|
||||||
|
0.88
|
||||||
|
],
|
||||||
},
|
},
|
||||||
minzoom: 8,
|
minzoom: 8,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
36
loko/assets/static/assets/its_bollard_plot_icon.svg
Normal file
36
loko/assets/static/assets/its_bollard_plot_icon.svg
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" aria-hidden="true">
|
||||||
|
<ellipse cx="32" cy="54" rx="22" ry="5" fill="#1e293b" opacity="0.3"/>
|
||||||
|
<ellipse cx="32" cy="52" rx="18" ry="4" fill="#334155"/>
|
||||||
|
<ellipse cx="32" cy="51" rx="16" ry="3.5" fill="#64748b"/>
|
||||||
|
<ellipse cx="32" cy="50.5" rx="13" ry="2.8" fill="#1e293b"/>
|
||||||
|
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bollardGrad" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||||
|
<stop offset="0%" stop-color="#0284c7"/>
|
||||||
|
<stop offset="25%" stop-color="#38bdf8"/>
|
||||||
|
<stop offset="60%" stop-color="#0284c7"/>
|
||||||
|
<stop offset="100%" stop-color="#0369a1"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="capGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#7dd3fc"/>
|
||||||
|
<stop offset="100%" stop-color="#0284c7"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="bandGrad" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||||
|
<stop offset="0%" stop-color="#f59e0b"/>
|
||||||
|
<stop offset="35%" stop-color="#fef08a"/>
|
||||||
|
<stop offset="70%" stop-color="#f59e0b"/>
|
||||||
|
<stop offset="100%" stop-color="#d97706"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<path d="M 21,24 L 21,50 C 21,53 43,53 43,50 L 43,24 Z" fill="url(#bollardGrad)"/>
|
||||||
|
<path d="M 21,21 L 21,27 C 21,29.5 43,29.5 43,27 L 43,21 C 43,23.5 21,23.5 21,21 Z" fill="url(#bandGrad)"/>
|
||||||
|
<path d="M 21,15 L 21,21 C 21,23.5 43,23.5 43,21 L 43,15 Z" fill="url(#bollardGrad)"/>
|
||||||
|
|
||||||
|
<ellipse cx="32" cy="15" rx="11" ry="3.5" fill="url(#capGrad)" stroke="#bae6fd" stroke-width="0.8"/>
|
||||||
|
<ellipse cx="32" cy="14.2" rx="7" ry="2" fill="#ffffff" opacity="0.6"/>
|
||||||
|
|
||||||
|
<circle cx="26" cy="24.5" r="1" fill="#ffffff"/>
|
||||||
|
<circle cx="32" cy="25.2" r="1.1" fill="#ffffff"/>
|
||||||
|
<circle cx="38" cy="24.5" r="1" fill="#ffffff"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
|
|
@ -535,7 +535,7 @@ function buildColumns() {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'element_type', title: gettext('Type'), sortable: true, width: 130, widthUnit: 'px', valign: 'middle',
|
field: 'element_type', title: gettext('Type'), sortable: true, width: 145, widthUnit: 'px', valign: 'middle',
|
||||||
formatter: function (v, row) {
|
formatter: function (v, row) {
|
||||||
const table = $('#unified-table');
|
const table = $('#unified-table');
|
||||||
let isCatVisible = false;
|
let isCatVisible = false;
|
||||||
|
|
@ -543,10 +543,21 @@ function buildColumns() {
|
||||||
isCatVisible = table.length && typeof table.bootstrapTable === 'function' &&
|
isCatVisible = table.length && typeof table.bootstrapTable === 'function' &&
|
||||||
table.bootstrapTable('getVisibleColumns').some(c => c.field === 'category');
|
table.bootstrapTable('getVisibleColumns').some(c => c.field === 'category');
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
if (!isCatVisible && row && row.is_location && row.category) {
|
|
||||||
return `${escapeHtml(v || '-')} <span class="badge bg-light text-secondary border fw-normal ms-1" style="font-size:0.75rem;">${escapeHtml(row.category)}</span>`;
|
let iconPrefix = '';
|
||||||
|
if (typeof AssetSymbology !== 'undefined' && row && !row.is_location && !row.is_management_zone) {
|
||||||
|
const symb = AssetSymbology.getAssetSymbology(row);
|
||||||
|
const colorStyle = symb.color ? `color:${symb.color};` : '';
|
||||||
|
iconPrefix = AssetSymbology.renderIconHtml(symb, {
|
||||||
|
style: `${colorStyle} font-size: 0.95rem; vertical-align: -1px; margin-right: 5px;`,
|
||||||
|
imgClass: 'me-1'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return escapeHtml(v || '-');
|
|
||||||
|
if (!isCatVisible && row && row.is_location && row.category) {
|
||||||
|
return `${iconPrefix}${escapeHtml(v || '-')} <span class="badge bg-light text-secondary border fw-normal ms-1" style="font-size:0.75rem;">${escapeHtml(row.category)}</span>`;
|
||||||
|
}
|
||||||
|
return `${iconPrefix}${escapeHtml(v || '-')}`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ field: 'thematic_name', title: gettext('Thématique'), sortable: true, width: 140, widthUnit: 'px', valign: 'middle' },
|
{ field: 'thematic_name', title: gettext('Thématique'), sortable: true, width: 140, widthUnit: 'px', valign: 'middle' },
|
||||||
|
|
@ -2016,6 +2027,7 @@ function collectClickedAssets(clickEvent) {
|
||||||
element_type: p.element_type || '',
|
element_type: p.element_type || '',
|
||||||
detail_url: p.detail_url || null,
|
detail_url: p.detail_url || null,
|
||||||
feature_id: id,
|
feature_id: id,
|
||||||
|
extra_info: p.plots_count != null ? `${p.plots_count} plot(s)` : (p.bollard_type || ''),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -2149,15 +2161,26 @@ function showAssetClickPopup(clickEvent) {
|
||||||
const type = escapeHtml(a.element_type || a.asset_type || '');
|
const type = escapeHtml(a.element_type || a.asset_type || '');
|
||||||
const thematic = escapeHtml(a.thematic_name || a.thematic || '');
|
const thematic = escapeHtml(a.thematic_name || a.thematic || '');
|
||||||
const name = escapeHtml(a.name || '');
|
const name = escapeHtml(a.name || '');
|
||||||
|
const extra = a.extra_info ? ` • <span class="badge bg-light text-primary border" style="font-size:0.7rem;">${escapeHtml(a.extra_info)}</span>` : '';
|
||||||
const detailAnchor = a.detail_url
|
const detailAnchor = a.detail_url
|
||||||
? `<a href="${escapeHtml(a.detail_url)}">${code}</a>`
|
? `<a href="${escapeHtml(a.detail_url)}">${code}</a>`
|
||||||
: `<span>${code}</span>`;
|
: `<span>${code}</span>`;
|
||||||
|
|
||||||
|
let iconPrefix = '';
|
||||||
|
if (typeof AssetSymbology !== 'undefined') {
|
||||||
|
const symb = AssetSymbology.getAssetSymbology(a);
|
||||||
|
const colorStyle = symb.color ? `color:${symb.color};` : '';
|
||||||
|
iconPrefix = AssetSymbology.renderIconHtml(symb, {
|
||||||
|
style: `${colorStyle} font-size: 1rem; vertical-align: -1px; margin-right: 5px;`,
|
||||||
|
imgClass: 'me-1'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="asset-popup-item" style="display:flex; gap:0.5rem; align-items:flex-start; justify-content:space-between; padding:0.25rem 0; border-bottom:1px solid #f1f3f5;">
|
<div class="asset-popup-item" style="display:flex; gap:0.5rem; align-items:flex-start; justify-content:space-between; padding:0.25rem 0; border-bottom:1px solid #f1f3f5;">
|
||||||
<div style="min-width:0;">
|
<div style="min-width:0;">
|
||||||
<div style="font-weight:600; line-height:1.1;">${detailAnchor}</div>
|
<div style="font-weight:600; line-height:1.1;">${iconPrefix}${detailAnchor}</div>
|
||||||
<div style="font-size:0.75rem; color:#6c757d; line-height:1.2;">${type}${thematic ? ' - ' + thematic : ''}</div>
|
<div style="font-size:0.75rem; color:#6c757d; line-height:1.2;">${type}${thematic ? ' - ' + thematic : ''}${extra}</div>
|
||||||
${name ? `<div style="font-size:0.75rem; color:#495057; line-height:1.2;">${name}</div>` : ''}
|
${name ? `<div style="font-size:0.75rem; color:#495057; line-height:1.2;">${name}</div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
<button type="button"
|
<button type="button"
|
||||||
|
|
|
||||||
|
|
@ -201,6 +201,12 @@ function loadUnifiedMapIcons(map) {
|
||||||
.then(data => { if (!map.hasImage('icon-artworkmonument')) map.addImage('icon-artworkmonument', data); })
|
.then(data => { if (!map.hasImage('icon-artworkmonument')) map.addImage('icon-artworkmonument', data); })
|
||||||
.catch(e => console.warn('unified_map_layers: could not load artworkmonument icon', e));
|
.catch(e => console.warn('unified_map_layers: could not load artworkmonument icon', e));
|
||||||
}
|
}
|
||||||
|
// itsbollardplot: dedicated automatic bollard / plot SVG (42 px)
|
||||||
|
if (iconUrls.itsbollardplot && !map.hasImage('icon-itsbollardplot')) {
|
||||||
|
loadNormalisedIcon(iconUrls.itsbollardplot, 42)
|
||||||
|
.then(data => { if (!map.hasImage('icon-itsbollardplot')) map.addImage('icon-itsbollardplot', data); })
|
||||||
|
.catch(e => console.warn('unified_map_layers: could not load itsbollardplot icon', e));
|
||||||
|
}
|
||||||
// If the SVG above is unavailable, publiclighting falls back to Bootstrap glyph.
|
// If the SVG above is unavailable, publiclighting falls back to Bootstrap glyph.
|
||||||
|
|
||||||
// ── Bootstrap Icons glyph SDF icons (recoloured by icon-color) ──────────
|
// ── Bootstrap Icons glyph SDF icons (recoloured by icon-color) ──────────
|
||||||
|
|
@ -634,6 +640,7 @@ const _unified_assets_symbol = {
|
||||||
['==', ['get', 'asset_type'], 'publiclightingelectricalcabinet'],
|
['==', ['get', 'asset_type'], 'publiclightingelectricalcabinet'],
|
||||||
['==', ['get', 'trafficlightelectricalcabinet'], 'trafficlightelectricalcabinet'],
|
['==', ['get', 'trafficlightelectricalcabinet'], 'trafficlightelectricalcabinet'],
|
||||||
['==', ['get', 'asset_type'], 'naturerwiasset'],
|
['==', ['get', 'asset_type'], 'naturerwiasset'],
|
||||||
|
['==', ['get', 'asset_type'], 'itsbollardplot'],
|
||||||
['==', ['get', 'category'], 'Avaloir'],
|
['==', ['get', 'category'], 'Avaloir'],
|
||||||
['==', ['get', 'category'], 'Corbeille de rue'],
|
['==', ['get', 'category'], 'Corbeille de rue'],
|
||||||
['==', ['get', 'category'], 'Corbeille'],
|
['==', ['get', 'category'], 'Corbeille'],
|
||||||
|
|
@ -669,6 +676,7 @@ const _unified_assets_symbol = {
|
||||||
['==', ['get', 'asset_type'], 'publiclightingelectricalcabinet'],
|
['==', ['get', 'asset_type'], 'publiclightingelectricalcabinet'],
|
||||||
['==', ['get', 'trafficlightelectricalcabinet'], 'trafficlightelectricalcabinet'],
|
['==', ['get', 'trafficlightelectricalcabinet'], 'trafficlightelectricalcabinet'],
|
||||||
['==', ['get', 'asset_type'], 'naturerwiasset'],
|
['==', ['get', 'asset_type'], 'naturerwiasset'],
|
||||||
|
['==', ['get', 'asset_type'], 'itsbollardplot'],
|
||||||
['==', ['get', 'category'], 'Avaloir'],
|
['==', ['get', 'category'], 'Avaloir'],
|
||||||
['==', ['get', 'category'], 'Corbeille de rue'],
|
['==', ['get', 'category'], 'Corbeille de rue'],
|
||||||
['==', ['get', 'category'], 'Corbeille'],
|
['==', ['get', 'category'], 'Corbeille'],
|
||||||
|
|
@ -704,6 +712,7 @@ const _unified_assets_symbol = {
|
||||||
['==', ['get', 'asset_type'], 'publiclightingelectricalcabinet'],
|
['==', ['get', 'asset_type'], 'publiclightingelectricalcabinet'],
|
||||||
['==', ['get', 'asset_type'], 'trafficlightelectricalcabinet'],
|
['==', ['get', 'asset_type'], 'trafficlightelectricalcabinet'],
|
||||||
['==', ['get', 'asset_type'], 'naturerwiasset'],
|
['==', ['get', 'asset_type'], 'naturerwiasset'],
|
||||||
|
['==', ['get', 'asset_type'], 'itsbollardplot'],
|
||||||
['==', ['get', 'category'], 'Avaloir'],
|
['==', ['get', 'category'], 'Avaloir'],
|
||||||
['==', ['get', 'category'], 'Corbeille de rue'],
|
['==', ['get', 'category'], 'Corbeille de rue'],
|
||||||
['==', ['get', 'category'], 'Corbeille'],
|
['==', ['get', 'category'], 'Corbeille'],
|
||||||
|
|
@ -739,6 +748,7 @@ const _unified_assets_symbol = {
|
||||||
['==', ['get', 'asset_type'], 'publiclightingelectricalcabinet'],
|
['==', ['get', 'asset_type'], 'publiclightingelectricalcabinet'],
|
||||||
['==', ['get', 'asset_type'], 'trafficlightelectricalcabinet'],
|
['==', ['get', 'asset_type'], 'trafficlightelectricalcabinet'],
|
||||||
['==', ['get', 'asset_type'], 'naturerwiasset'],
|
['==', ['get', 'asset_type'], 'naturerwiasset'],
|
||||||
|
['==', ['get', 'asset_type'], 'itsbollardplot'],
|
||||||
['==', ['get', 'category'], 'Avaloir'],
|
['==', ['get', 'category'], 'Avaloir'],
|
||||||
['==', ['get', 'category'], 'Corbeille de rue'],
|
['==', ['get', 'category'], 'Corbeille de rue'],
|
||||||
['==', ['get', 'category'], 'Corbeille'],
|
['==', ['get', 'category'], 'Corbeille'],
|
||||||
|
|
@ -774,6 +784,7 @@ const _unified_assets_symbol = {
|
||||||
['==', ['get', 'asset_type'], 'publiclightingelectricalcabinet'],
|
['==', ['get', 'asset_type'], 'publiclightingelectricalcabinet'],
|
||||||
['==', ['get', 'asset_type'], 'trafficlightelectricalcabinet'],
|
['==', ['get', 'asset_type'], 'trafficlightelectricalcabinet'],
|
||||||
['==', ['get', 'asset_type'], 'naturerwiasset'],
|
['==', ['get', 'asset_type'], 'naturerwiasset'],
|
||||||
|
['==', ['get', 'asset_type'], 'itsbollardplot'],
|
||||||
['==', ['get', 'category'], 'Avaloir'],
|
['==', ['get', 'category'], 'Avaloir'],
|
||||||
['==', ['get', 'category'], 'Corbeille de rue'],
|
['==', ['get', 'category'], 'Corbeille de rue'],
|
||||||
['==', ['get', 'category'], 'Corbeille'],
|
['==', ['get', 'category'], 'Corbeille'],
|
||||||
|
|
@ -809,6 +820,7 @@ const _unified_assets_symbol = {
|
||||||
['==', ['get', 'asset_type'], 'publiclightingelectricalcabinet'],
|
['==', ['get', 'asset_type'], 'publiclightingelectricalcabinet'],
|
||||||
['==', ['get', 'asset_type'], 'trafficlightelectricalcabinet'],
|
['==', ['get', 'asset_type'], 'trafficlightelectricalcabinet'],
|
||||||
['==', ['get', 'asset_type'], 'naturerwiasset'],
|
['==', ['get', 'asset_type'], 'naturerwiasset'],
|
||||||
|
['==', ['get', 'asset_type'], 'itsbollardplot'],
|
||||||
['==', ['get', 'category'], 'Avaloir'],
|
['==', ['get', 'category'], 'Avaloir'],
|
||||||
['==', ['get', 'category'], 'Corbeille de rue'],
|
['==', ['get', 'category'], 'Corbeille de rue'],
|
||||||
['==', ['get', 'category'], 'Corbeille'],
|
['==', ['get', 'category'], 'Corbeille'],
|
||||||
|
|
@ -844,6 +856,7 @@ const _unified_assets_symbol = {
|
||||||
['==', ['get', 'asset_type'], 'publiclightingelectricalcabinet'],
|
['==', ['get', 'asset_type'], 'publiclightingelectricalcabinet'],
|
||||||
['==', ['get', 'asset_type'], 'trafficlightelectricalcabinet'],
|
['==', ['get', 'asset_type'], 'trafficlightelectricalcabinet'],
|
||||||
['==', ['get', 'asset_type'], 'naturerwiasset'],
|
['==', ['get', 'asset_type'], 'naturerwiasset'],
|
||||||
|
['==', ['get', 'asset_type'], 'itsbollardplot'],
|
||||||
['==', ['get', 'category'], 'Avaloir'],
|
['==', ['get', 'category'], 'Avaloir'],
|
||||||
['==', ['get', 'category'], 'Corbeille de rue'],
|
['==', ['get', 'category'], 'Corbeille de rue'],
|
||||||
['==', ['get', 'category'], 'Corbeille'],
|
['==', ['get', 'category'], 'Corbeille'],
|
||||||
|
|
@ -876,6 +889,7 @@ const _unified_assets_symbol = {
|
||||||
['==', ['get', 'thematic'], 'publiclighting'], 'bottom',
|
['==', ['get', 'thematic'], 'publiclighting'], 'bottom',
|
||||||
['==', ['get', 'thematic'], 'trafficlights'], 'bottom',
|
['==', ['get', 'thematic'], 'trafficlights'], 'bottom',
|
||||||
['==', ['get', 'thematic'], 'nature'], 'bottom',
|
['==', ['get', 'thematic'], 'nature'], 'bottom',
|
||||||
|
['==', ['get', 'asset_type'], 'itsbollardplot'], 'bottom',
|
||||||
'center'
|
'center'
|
||||||
],
|
],
|
||||||
'icon-offset': [
|
'icon-offset': [
|
||||||
|
|
@ -912,6 +926,8 @@ const _unified_assets_symbol = {
|
||||||
['all', ['==', ['get', 'thematic'], 'roads'], ['match', ['geometry-type'], ['Polygon', 'MultiPolygon', 'LineString', 'MultiLineString'], true, false]], 0,
|
['all', ['==', ['get', 'thematic'], 'roads'], ['match', ['geometry-type'], ['Polygon', 'MultiPolygon', 'LineString', 'MultiLineString'], true, false]], 0,
|
||||||
['all', ['==', ['get', 'asset_type'], 'naturerwiasset'], ['match', ['geometry-type'], ['Polygon', 'MultiPolygon', 'LineString', 'MultiLineString'], true, false]], 0,
|
['all', ['==', ['get', 'asset_type'], 'naturerwiasset'], ['match', ['geometry-type'], ['Polygon', 'MultiPolygon', 'LineString', 'MultiLineString'], true, false]], 0,
|
||||||
['==', ['get', 'asset_type'], 'naturegreensurface'], 0,
|
['==', ['get', 'asset_type'], 'naturegreensurface'], 0,
|
||||||
|
['==', ['get', 'asset_type'], 'itsbollardsystem'], 0,
|
||||||
|
['match', ['geometry-type'], ['LineString', 'MultiLineString'], true, false], 0,
|
||||||
['boolean', ['feature-state', 'highlighted'], false], 1,
|
['boolean', ['feature-state', 'highlighted'], false], 1,
|
||||||
0.85
|
0.85
|
||||||
],
|
],
|
||||||
|
|
@ -1084,14 +1100,20 @@ const _unified_assets_line = (typeof AssetSymbology !== 'undefined' && AssetSymb
|
||||||
'line-color': [
|
'line-color': [
|
||||||
'case',
|
'case',
|
||||||
['==', ['get', 'thematic'], 'nature'], '#2e7d32',
|
['==', ['get', 'thematic'], 'nature'], '#2e7d32',
|
||||||
|
['==', ['get', 'asset_type'], 'itsbollardsystem'], '#0288d1',
|
||||||
THEMATIC_COLOR_EXPR
|
THEMATIC_COLOR_EXPR
|
||||||
],
|
],
|
||||||
'line-width': [
|
'line-width': [
|
||||||
'case',
|
'case',
|
||||||
['boolean', ['feature-state', 'highlighted'], false], 5,
|
['boolean', ['feature-state', 'highlighted'], false], 6,
|
||||||
|
['==', ['get', 'asset_type'], 'itsbollardsystem'], 5,
|
||||||
3
|
3
|
||||||
],
|
],
|
||||||
'line-opacity': 0.8,
|
'line-opacity': [
|
||||||
|
'case',
|
||||||
|
['==', ['get', 'asset_type'], 'itsbollardsystem'], 0.95,
|
||||||
|
0.8
|
||||||
|
],
|
||||||
},
|
},
|
||||||
minzoom: 8,
|
minzoom: 8,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,11 @@
|
||||||
id: 'assets',
|
id: 'assets',
|
||||||
type: 'circle',
|
type: 'circle',
|
||||||
source: 'assets',
|
source: 'assets',
|
||||||
|
filter: [
|
||||||
|
'any',
|
||||||
|
['==', ['geometry-type'], 'Point'],
|
||||||
|
['==', ['geometry-type'], 'MultiPoint']
|
||||||
|
],
|
||||||
paint: {
|
paint: {
|
||||||
'circle-radius': [
|
'circle-radius': [
|
||||||
'interpolate',
|
'interpolate',
|
||||||
|
|
@ -135,7 +140,32 @@
|
||||||
minzoom: 8
|
minzoom: 8
|
||||||
};
|
};
|
||||||
|
|
||||||
const assets_layers_defs = [assets_layer_def];
|
const assets_lines_layer_def = {
|
||||||
|
id: 'assets-lines',
|
||||||
|
type: 'line',
|
||||||
|
source: 'assets',
|
||||||
|
filter: [
|
||||||
|
'any',
|
||||||
|
['==', ['geometry-type'], 'LineString'],
|
||||||
|
['==', ['geometry-type'], 'MultiLineString']
|
||||||
|
],
|
||||||
|
paint: {
|
||||||
|
'line-color': [
|
||||||
|
'case',
|
||||||
|
['boolean', ['feature-state', 'highlighted'], false], '#ffeb3b',
|
||||||
|
'#0022ff'
|
||||||
|
],
|
||||||
|
'line-width': [
|
||||||
|
'case',
|
||||||
|
['boolean', ['feature-state', 'highlighted'], false], 6,
|
||||||
|
4
|
||||||
|
],
|
||||||
|
'line-opacity': 0.85
|
||||||
|
},
|
||||||
|
minzoom: 8
|
||||||
|
};
|
||||||
|
|
||||||
|
const assets_layers_defs = [assets_lines_layer_def, assets_layer_def];
|
||||||
|
|
||||||
|
|
||||||
// Filtrer les modèles par catégorie dans le formulaire des assets
|
// Filtrer les modèles par catégorie dans le formulaire des assets
|
||||||
|
|
|
||||||
92
loko/assets/templates/its/detail_itsbollardplot.html
Normal file
92
loko/assets/templates/its/detail_itsbollardplot.html
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
{% extends "assets/detail_generic.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block extra_blocks %}
|
||||||
|
<!-- Système de bornes parent -->
|
||||||
|
<div class="card mb-3" id="bollard-plot-system-card">
|
||||||
|
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="bi bi-diagram-3-fill text-primary me-2"></i>{% translate "Système de bornes de rattachement" %}
|
||||||
|
</h5>
|
||||||
|
{% if asset.system %}
|
||||||
|
<a href="{{ asset.system.get_absolute_url }}" class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="bi bi-arrow-right-circle me-1"></i>{% translate "Voir le système complet" %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if asset.system %}
|
||||||
|
<div class="row align-items-center mb-3">
|
||||||
|
<div class="col-md-7">
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<span class="badge bg-dark rounded-pill fs-6 me-2">#{{ asset.position }}</span>
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<a href="{{ asset.system.get_absolute_url }}" class="text-decoration-none">
|
||||||
|
{{ asset.system.code }} - {{ asset.system.get_name|default:asset.system.code }}
|
||||||
|
</a>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="text-muted small mb-2">
|
||||||
|
<i class="bi bi-geo-alt me-1"></i>{{ asset.system.location|default:"Localisation non définie" }}
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
{% if asset.system.width %}
|
||||||
|
<span class="badge bg-light text-dark border">
|
||||||
|
<i class="bi bi-arrows-expand me-1"></i>{% translate "Largeur" %} : {{ asset.system.width }} m
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if asset.system.direction %}
|
||||||
|
<span class="badge bg-light text-dark border">
|
||||||
|
<i class="bi bi-signpost-split me-1"></i>{{ asset.system.get_direction_display }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if asset.system.model %}
|
||||||
|
<span class="badge bg-light text-dark border">
|
||||||
|
<i class="bi bi-tag me-1"></i>{{ asset.system.model.get_name }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-5 text-md-end mt-3 mt-md-0">
|
||||||
|
<div class="p-3 bg-light rounded border text-start">
|
||||||
|
<div class="small text-muted mb-1">{% translate "Traverse complète" %} ({{ asset.system.plots.count }} {% translate "plots" %}) :</div>
|
||||||
|
<div class="d-flex flex-wrap gap-1">
|
||||||
|
{% for sibling in asset.system.plots.all %}
|
||||||
|
<a href="{{ sibling.get_absolute_url }}"
|
||||||
|
class="btn btn-sm {% if sibling.id == asset.id %}btn-primary fw-bold{% else %}btn-outline-secondary{% endif %}"
|
||||||
|
title="{{ sibling.code }} - {{ sibling.get_bollard_type_display }}">
|
||||||
|
#{{ sibling.position }} ({{ sibling.code }})
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-muted text-center p-3">
|
||||||
|
<i class="bi bi-exclamation-triangle text-warning me-2"></i>{% translate "Ce plot n'est actuellement rattaché à aucun système de bornes." %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Métadonnées techniques (extra_data) -->
|
||||||
|
{% if asset.extra_data %}
|
||||||
|
<div class="card mb-3" id="bollard-plot-extra-data-card">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="bi bi-code-slash text-secondary me-2"></i>{% translate "Métadonnées techniques" %}
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<dl class="row mb-0">
|
||||||
|
{% for key, val in asset.extra_data.items %}
|
||||||
|
<dt class="col-sm-4 text-truncate" title="{{ key }}">{{ key }}</dt>
|
||||||
|
<dd class="col-sm-8 mb-2"><code>{{ val }}</code></dd>
|
||||||
|
{% endfor %}
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock extra_blocks %}
|
||||||
218
loko/assets/templates/its/detail_itsbollardsystem.html
Normal file
218
loko/assets/templates/its/detail_itsbollardsystem.html
Normal file
|
|
@ -0,0 +1,218 @@
|
||||||
|
{% extends "assets/detail_generic.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block extra_blocks %}
|
||||||
|
<!-- Plots de la traverse -->
|
||||||
|
<div class="card mb-3" id="bollard-system-plots-card">
|
||||||
|
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="bi bi-geo-alt-fill text-primary me-2"></i>{% translate "Plots de la traverse" %}
|
||||||
|
<span class="badge bg-primary ms-2">{{ asset.plots.count }}</span>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
{% if asset.plots.all %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover table-striped mb-0 align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 80px;" class="text-center">{% translate "Pos." %}</th>
|
||||||
|
<th>{% translate "Code" %}</th>
|
||||||
|
<th>{% translate "Type de borne" %}</th>
|
||||||
|
<th>{% translate "Modèle" %}</th>
|
||||||
|
<th>{% translate "N° de série" %}</th>
|
||||||
|
<th>{% translate "Statut" %}</th>
|
||||||
|
<th style="width: 110px;" class="text-end">{% translate "Action" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for plot in asset.plots.all %}
|
||||||
|
<tr>
|
||||||
|
<td class="text-center">
|
||||||
|
<span class="badge bg-dark rounded-pill">#{{ plot.position }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="{{ plot.get_absolute_url }}" class="fw-bold text-decoration-none">
|
||||||
|
{{ plot.code }}
|
||||||
|
</a>
|
||||||
|
{% if plot.get_name %}
|
||||||
|
<div class="small text-muted">{{ plot.get_name }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if plot.bollard_type == 'automatic' %}
|
||||||
|
<span class="badge bg-primary"><i class="bi bi-arrow-up-down me-1"></i>{{ plot.get_bollard_type_display }}</span>
|
||||||
|
{% elif plot.bollard_type == 'semi_automatic' %}
|
||||||
|
<span class="badge bg-info text-dark">{{ plot.get_bollard_type_display }}</span>
|
||||||
|
{% elif plot.bollard_type == 'removable' %}
|
||||||
|
<span class="badge bg-warning text-dark">{{ plot.get_bollard_type_display }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">{{ plot.get_bollard_type_display }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ plot.model.get_name|default:"-" }}</td>
|
||||||
|
<td><code>{{ plot.serial_number|default:"-" }}</code></td>
|
||||||
|
<td>
|
||||||
|
{% if plot.status == 'active' %}
|
||||||
|
<span class="badge bg-success">{{ plot.get_status_display }}</span>
|
||||||
|
{% elif plot.status == 'damaged' %}
|
||||||
|
<span class="badge bg-warning text-dark">{{ plot.get_status_display }}</span>
|
||||||
|
{% elif plot.status == 'to_replace' %}
|
||||||
|
<span class="badge bg-danger">{{ plot.get_status_display }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">{{ plot.get_status_display }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<a href="{{ plot.get_absolute_url }}" class="btn btn-sm btn-outline-primary" title="{% translate 'Voir le détail du plot' %}">
|
||||||
|
<i class="bi bi-eye me-1"></i>{% translate "Voir" %}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="p-4 text-center text-muted">
|
||||||
|
<i class="bi bi-cone-striped display-6 d-block mb-2 text-secondary opacity-50"></i>
|
||||||
|
{% translate "Aucun plot physique n'est encore configuré sur ce système de bornes." %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Équipements associés au système (Caméras LAPI, totems, armoires...) -->
|
||||||
|
<div class="card mb-3" id="bollard-system-assets-card">
|
||||||
|
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="bi bi-hdd-network text-info me-2"></i>{% translate "Équipements associés" %}
|
||||||
|
<span class="badge bg-secondary ms-2">{{ asset.system_assets.count }}</span>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
{% if asset.system_assets.all %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover table-striped mb-0 align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{% translate "Rôle" %}</th>
|
||||||
|
<th>{% translate "Équipement" %}</th>
|
||||||
|
<th>{% translate "Modèle" %}</th>
|
||||||
|
<th>{% translate "Statut" %}</th>
|
||||||
|
<th>{% translate "Commentaire" %}</th>
|
||||||
|
<th style="width: 110px;" class="text-end">{% translate "Action" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for item in asset.system_assets.all %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{% if 'camera' in item.role %}
|
||||||
|
<span class="badge bg-info text-dark"><i class="bi bi-camera-video me-1"></i>{{ item.get_role_display }}</span>
|
||||||
|
{% elif item.role == 'cabinet' %}
|
||||||
|
<span class="badge bg-dark"><i class="bi bi-cpu me-1"></i>{{ item.get_role_display }}</span>
|
||||||
|
{% elif item.role == 'totem' %}
|
||||||
|
<span class="badge bg-primary"><i class="bi bi-tablet me-1"></i>{{ item.get_role_display }}</span>
|
||||||
|
{% elif item.role == 'traffic_light' %}
|
||||||
|
<span class="badge bg-warning text-dark"><i class="bi bi-traffic-light me-1"></i>{{ item.get_role_display }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">{{ item.get_role_display }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="{{ item.asset.get_absolute_url }}" class="fw-bold text-decoration-none">
|
||||||
|
{{ item.asset.code }}
|
||||||
|
</a>
|
||||||
|
{% if item.asset.get_name %}
|
||||||
|
<div class="small text-muted">{{ item.asset.get_name }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ item.asset.model.get_name|default:"-" }}</td>
|
||||||
|
<td>
|
||||||
|
{% if item.asset.status == 'active' %}
|
||||||
|
<span class="badge bg-success">{{ item.asset.get_status_display }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">{{ item.asset.get_status_display }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ item.comment|default:"-" }}</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<a href="{{ item.asset.get_absolute_url }}" class="btn btn-sm btn-outline-secondary" title="{% translate 'Voir équipement' %}">
|
||||||
|
<i class="bi bi-box-arrow-up-right"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="p-4 text-center text-muted">
|
||||||
|
<i class="bi bi-camera-video display-6 d-block mb-2 text-secondary opacity-50"></i>
|
||||||
|
{% translate "Aucun équipement périphérique (caméra, armoire, totem...) n'est actuellement associé." %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Contrats de maintenance liés -->
|
||||||
|
{% if asset.bollard_systems_contracts.exists %}
|
||||||
|
<div class="card mb-3" id="bollard-system-contracts-card">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="bi bi-file-earmark-text text-secondary me-2"></i>{% translate "Contrats d'entretien" %}
|
||||||
|
<span class="badge bg-secondary ms-2">{{ asset.bollard_systems_contracts.count }}</span>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover table-striped mb-0 align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{% translate "Numéro de contrat" %}</th>
|
||||||
|
<th>{% translate "Type de maintenance" %}</th>
|
||||||
|
<th>{% translate "Statut" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for sc in asset.bollard_systems_contracts.all %}
|
||||||
|
<tr>
|
||||||
|
<td class="fw-bold">{{ sc.contract.contract_number }} - {{ sc.contract.name }}</td>
|
||||||
|
<td>{{ sc.get_maintenance_type_display }}</td>
|
||||||
|
<td>
|
||||||
|
{% if sc.status == 'active' %}
|
||||||
|
<span class="badge bg-success">{% translate "Actif" %}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">{{ sc.get_status_display }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Métadonnées techniques (extra_data) -->
|
||||||
|
{% if asset.extra_data %}
|
||||||
|
<div class="card mb-3" id="bollard-system-extra-data-card">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="bi bi-code-slash text-secondary me-2"></i>{% translate "Métadonnées techniques" %}
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<dl class="row mb-0">
|
||||||
|
{% for key, val in asset.extra_data.items %}
|
||||||
|
<dt class="col-sm-4 text-truncate" title="{{ key }}">{{ key }}</dt>
|
||||||
|
<dd class="col-sm-8 mb-2"><code>{{ val }}</code></dd>
|
||||||
|
{% endfor %}
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock extra_blocks %}
|
||||||
|
|
@ -103,11 +103,11 @@ def get_asset_specific_fields(asset):
|
||||||
from django.contrib.gis.db.models import GeometryField
|
from django.contrib.gis.db.models import GeometryField
|
||||||
from django.db.models import (ForeignKey, OneToOneField, ManyToManyField, AutoField,
|
from django.db.models import (ForeignKey, OneToOneField, ManyToManyField, AutoField,
|
||||||
BooleanField, FloatField, DecimalField, IntegerField,
|
BooleanField, FloatField, DecimalField, IntegerField,
|
||||||
DateField, DateTimeField, TextField)
|
DateField, DateTimeField, TextField, JSONField)
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from assets.models import AbstractAsset, AbstractGeoAsset
|
from assets.models import AbstractAsset, AbstractGeoAsset
|
||||||
|
|
||||||
excluded_types = (ForeignKey, OneToOneField, ManyToManyField, AutoField, GeometryField)
|
excluded_types = (ForeignKey, OneToOneField, ManyToManyField, AutoField, GeometryField, JSONField)
|
||||||
|
|
||||||
base_field_names = {f.name for f in AbstractAsset._meta.fields}
|
base_field_names = {f.name for f in AbstractAsset._meta.fields}
|
||||||
base_field_names |= {f.name for f in AbstractGeoAsset._meta.fields}
|
base_field_names |= {f.name for f in AbstractGeoAsset._meta.fields}
|
||||||
|
|
|
||||||
|
|
@ -1411,6 +1411,11 @@ CATEGORY_ASSET_MAPPING = {
|
||||||
'LI_poles': ('PublicLightingPole', 'PublicLightingPoleModel'),
|
'LI_poles': ('PublicLightingPole', 'PublicLightingPoleModel'),
|
||||||
'publiclighting_luminaire': ('PublicLightingLuminaire', 'PublicLightingLuminaireModel'),
|
'publiclighting_luminaire': ('PublicLightingLuminaire', 'PublicLightingLuminaireModel'),
|
||||||
'LI_lightfixtures': ('PublicLightingLightFixture', 'PublicLightingLightFixtureModel'),
|
'LI_lightfixtures': ('PublicLightingLightFixture', 'PublicLightingLightFixtureModel'),
|
||||||
|
# ITS categories
|
||||||
|
'its_bollard_system': ('ITSBollardSystem', 'ITSAssetModel'),
|
||||||
|
'bollard_system': ('ITSBollardSystem', 'ITSAssetModel'),
|
||||||
|
'its_bollard_plot': ('ITSBollardPlot', 'ITSAssetModel'),
|
||||||
|
'bollard_plot': ('ITSBollardPlot', 'ITSAssetModel'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1856,6 +1861,18 @@ def create_asset_api(request):
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Handle bollard system (for bollard plots)
|
||||||
|
system_id = data.get('system_id') or data.get('bollard_system_id')
|
||||||
|
if system_id and _has_model_field(new_asset, 'system'):
|
||||||
|
from assets.models import ITSBollardSystem
|
||||||
|
try:
|
||||||
|
system_obj = ITSBollardSystem.objects.get(pk=system_id)
|
||||||
|
new_asset.system = system_obj
|
||||||
|
if not getattr(new_asset, 'location', None) and system_obj.location:
|
||||||
|
new_asset.location = system_obj.location
|
||||||
|
except ITSBollardSystem.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
# Set additional data
|
# Set additional data
|
||||||
extra_data = data.get('data', {})
|
extra_data = data.get('data', {})
|
||||||
if isinstance(extra_data, dict):
|
if isinstance(extra_data, dict):
|
||||||
|
|
|
||||||
|
|
@ -461,6 +461,8 @@ ELEMENT_TYPE_LABELS = {
|
||||||
'controlcentergeoasset': _('Équipement CdC'),
|
'controlcentergeoasset': _('Équipement CdC'),
|
||||||
# --- ITS ---
|
# --- ITS ---
|
||||||
'itsgeoasset': _('Équipement ITS'),
|
'itsgeoasset': _('Équipement ITS'),
|
||||||
|
'itsbollardsystem': _('Système de bornes'),
|
||||||
|
'itsbollardplot': _('Plot de borne'),
|
||||||
# --- Propreté ---
|
# --- Propreté ---
|
||||||
'cleanlitterbin': _('Corbeille'),
|
'cleanlitterbin': _('Corbeille'),
|
||||||
'cleanglasscontainer': _('Conteneur verre'),
|
'cleanglasscontainer': _('Conteneur verre'),
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ from django.contrib.gis.db.models.functions import Transform
|
||||||
from django.contrib.gis.geos import GEOSGeometry, Point
|
from django.contrib.gis.geos import GEOSGeometry, Point
|
||||||
from django.contrib.gis.db.models.functions import Distance
|
from django.contrib.gis.db.models.functions import Distance
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db.models import Q, Case, When, IntegerField, CharField
|
from django.db.models import Q, Case, When, IntegerField, CharField, Count
|
||||||
from django.db.models.functions import Lower
|
from django.db.models.functions import Lower
|
||||||
from django.db.models import Func, Prefetch
|
from django.db.models import Func, Prefetch
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
|
|
@ -25,7 +25,10 @@ from collections import defaultdict
|
||||||
from common.utils import check_thematic_access
|
from common.utils import check_thematic_access
|
||||||
|
|
||||||
from common.models import Thematic, UserConfig, UserContractAccess, UserThematics
|
from common.models import Thematic, UserConfig, UserContractAccess, UserThematics
|
||||||
from assets.models import ITSLocation, ITSLocationDocument, ITSAssetModel, ITSGeoAsset, AssetCategory
|
from assets.models import (
|
||||||
|
ITSLocation, ITSLocationDocument, ITSAssetModel, ITSGeoAsset, AssetCategory,
|
||||||
|
ITSBollardSystem, ITSBollardPlot, ITSBollardSystemAsset, ITSBollardSystemContract
|
||||||
|
)
|
||||||
from assets.permissions import can_edit_location_for_thematic, get_allowed_actions_for_thematic
|
from assets.permissions import can_edit_location_for_thematic, get_allowed_actions_for_thematic
|
||||||
|
|
||||||
from assets.forms import ITSLocationDocumentForm
|
from assets.forms import ITSLocationDocumentForm
|
||||||
|
|
@ -297,11 +300,19 @@ def its_assets_geojson(request):
|
||||||
|
|
||||||
if lon is None or lat is None:
|
if lon is None or lat is None:
|
||||||
location = getattr(obj, 'location', None)
|
location = getattr(obj, 'location', None)
|
||||||
|
system = getattr(obj, 'system', None)
|
||||||
|
if location is None and system is not None:
|
||||||
|
location = getattr(system, 'location', None)
|
||||||
if location is not None:
|
if location is not None:
|
||||||
if lon is None:
|
if lon is None:
|
||||||
lon = getattr(location, 'lon', None)
|
lon = getattr(location, 'lon', None)
|
||||||
if lat is None:
|
if lat is None:
|
||||||
lat = getattr(location, 'lat', None)
|
lat = getattr(location, 'lat', None)
|
||||||
|
if (lon is None or lat is None) and system is not None:
|
||||||
|
if lon is None:
|
||||||
|
lon = getattr(system, 'lon', None)
|
||||||
|
if lat is None:
|
||||||
|
lat = getattr(system, 'lat', None)
|
||||||
|
|
||||||
if geometry is None and lon is not None and lat is not None:
|
if geometry is None and lon is not None and lat is not None:
|
||||||
geometry = {
|
geometry = {
|
||||||
|
|
@ -313,15 +324,13 @@ def its_assets_geojson(request):
|
||||||
|
|
||||||
def obj_to_feature(obj, asset_type=None):
|
def obj_to_feature(obj, asset_type=None):
|
||||||
geometry, lon, lat = build_geometry_and_coords(obj)
|
geometry, lon, lat = build_geometry_and_coords(obj)
|
||||||
return {
|
props = {
|
||||||
"type": "Feature",
|
|
||||||
"id": obj.id,
|
|
||||||
"geometry": geometry,
|
|
||||||
"properties": {
|
|
||||||
"code": obj.code,
|
"code": obj.code,
|
||||||
"name": obj.get_name(),
|
"name": obj.get_name(),
|
||||||
"category": obj.category.get_name() if obj.category else None,
|
"category": obj.category.get_name() if obj.category else None,
|
||||||
|
"category_id": obj.category_id,
|
||||||
"model": obj.model.get_name() if obj.model else None,
|
"model": obj.model.get_name() if obj.model else None,
|
||||||
|
"model_id": obj.model_id,
|
||||||
"status": obj.status,
|
"status": obj.status,
|
||||||
"validation_status": obj.validation_status,
|
"validation_status": obj.validation_status,
|
||||||
"lon": lon,
|
"lon": lon,
|
||||||
|
|
@ -331,7 +340,26 @@ def its_assets_geojson(request):
|
||||||
"element_type": get_element_type_label(asset_type or ''),
|
"element_type": get_element_type_label(asset_type or ''),
|
||||||
"thematic": "its",
|
"thematic": "its",
|
||||||
"is_location": False,
|
"is_location": False,
|
||||||
},
|
}
|
||||||
|
if asset_type == "itsbollardsystem":
|
||||||
|
props["width"] = obj.width
|
||||||
|
props["direction"] = obj.get_direction_display() if hasattr(obj, 'get_direction_display') else obj.direction
|
||||||
|
props["plots_count"] = getattr(obj, 'plots_total', None)
|
||||||
|
if props["plots_count"] is None and hasattr(obj, 'plots'):
|
||||||
|
props["plots_count"] = obj.plots.count()
|
||||||
|
if obj.geom:
|
||||||
|
props["length"] = round(obj.geom.length, 2)
|
||||||
|
elif asset_type == "itsbollardplot":
|
||||||
|
props["position"] = obj.position
|
||||||
|
props["bollard_type"] = obj.get_bollard_type_display() if hasattr(obj, 'get_bollard_type_display') else obj.bollard_type
|
||||||
|
if getattr(obj, 'system', None):
|
||||||
|
props["system_code"] = obj.system.code
|
||||||
|
props["system_name"] = obj.system.name
|
||||||
|
return {
|
||||||
|
"type": "Feature",
|
||||||
|
"id": obj.id,
|
||||||
|
"geometry": geometry,
|
||||||
|
"properties": props,
|
||||||
}
|
}
|
||||||
|
|
||||||
def add_assets_to_features(queryset, asset_type, features, feature_count):
|
def add_assets_to_features(queryset, asset_type, features, feature_count):
|
||||||
|
|
@ -348,16 +376,35 @@ def its_assets_geojson(request):
|
||||||
.select_related('location', 'category', 'model')
|
.select_related('location', 'category', 'model')
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
bollard_system_qs = (
|
||||||
|
ITSBollardSystem.objects
|
||||||
|
.select_related('location', 'category', 'model')
|
||||||
|
.annotate(plots_total=Count('plots'))
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
bollard_plot_qs = (
|
||||||
|
ITSBollardPlot.objects
|
||||||
|
.select_related('system', 'location', 'category', 'model')
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
# --------- Filtres métier existants
|
# --------- Filtres métier existants
|
||||||
if its_location_ids:
|
if its_location_ids:
|
||||||
its_asset_qs = its_asset_qs.filter(location__id__in=its_location_ids)
|
its_asset_qs = its_asset_qs.filter(location__id__in=its_location_ids)
|
||||||
|
bollard_system_qs = bollard_system_qs.filter(location__id__in=its_location_ids)
|
||||||
|
bollard_plot_qs = bollard_plot_qs.filter(
|
||||||
|
Q(location__id__in=its_location_ids) | Q(system__location__id__in=its_location_ids)
|
||||||
|
)
|
||||||
|
|
||||||
if models:
|
if models:
|
||||||
its_asset_qs = its_asset_qs.filter(model__id__in=models)
|
its_asset_qs = its_asset_qs.filter(model__id__in=models)
|
||||||
|
bollard_system_qs = bollard_system_qs.filter(model__id__in=models)
|
||||||
|
bollard_plot_qs = bollard_plot_qs.filter(model__id__in=models)
|
||||||
|
|
||||||
if categories:
|
if categories:
|
||||||
its_asset_qs = its_asset_qs.filter(category__id__in=categories)
|
its_asset_qs = its_asset_qs.filter(category__id__in=categories)
|
||||||
|
bollard_system_qs = bollard_system_qs.filter(category__id__in=categories)
|
||||||
|
bollard_plot_qs = bollard_plot_qs.filter(category__id__in=categories)
|
||||||
|
|
||||||
if filter_geom:
|
if filter_geom:
|
||||||
its_asset_qs = its_asset_qs.filter(
|
its_asset_qs = its_asset_qs.filter(
|
||||||
|
|
@ -365,11 +412,24 @@ def its_assets_geojson(request):
|
||||||
Q(geom__isnull=True, location__geom__intersects=filter_geom) |
|
Q(geom__isnull=True, location__geom__intersects=filter_geom) |
|
||||||
Q(geom__isnull=True, location__isnull=True)
|
Q(geom__isnull=True, location__isnull=True)
|
||||||
)
|
)
|
||||||
|
bollard_system_qs = bollard_system_qs.filter(
|
||||||
|
Q(geom__intersects=filter_geom) |
|
||||||
|
Q(geom__isnull=True, location__geom__intersects=filter_geom) |
|
||||||
|
Q(geom__isnull=True, location__isnull=True)
|
||||||
|
)
|
||||||
|
bollard_plot_qs = bollard_plot_qs.filter(
|
||||||
|
Q(geom__intersects=filter_geom) |
|
||||||
|
Q(geom__isnull=True, system__geom__intersects=filter_geom) |
|
||||||
|
Q(geom__isnull=True, location__geom__intersects=filter_geom) |
|
||||||
|
Q(geom__isnull=True, system__isnull=True)
|
||||||
|
)
|
||||||
|
|
||||||
# Recherche plein texte
|
# Recherche plein texte
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
from assets.views.core import apply_search
|
from assets.views.core import apply_search
|
||||||
its_asset_qs = apply_search(its_asset_qs, data)
|
its_asset_qs = apply_search(its_asset_qs, data)
|
||||||
|
bollard_system_qs = apply_search(bollard_system_qs, data)
|
||||||
|
bollard_plot_qs = apply_search(bollard_plot_qs, data)
|
||||||
|
|
||||||
# --------- Limitation pour utilisateurs externes
|
# --------- Limitation pour utilisateurs externes
|
||||||
user = request.user
|
user = request.user
|
||||||
|
|
@ -388,26 +448,32 @@ def its_assets_geojson(request):
|
||||||
can_view_assets=True
|
can_view_assets=True
|
||||||
).values_list('contract_id', flat=True)
|
).values_list('contract_id', flat=True)
|
||||||
|
|
||||||
# Ne garder que les assets qui ont un contrat actif
|
# Ne garder que les assets qui ont un contrat actif auquel l'utilisateur a accès
|
||||||
# auquel l'utilisateur a accès
|
|
||||||
its_asset_qs = its_asset_qs.filter(
|
its_asset_qs = its_asset_qs.filter(
|
||||||
its_assets_contracts__status='active',
|
its_assets_contracts__status='active',
|
||||||
its_assets_contracts__contract_id__in=accessible_contract_ids
|
its_assets_contracts__contract_id__in=accessible_contract_ids
|
||||||
).distinct()
|
).distinct()
|
||||||
|
bollard_system_qs = bollard_system_qs.filter(
|
||||||
|
bollard_systems_contracts__status='active',
|
||||||
|
bollard_systems_contracts__contract_id__in=accessible_contract_ids
|
||||||
|
).distinct()
|
||||||
|
bollard_plot_qs = bollard_plot_qs.filter(
|
||||||
|
system__bollard_systems_contracts__status='active',
|
||||||
|
system__bollard_systems_contracts__contract_id__in=accessible_contract_ids
|
||||||
|
).distinct()
|
||||||
else:
|
else:
|
||||||
# Non authentifié : ne rien exposer (plus sûr)
|
# Non authentifié : ne rien exposer (plus sûr)
|
||||||
its_asset_qs = its_asset_qs.none()
|
its_asset_qs = its_asset_qs.none()
|
||||||
|
bollard_system_qs = bollard_system_qs.none()
|
||||||
# --------- Cap de sécurité
|
bollard_plot_qs = bollard_plot_qs.none()
|
||||||
its_asset_qs = its_asset_qs[0:MAX_ASSET_FEATURES+1]
|
|
||||||
if len(its_asset_qs) >= MAX_ASSET_FEATURES:
|
|
||||||
return JsonResponse({"error": f"{_("Trop d'objets à afficher")} (> {MAX_ASSET_FEATURES})"}, status=413)
|
|
||||||
|
|
||||||
# --------- Construction GeoJSON
|
# --------- Construction GeoJSON
|
||||||
features = []
|
features = []
|
||||||
feature_count = 0
|
feature_count = 0
|
||||||
for qs, asset_type in [
|
for qs, asset_type in [
|
||||||
(its_asset_qs, "itsgeoasset"),
|
(its_asset_qs, "itsgeoasset"),
|
||||||
|
(bollard_system_qs, "itsbollardsystem"),
|
||||||
|
(bollard_plot_qs, "itsbollardplot"),
|
||||||
]:
|
]:
|
||||||
feature_count, limit_reached = add_assets_to_features(qs, asset_type, features, feature_count)
|
feature_count, limit_reached = add_assets_to_features(qs, asset_type, features, feature_count)
|
||||||
if limit_reached:
|
if limit_reached:
|
||||||
|
|
@ -603,7 +669,9 @@ def its_assets_autocomplete(request):
|
||||||
raw_query = raw_query_original.strip()
|
raw_query = raw_query_original.strip()
|
||||||
|
|
||||||
# CT ids (calculés une fois)
|
# CT ids (calculés une fois)
|
||||||
equipment_ct_id = ContentType.objects.get_for_model(ITSGeoAsset).id
|
geoasset_ct_id = ContentType.objects.get_for_model(ITSGeoAsset).id
|
||||||
|
bollard_system_ct_id = ContentType.objects.get_for_model(ITSBollardSystem).id
|
||||||
|
bollard_plot_ct_id = ContentType.objects.get_for_model(ITSBollardPlot).id
|
||||||
location_ct_id = ContentType.objects.get_for_model(ITSLocation).id
|
location_ct_id = ContentType.objects.get_for_model(ITSLocation).id
|
||||||
|
|
||||||
qn = strip_accents_lower(raw_query)
|
qn = strip_accents_lower(raw_query)
|
||||||
|
|
@ -626,41 +694,36 @@ def its_assets_autocomplete(request):
|
||||||
# en cas d'ID invalide ou autre, on ignore le filtre
|
# en cas d'ID invalide ou autre, on ignore le filtre
|
||||||
location_ids = None
|
location_ids = None
|
||||||
|
|
||||||
# Base queryset accent-insensible + actifs uniquement
|
def query_model(model_class, ct_id, extra_select=()):
|
||||||
qs = (
|
q = (
|
||||||
ITSGeoAsset.objects
|
model_class.objects
|
||||||
.annotate(
|
.annotate(
|
||||||
code_u=Lower(Unaccent('code')),
|
code_u=Lower(Unaccent('code')),
|
||||||
name_fr_u=Lower(Unaccent('name_fr')),
|
name_fr_u=Lower(Unaccent('name_fr')),
|
||||||
name_nl_u=Lower(Unaccent('name_nl')),
|
name_nl_u=Lower(Unaccent('name_nl')),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Si un terme de recherche est fourni, filtrer dessus
|
|
||||||
if has_search_term:
|
if has_search_term:
|
||||||
qs = qs.filter(
|
q = q.filter(
|
||||||
Q(code_u__contains=qn) |
|
Q(code_u__contains=qn) |
|
||||||
Q(name_fr_u__contains=qn) |
|
Q(name_fr_u__contains=qn) |
|
||||||
Q(name_nl_u__contains=qn)
|
Q(name_nl_u__contains=qn)
|
||||||
)
|
)
|
||||||
|
q = q.exclude(status='removed')
|
||||||
qs = qs.exclude(status='removed') # <-- exclure les supprimés
|
q = q.select_related('location', 'model', 'category', *extra_select)
|
||||||
qs = qs.select_related(
|
|
||||||
'location',
|
|
||||||
'model',
|
|
||||||
'category',
|
|
||||||
)
|
|
||||||
|
|
||||||
if location_ids:
|
if location_ids:
|
||||||
qs = qs.filter(location_id__in=location_ids)
|
if hasattr(model_class, 'system'):
|
||||||
|
q = q.filter(Q(location_id__in=location_ids) | Q(system__location_id__in=location_ids))
|
||||||
|
else:
|
||||||
|
q = q.filter(location_id__in=location_ids)
|
||||||
|
|
||||||
if category_id is not None:
|
if category_id is not None:
|
||||||
qs = qs.filter(
|
q = q.filter(
|
||||||
Q(category_id=category_id) |
|
Q(category_id=category_id) |
|
||||||
Q(category__isnull=True, model__category_id=category_id)
|
Q(category__isnull=True, model__category_id=category_id)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Priorité : exact > code startswith > nom startswith > contient
|
|
||||||
if has_search_term:
|
if has_search_term:
|
||||||
priority = Case(
|
priority = Case(
|
||||||
When(code_u=qn, then=0),
|
When(code_u=qn, then=0),
|
||||||
|
|
@ -673,21 +736,29 @@ def its_assets_autocomplete(request):
|
||||||
default=7,
|
default=7,
|
||||||
output_field=IntegerField(),
|
output_field=IntegerField(),
|
||||||
)
|
)
|
||||||
qs = qs.annotate(_prio=priority).order_by('_prio', 'code')[:100]
|
q = q.annotate(_prio=priority).order_by('_prio', 'code')[:50]
|
||||||
else:
|
else:
|
||||||
# Sans terme de recherche, tri par code uniquement
|
q = q.order_by('code')[:50]
|
||||||
qs = qs.order_by('code')[:100]
|
|
||||||
|
results = []
|
||||||
|
for obj in q:
|
||||||
|
results.append((obj, ct_id))
|
||||||
|
return results
|
||||||
|
|
||||||
|
all_results = (
|
||||||
|
query_model(ITSGeoAsset, geoasset_ct_id) +
|
||||||
|
query_model(ITSBollardSystem, bollard_system_ct_id) +
|
||||||
|
query_model(ITSBollardPlot, bollard_plot_ct_id, ('system', 'system__location'))
|
||||||
|
)
|
||||||
|
|
||||||
data = []
|
data = []
|
||||||
for a in qs:
|
for a, ct_id in all_results[:100]:
|
||||||
# equipment_type depuis model/category
|
|
||||||
equipment_type = None
|
equipment_type = None
|
||||||
if getattr(a, 'model', None):
|
if getattr(a, 'model', None):
|
||||||
equipment_type = getattr(a.model, 'name', None) or getattr(a.model, 'code', None)
|
equipment_type = getattr(a.model, 'name', None) or getattr(a.model, 'code', None)
|
||||||
if not equipment_type and getattr(a, 'category', None):
|
if not equipment_type and getattr(a, 'category', None):
|
||||||
equipment_type = getattr(a.category, 'name', None)
|
equipment_type = getattr(a.category, 'name', None)
|
||||||
|
|
||||||
# coordonnées: asset > geom(4326) > location
|
|
||||||
lon = a.lon
|
lon = a.lon
|
||||||
lat = a.lat
|
lat = a.lat
|
||||||
|
|
||||||
|
|
@ -698,24 +769,33 @@ def its_assets_autocomplete(request):
|
||||||
g = a.geom.transform(4326, clone=True)
|
g = a.geom.transform(4326, clone=True)
|
||||||
coords = getattr(g, 'coords', None)
|
coords = getattr(g, 'coords', None)
|
||||||
if coords:
|
if coords:
|
||||||
|
if isinstance(coords[0], (int, float)):
|
||||||
if lon is None: lon = coords[0]
|
if lon is None: lon = coords[0]
|
||||||
if lat is None: lat = coords[1]
|
if lat is None: lat = coords[1]
|
||||||
|
elif isinstance(coords[0], (list, tuple)) and coords[0]:
|
||||||
|
# Pour LineString (ITSBollardSystem)
|
||||||
|
mid = len(coords) // 2
|
||||||
|
if lon is None: lon = coords[mid][0]
|
||||||
|
if lat is None: lat = coords[mid][1]
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if (lon is None or lat is None) and getattr(a, 'location', None):
|
loc = getattr(a, 'location', None)
|
||||||
if lon is None and getattr(a.location, 'lon', None) is not None:
|
if loc is None and hasattr(a, 'system') and a.system:
|
||||||
lon = a.location.lon
|
loc = getattr(a.system, 'location', None)
|
||||||
if lat is None and getattr(a.location, 'lat', None) is not None:
|
|
||||||
lat = a.location.lat
|
|
||||||
|
|
||||||
|
if (lon is None or lat is None) and loc:
|
||||||
|
if lon is None and getattr(loc, 'lon', None) is not None:
|
||||||
|
lon = loc.lon
|
||||||
|
if lat is None and getattr(loc, 'lat', None) is not None:
|
||||||
|
lat = loc.lat
|
||||||
|
|
||||||
equipment_label = f"{a.get_name()} ({a.code})" if a.code else a.get_name()
|
equipment_label = f"{a.get_name()} ({a.code})" if a.code else a.get_name()
|
||||||
label = f"{a.code} - {equipment_label}" if a.code else equipment_label
|
label = f"{a.code} - {equipment_label}" if a.code else equipment_label
|
||||||
|
|
||||||
location_label = (
|
location_label = (
|
||||||
f"{a.location.get_name()} ({a.location.code})"
|
f"{loc.get_name()} ({loc.code})"
|
||||||
if getattr(a, "location", None) else None
|
if loc else None
|
||||||
)
|
)
|
||||||
|
|
||||||
data.append({
|
data.append({
|
||||||
|
|
@ -725,9 +805,9 @@ def its_assets_autocomplete(request):
|
||||||
"equipment_id": a.id,
|
"equipment_id": a.id,
|
||||||
"equipment_label": equipment_label,
|
"equipment_label": equipment_label,
|
||||||
"equipment_type": equipment_type,
|
"equipment_type": equipment_type,
|
||||||
"equipment_ct": equipment_ct_id,
|
"equipment_ct": ct_id,
|
||||||
|
|
||||||
"location_id": a.location.id if getattr(a, "location", None) else None,
|
"location_id": loc.id if loc else None,
|
||||||
"location_label": location_label,
|
"location_label": location_label,
|
||||||
"location_ct": location_ct_id,
|
"location_ct": location_ct_id,
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue