feat: automate asset parent data synchronization for geometry and location fields with new management command and model hooks
This commit is contained in:
parent
016ce202b4
commit
1e7e8ae697
16 changed files with 1032 additions and 20 deletions
379
loko/assets/management/commands/sync_asset_parent_data.py
Normal file
379
loko/assets/management/commands/sync_asset_parent_data.py
Normal file
|
|
@ -0,0 +1,379 @@
|
||||||
|
import logging
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.db import transaction
|
||||||
|
|
||||||
|
from assets.models.trafficlights import (
|
||||||
|
TrafficLightLantern,
|
||||||
|
TrafficLightAccessory,
|
||||||
|
TrafficLightDetector,
|
||||||
|
TrafficLightControllerHardware,
|
||||||
|
TrafficLightNetworkHardware,
|
||||||
|
TrafficLightVoltageHardware,
|
||||||
|
TrafficLightElectricalCabinetContent,
|
||||||
|
)
|
||||||
|
from assets.models.sign import SignPanel
|
||||||
|
from assets.models.publiclighting import PublicLightingLightFixture
|
||||||
|
from assets.models.its import ITSGeoAsset
|
||||||
|
from assets.models.structures import StructureGeoAsset
|
||||||
|
from assets.models.buildings import BuildingAsset
|
||||||
|
from assets.models.outdoor import OutdoorAsset
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Synchronizes coordinates (geom, lon, lat, geojson) and locations from parent supports to child assets."
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument(
|
||||||
|
'--batch-size',
|
||||||
|
type=int,
|
||||||
|
default=1000,
|
||||||
|
help="Batch size for processing records."
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
batch_size = options['batch_size']
|
||||||
|
self.stdout.write(self.style.NOTICE("Starting parent-child asset data synchronization..."))
|
||||||
|
|
||||||
|
# 1. Traffic Light Lanterns
|
||||||
|
self.stdout.write("1. Syncing Traffic Light Lanterns...")
|
||||||
|
lanterns = TrafficLightLantern.objects.select_related('pole').filter(pole__geom__isnull=False)
|
||||||
|
count = 0
|
||||||
|
to_update = []
|
||||||
|
for lantern in lanterns.iterator(chunk_size=batch_size):
|
||||||
|
if lantern.pole and lantern.pole.geom:
|
||||||
|
if lantern.geom != lantern.pole.geom or lantern.lon != lantern.pole.lon or lantern.lat != lantern.pole.lat:
|
||||||
|
lantern.geom = lantern.pole.geom
|
||||||
|
lantern.lon = lantern.pole.lon
|
||||||
|
lantern.lat = lantern.pole.lat
|
||||||
|
lantern.geojson = lantern.pole.geojson
|
||||||
|
to_update.append(lantern)
|
||||||
|
count += 1
|
||||||
|
if len(to_update) >= batch_size:
|
||||||
|
TrafficLightLantern.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson'])
|
||||||
|
to_update = []
|
||||||
|
if to_update:
|
||||||
|
TrafficLightLantern.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson'])
|
||||||
|
self.stdout.write(self.style.SUCCESS(f" ✓ Updated {count} Traffic Light Lanterns"))
|
||||||
|
|
||||||
|
# 2. Traffic Light Accessories
|
||||||
|
self.stdout.write("2. Syncing Traffic Light Accessories...")
|
||||||
|
accessories = TrafficLightAccessory.objects.select_related('pole', 'intersection').filter(pole__geom__isnull=False)
|
||||||
|
count = 0
|
||||||
|
to_update = []
|
||||||
|
for acc in accessories.iterator(chunk_size=batch_size):
|
||||||
|
changed = False
|
||||||
|
if acc.pole and acc.pole.geom:
|
||||||
|
if acc.geom != acc.pole.geom or acc.lon != acc.pole.lon or acc.lat != acc.pole.lat:
|
||||||
|
acc.geom = acc.pole.geom
|
||||||
|
acc.lon = acc.pole.lon
|
||||||
|
acc.lat = acc.pole.lat
|
||||||
|
acc.geojson = acc.pole.geojson
|
||||||
|
changed = True
|
||||||
|
if acc.pole and acc.pole.intersection and not acc.intersection_id:
|
||||||
|
acc.intersection = acc.pole.intersection
|
||||||
|
changed = True
|
||||||
|
if changed:
|
||||||
|
to_update.append(acc)
|
||||||
|
count += 1
|
||||||
|
if len(to_update) >= batch_size:
|
||||||
|
TrafficLightAccessory.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson', 'intersection'])
|
||||||
|
to_update = []
|
||||||
|
if to_update:
|
||||||
|
TrafficLightAccessory.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson', 'intersection'])
|
||||||
|
self.stdout.write(self.style.SUCCESS(f" ✓ Updated {count} Traffic Light Accessories"))
|
||||||
|
|
||||||
|
# 3. Traffic Light Detectors
|
||||||
|
self.stdout.write("3. Syncing Traffic Light Detectors...")
|
||||||
|
detectors = TrafficLightDetector.objects.select_related('pole', 'controller')
|
||||||
|
count = 0
|
||||||
|
to_update = []
|
||||||
|
for det in detectors.iterator(chunk_size=batch_size):
|
||||||
|
changed = False
|
||||||
|
support = det.pole or det.controller
|
||||||
|
if support and getattr(support, 'geom', None):
|
||||||
|
if det.geom != support.geom or det.lon != support.lon or det.lat != support.lat:
|
||||||
|
det.geom = support.geom
|
||||||
|
det.lon = support.lon
|
||||||
|
det.lat = support.lat
|
||||||
|
det.geojson = support.geojson
|
||||||
|
changed = True
|
||||||
|
if det.pole and det.pole.intersection and not det.intersection_id:
|
||||||
|
det.intersection = det.pole.intersection
|
||||||
|
changed = True
|
||||||
|
elif det.controller and det.controller.intersection and not det.intersection_id:
|
||||||
|
det.intersection = det.controller.intersection
|
||||||
|
changed = True
|
||||||
|
if changed:
|
||||||
|
to_update.append(det)
|
||||||
|
count += 1
|
||||||
|
if len(to_update) >= batch_size:
|
||||||
|
TrafficLightDetector.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson', 'intersection'])
|
||||||
|
to_update = []
|
||||||
|
if to_update:
|
||||||
|
TrafficLightDetector.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson', 'intersection'])
|
||||||
|
self.stdout.write(self.style.SUCCESS(f" ✓ Updated {count} Traffic Light Detectors"))
|
||||||
|
|
||||||
|
# 4. Traffic Light Controller Hardware
|
||||||
|
self.stdout.write("4. Syncing Traffic Light Controller Hardware...")
|
||||||
|
hws = TrafficLightControllerHardware.objects.select_related('controller').filter(controller__geom__isnull=False)
|
||||||
|
count = 0
|
||||||
|
to_update = []
|
||||||
|
for hw in hws.iterator(chunk_size=batch_size):
|
||||||
|
if hw.controller and hw.controller.geom:
|
||||||
|
if hw.geom != hw.controller.geom or hw.lon != hw.controller.lon or hw.lat != hw.controller.lat:
|
||||||
|
hw.geom = hw.controller.geom
|
||||||
|
hw.lon = hw.controller.lon
|
||||||
|
hw.lat = hw.controller.lat
|
||||||
|
hw.geojson = hw.controller.geojson
|
||||||
|
to_update.append(hw)
|
||||||
|
count += 1
|
||||||
|
if len(to_update) >= batch_size:
|
||||||
|
TrafficLightControllerHardware.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson'])
|
||||||
|
to_update = []
|
||||||
|
if to_update:
|
||||||
|
TrafficLightControllerHardware.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson'])
|
||||||
|
self.stdout.write(self.style.SUCCESS(f" ✓ Updated {count} Traffic Light Controller Hardwares"))
|
||||||
|
|
||||||
|
# 5. Traffic Light Network Hardware
|
||||||
|
self.stdout.write("5. Syncing Traffic Light Network Hardware...")
|
||||||
|
nets = TrafficLightNetworkHardware.objects.select_related('controller').filter(controller__geom__isnull=False)
|
||||||
|
count = 0
|
||||||
|
to_update = []
|
||||||
|
for net in nets.iterator(chunk_size=batch_size):
|
||||||
|
changed = False
|
||||||
|
if net.controller and net.controller.geom:
|
||||||
|
if net.geom != net.controller.geom or net.lon != net.controller.lon or net.lat != net.controller.lat:
|
||||||
|
net.geom = net.controller.geom
|
||||||
|
net.lon = net.controller.lon
|
||||||
|
net.lat = net.controller.lat
|
||||||
|
net.geojson = net.controller.geojson
|
||||||
|
changed = True
|
||||||
|
if net.controller and net.controller.intersection and not net.intersection_id:
|
||||||
|
net.intersection = net.controller.intersection
|
||||||
|
changed = True
|
||||||
|
if changed:
|
||||||
|
to_update.append(net)
|
||||||
|
count += 1
|
||||||
|
if len(to_update) >= batch_size:
|
||||||
|
TrafficLightNetworkHardware.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson', 'intersection'])
|
||||||
|
to_update = []
|
||||||
|
if to_update:
|
||||||
|
TrafficLightNetworkHardware.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson', 'intersection'])
|
||||||
|
self.stdout.write(self.style.SUCCESS(f" ✓ Updated {count} Traffic Light Network Hardwares"))
|
||||||
|
|
||||||
|
# 6. Traffic Light Voltage Hardware
|
||||||
|
self.stdout.write("6. Syncing Traffic Light Voltage Hardware...")
|
||||||
|
volts = TrafficLightVoltageHardware.objects.select_related('controller').filter(controller__geom__isnull=False)
|
||||||
|
count = 0
|
||||||
|
to_update = []
|
||||||
|
for volt in volts.iterator(chunk_size=batch_size):
|
||||||
|
changed = False
|
||||||
|
if volt.controller and volt.controller.geom:
|
||||||
|
if volt.geom != volt.controller.geom or volt.lon != volt.controller.lon or volt.lat != volt.controller.lat:
|
||||||
|
volt.geom = volt.controller.geom
|
||||||
|
volt.lon = volt.controller.lon
|
||||||
|
volt.lat = volt.controller.lat
|
||||||
|
volt.geojson = volt.controller.geojson
|
||||||
|
changed = True
|
||||||
|
if volt.controller and volt.controller.intersection and not volt.intersection_id:
|
||||||
|
volt.intersection = volt.controller.intersection
|
||||||
|
changed = True
|
||||||
|
if changed:
|
||||||
|
to_update.append(volt)
|
||||||
|
count += 1
|
||||||
|
if len(to_update) >= batch_size:
|
||||||
|
TrafficLightVoltageHardware.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson', 'intersection'])
|
||||||
|
to_update = []
|
||||||
|
if to_update:
|
||||||
|
TrafficLightVoltageHardware.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson', 'intersection'])
|
||||||
|
self.stdout.write(self.style.SUCCESS(f" ✓ Updated {count} Traffic Light Voltage Hardwares"))
|
||||||
|
|
||||||
|
# 7. Traffic Light Electrical Cabinet Contents
|
||||||
|
self.stdout.write("7. Syncing Traffic Light Electrical Cabinet Contents...")
|
||||||
|
contents = TrafficLightElectricalCabinetContent.objects.select_related('cabinet').filter(cabinet__geom__isnull=False)
|
||||||
|
count = 0
|
||||||
|
to_update = []
|
||||||
|
for content in contents.iterator(chunk_size=batch_size):
|
||||||
|
if content.cabinet and content.cabinet.geom:
|
||||||
|
if content.geom != content.cabinet.geom or content.lon != content.cabinet.lon or content.lat != content.cabinet.lat:
|
||||||
|
content.geom = content.cabinet.geom
|
||||||
|
content.lon = content.cabinet.lon
|
||||||
|
content.lat = content.cabinet.lat
|
||||||
|
content.geojson = content.cabinet.geojson
|
||||||
|
to_update.append(content)
|
||||||
|
count += 1
|
||||||
|
if len(to_update) >= batch_size:
|
||||||
|
TrafficLightElectricalCabinetContent.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson'])
|
||||||
|
to_update = []
|
||||||
|
if to_update:
|
||||||
|
TrafficLightElectricalCabinetContent.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson'])
|
||||||
|
self.stdout.write(self.style.SUCCESS(f" ✓ Updated {count} Electrical Cabinet Contents"))
|
||||||
|
|
||||||
|
# 8. Road Sign Panels
|
||||||
|
self.stdout.write("8. Syncing Road Sign Panels...")
|
||||||
|
panels = SignPanel.objects.select_related('pole', 'trafficlight_pole', 'publiclighting_pole')
|
||||||
|
count = 0
|
||||||
|
to_update = []
|
||||||
|
for panel in panels.iterator(chunk_size=batch_size):
|
||||||
|
changed = False
|
||||||
|
support = panel.pole or panel.trafficlight_pole or panel.publiclighting_pole
|
||||||
|
if support and getattr(support, 'geom', None):
|
||||||
|
if panel.geom != support.geom or panel.lon != support.lon or panel.lat != support.lat:
|
||||||
|
panel.geom = support.geom
|
||||||
|
panel.lon = support.lon
|
||||||
|
panel.lat = support.lat
|
||||||
|
panel.geojson = support.geojson
|
||||||
|
changed = True
|
||||||
|
if panel.pole and panel.pole.street and not panel.street_id:
|
||||||
|
panel.street = panel.pole.street
|
||||||
|
changed = True
|
||||||
|
if changed:
|
||||||
|
to_update.append(panel)
|
||||||
|
count += 1
|
||||||
|
if len(to_update) >= batch_size:
|
||||||
|
SignPanel.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson', 'street'])
|
||||||
|
to_update = []
|
||||||
|
if to_update:
|
||||||
|
SignPanel.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson', 'street'])
|
||||||
|
self.stdout.write(self.style.SUCCESS(f" ✓ Updated {count} Road Sign Panels"))
|
||||||
|
|
||||||
|
# 9. Public Lighting Light Fixtures (Luminaires)
|
||||||
|
self.stdout.write("9. Syncing Public Lighting Light Fixtures...")
|
||||||
|
fixtures = PublicLightingLightFixture.objects.select_related('pole', 'traffic_light_pole')
|
||||||
|
count = 0
|
||||||
|
to_update = []
|
||||||
|
for fix in fixtures.iterator(chunk_size=batch_size):
|
||||||
|
changed = False
|
||||||
|
support = fix.pole or fix.traffic_light_pole
|
||||||
|
if support and getattr(support, 'geom', None):
|
||||||
|
if fix.geom != support.geom or fix.lon != support.lon or fix.lat != support.lat:
|
||||||
|
fix.geom = support.geom
|
||||||
|
fix.lon = support.lon
|
||||||
|
fix.lat = support.lat
|
||||||
|
fix.geojson = support.geojson
|
||||||
|
changed = True
|
||||||
|
if fix.pole and fix.pole.street and not fix.street_id:
|
||||||
|
fix.street = fix.pole.street
|
||||||
|
changed = True
|
||||||
|
if changed:
|
||||||
|
to_update.append(fix)
|
||||||
|
count += 1
|
||||||
|
if len(to_update) >= batch_size:
|
||||||
|
PublicLightingLightFixture.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson', 'street'])
|
||||||
|
to_update = []
|
||||||
|
if to_update:
|
||||||
|
PublicLightingLightFixture.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson', 'street'])
|
||||||
|
self.stdout.write(self.style.SUCCESS(f" ✓ Updated {count} Public Lighting Light Fixtures"))
|
||||||
|
|
||||||
|
# 10. ITS Assets
|
||||||
|
self.stdout.write("10. Syncing ITS Assets...")
|
||||||
|
its_assets = ITSGeoAsset.objects.select_related('trafficlights_pole', 'lighting_pole')
|
||||||
|
count = 0
|
||||||
|
to_update = []
|
||||||
|
for its in its_assets.iterator(chunk_size=batch_size):
|
||||||
|
support = its.trafficlights_pole or its.lighting_pole
|
||||||
|
if support and getattr(support, 'geom', None):
|
||||||
|
if its.geom != support.geom or its.lon != support.lon or its.lat != support.lat:
|
||||||
|
its.geom = support.geom
|
||||||
|
its.lon = support.lon
|
||||||
|
its.lat = support.lat
|
||||||
|
its.geojson = support.geojson
|
||||||
|
to_update.append(its)
|
||||||
|
count += 1
|
||||||
|
if len(to_update) >= batch_size:
|
||||||
|
ITSGeoAsset.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson'])
|
||||||
|
to_update = []
|
||||||
|
if to_update:
|
||||||
|
ITSGeoAsset.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson'])
|
||||||
|
self.stdout.write(self.style.SUCCESS(f" ✓ Updated {count} ITS Assets"))
|
||||||
|
|
||||||
|
# 11. Structure Assets
|
||||||
|
self.stdout.write("11. Syncing Structure Assets...")
|
||||||
|
struct_assets = StructureGeoAsset.objects.select_related('parent_asset')
|
||||||
|
count = 0
|
||||||
|
to_update = []
|
||||||
|
for sa in struct_assets.iterator(chunk_size=batch_size):
|
||||||
|
changed = False
|
||||||
|
if sa.parent_asset and getattr(sa.parent_asset, 'geom', None) and not sa.geom:
|
||||||
|
sa.geom = sa.parent_asset.geom
|
||||||
|
sa.lon = sa.parent_asset.lon
|
||||||
|
sa.lat = sa.parent_asset.lat
|
||||||
|
sa.geojson = sa.parent_asset.geojson
|
||||||
|
changed = True
|
||||||
|
if sa.parent_asset and sa.parent_asset.location and not sa.location_id:
|
||||||
|
sa.location = sa.parent_asset.location
|
||||||
|
changed = True
|
||||||
|
if sa.parent_asset and sa.parent_asset.structure and not sa.structure_id:
|
||||||
|
sa.structure = sa.parent_asset.structure
|
||||||
|
changed = True
|
||||||
|
if changed:
|
||||||
|
to_update.append(sa)
|
||||||
|
count += 1
|
||||||
|
if len(to_update) >= batch_size:
|
||||||
|
StructureGeoAsset.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson', 'location', 'structure'])
|
||||||
|
to_update = []
|
||||||
|
if to_update:
|
||||||
|
StructureGeoAsset.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson', 'location', 'structure'])
|
||||||
|
self.stdout.write(self.style.SUCCESS(f" ✓ Updated {count} Structure Assets"))
|
||||||
|
|
||||||
|
# 12. Building Assets
|
||||||
|
self.stdout.write("12. Syncing Building Assets...")
|
||||||
|
bld_assets = BuildingAsset.objects.select_related('parent_asset')
|
||||||
|
count = 0
|
||||||
|
to_update = []
|
||||||
|
for ba in bld_assets.iterator(chunk_size=batch_size):
|
||||||
|
changed = False
|
||||||
|
if ba.parent_asset and getattr(ba.parent_asset, 'geom', None) and not ba.geom:
|
||||||
|
ba.geom = ba.parent_asset.geom
|
||||||
|
ba.lon = ba.parent_asset.lon
|
||||||
|
ba.lat = ba.parent_asset.lat
|
||||||
|
ba.geojson = ba.parent_asset.geojson
|
||||||
|
changed = True
|
||||||
|
if ba.parent_asset and ba.parent_asset.location and not ba.location_id:
|
||||||
|
ba.location = ba.parent_asset.location
|
||||||
|
changed = True
|
||||||
|
if ba.parent_asset and ba.parent_asset.building and not ba.building_id:
|
||||||
|
ba.building = ba.parent_asset.building
|
||||||
|
changed = True
|
||||||
|
if changed:
|
||||||
|
to_update.append(ba)
|
||||||
|
count += 1
|
||||||
|
if len(to_update) >= batch_size:
|
||||||
|
BuildingAsset.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson', 'location', 'building'])
|
||||||
|
to_update = []
|
||||||
|
if to_update:
|
||||||
|
BuildingAsset.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson', 'location', 'building'])
|
||||||
|
self.stdout.write(self.style.SUCCESS(f" ✓ Updated {count} Building Assets"))
|
||||||
|
|
||||||
|
# 13. Outdoor Assets
|
||||||
|
self.stdout.write("13. Syncing Outdoor Assets...")
|
||||||
|
out_assets = OutdoorAsset.objects.select_related('parent_asset')
|
||||||
|
count = 0
|
||||||
|
to_update = []
|
||||||
|
for oa in out_assets.iterator(chunk_size=batch_size):
|
||||||
|
changed = False
|
||||||
|
if oa.parent_asset and getattr(oa.parent_asset, 'geom', None) and not oa.geom:
|
||||||
|
oa.geom = oa.parent_asset.geom
|
||||||
|
oa.lon = oa.parent_asset.lon
|
||||||
|
oa.lat = oa.parent_asset.lat
|
||||||
|
oa.geojson = oa.parent_asset.geojson
|
||||||
|
changed = True
|
||||||
|
if oa.parent_asset and oa.parent_asset.location and not oa.location_id:
|
||||||
|
oa.location = oa.parent_asset.location
|
||||||
|
changed = True
|
||||||
|
if oa.parent_asset and oa.parent_asset.site and not oa.site_id:
|
||||||
|
oa.site = oa.parent_asset.site
|
||||||
|
changed = True
|
||||||
|
if changed:
|
||||||
|
to_update.append(oa)
|
||||||
|
count += 1
|
||||||
|
if len(to_update) >= batch_size:
|
||||||
|
OutdoorAsset.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson', 'location', 'site'])
|
||||||
|
to_update = []
|
||||||
|
if to_update:
|
||||||
|
OutdoorAsset.objects.bulk_update(to_update, ['geom', 'lon', 'lat', 'geojson', 'location', 'site'])
|
||||||
|
self.stdout.write(self.style.SUCCESS(f" ✓ Updated {count} Outdoor Assets"))
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS("All asset parent data synchronized successfully!"))
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
# Generated by Django 6.0.7 on 2026-08-17 07:23
|
||||||
|
|
||||||
|
import django.contrib.gis.db.models.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('assets', '0019_naturerwiasset_location'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='trafficlightlantern',
|
||||||
|
name='geojson',
|
||||||
|
field=models.TextField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='trafficlightlantern',
|
||||||
|
name='geom',
|
||||||
|
field=django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=3812),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='trafficlightlantern',
|
||||||
|
name='lat',
|
||||||
|
field=models.FloatField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='trafficlightlantern',
|
||||||
|
name='lon',
|
||||||
|
field=models.FloatField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
20
loko/assets/migrations/0021_sync_asset_parent_data.py
Normal file
20
loko/assets/migrations/0021_sync_asset_parent_data.py
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
from django.db import migrations
|
||||||
|
from django.core.management import call_command
|
||||||
|
|
||||||
|
|
||||||
|
def sync_parent_data(apps, schema_editor):
|
||||||
|
try:
|
||||||
|
call_command('sync_asset_parent_data')
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning during sync_asset_parent_data migration: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('assets', '0020_trafficlightlantern_geojson_trafficlightlantern_geom_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(sync_parent_data, migrations.RunPython.noop),
|
||||||
|
]
|
||||||
|
|
@ -211,6 +211,30 @@ class BuildingAsset(AbstractGeoAsset):
|
||||||
last_control_date = models.DateField(null=True, blank=True, verbose_name=_("Dernier contrôle réglementaire"))
|
last_control_date = models.DateField(null=True, blank=True, verbose_name=_("Dernier contrôle réglementaire"))
|
||||||
next_control_date = models.DateField(null=True, blank=True, verbose_name=_("Prochain contrôle réglementaire"))
|
next_control_date = models.DateField(null=True, blank=True, verbose_name=_("Prochain contrôle réglementaire"))
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
if self.parent_asset and getattr(self.parent_asset, 'geom', None) and not self.geom:
|
||||||
|
self.geom = self.parent_asset.geom
|
||||||
|
self.lon = self.parent_asset.lon
|
||||||
|
self.lat = self.parent_asset.lat
|
||||||
|
self.geojson = self.parent_asset.geojson
|
||||||
|
if self.parent_asset and self.parent_asset.location and not self.location_id:
|
||||||
|
self.location = self.parent_asset.location
|
||||||
|
if self.parent_asset and self.parent_asset.building and not self.building_id:
|
||||||
|
self.building = self.parent_asset.building
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self.parent_asset and getattr(self.parent_asset, 'geom', None) and not self.geom:
|
||||||
|
self.geom = self.parent_asset.geom
|
||||||
|
self.lon = self.parent_asset.lon
|
||||||
|
self.lat = self.parent_asset.lat
|
||||||
|
self.geojson = self.parent_asset.geojson
|
||||||
|
if self.parent_asset and self.parent_asset.location and not self.location_id:
|
||||||
|
self.location = self.parent_asset.location
|
||||||
|
if self.parent_asset and self.parent_asset.building and not self.building_id:
|
||||||
|
self.building = self.parent_asset.building
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Asset / Équipement de bâtiment")
|
verbose_name = _("Asset / Équipement de bâtiment")
|
||||||
verbose_name_plural = _("Assets / Équipements de bâtiment")
|
verbose_name_plural = _("Assets / Équipements de bâtiment")
|
||||||
|
|
|
||||||
|
|
@ -331,6 +331,50 @@ class AbstractAsset(models.Model):
|
||||||
|
|
||||||
return original
|
return original
|
||||||
|
|
||||||
|
def get_parent_support(self):
|
||||||
|
"""
|
||||||
|
Returns the direct parent support object if this asset is mounted or attached to another asset.
|
||||||
|
e.g., pole, controller, cabinet, parent_asset, etc.
|
||||||
|
"""
|
||||||
|
for attr in (
|
||||||
|
'pole', 'trafficlight_pole', 'trafficlights_pole',
|
||||||
|
'publiclighting_pole', 'lighting_pole', 'controller',
|
||||||
|
'cabinet', 'parent_asset'
|
||||||
|
):
|
||||||
|
support = getattr(self, attr, None)
|
||||||
|
if support is not None:
|
||||||
|
return support
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_location(self):
|
||||||
|
"""
|
||||||
|
Resolves the associated location object (Street, Intersection, StructureLocation, PublicBuilding, etc.)
|
||||||
|
either directly from the asset or indirectly via its parent support.
|
||||||
|
"""
|
||||||
|
# 1. Direct location fields
|
||||||
|
for attr in ('location', 'street', 'intersection', 'building', 'site', 'structure'):
|
||||||
|
loc = getattr(self, attr, None)
|
||||||
|
if loc is not None:
|
||||||
|
if attr == 'structure' and hasattr(loc, 'main_location') and loc.main_location:
|
||||||
|
return loc.main_location
|
||||||
|
return loc
|
||||||
|
|
||||||
|
# 2. Indirectly via parent support
|
||||||
|
parent_support = self.get_parent_support()
|
||||||
|
if parent_support is not None:
|
||||||
|
if hasattr(parent_support, 'get_location'):
|
||||||
|
loc = parent_support.get_location()
|
||||||
|
if loc is not None:
|
||||||
|
return loc
|
||||||
|
for attr in ('location', 'street', 'intersection', 'building', 'site', 'structure'):
|
||||||
|
loc = getattr(parent_support, attr, None)
|
||||||
|
if loc is not None:
|
||||||
|
if attr == 'structure' and hasattr(loc, 'main_location') and loc.main_location:
|
||||||
|
return loc.main_location
|
||||||
|
return loc
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def get_name(self):
|
def get_name(self):
|
||||||
lang = get_language()
|
lang = get_language()
|
||||||
name = getattr(self, f"name_{lang}", self.name_fr) or "" # Fallback to name_fr
|
name = getattr(self, f"name_{lang}", self.name_fr) or "" # Fallback to name_fr
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,24 @@ class ITSGeoAsset(AbstractGeoAsset):
|
||||||
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")
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
support = self.trafficlights_pole or self.lighting_pole
|
||||||
|
if support and getattr(support, 'geom', None):
|
||||||
|
self.geom = support.geom
|
||||||
|
self.lon = support.lon
|
||||||
|
self.lat = support.lat
|
||||||
|
self.geojson = support.geojson
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
support = self.trafficlights_pole or self.lighting_pole
|
||||||
|
if support and getattr(support, 'geom', None):
|
||||||
|
self.geom = support.geom
|
||||||
|
self.lon = support.lon
|
||||||
|
self.lat = support.lat
|
||||||
|
self.geojson = support.geojson
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=["code"]),
|
models.Index(fields=["code"]),
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,30 @@ class OutdoorAsset(AbstractGeoAsset):
|
||||||
impact_surface_type = models.CharField(max_length=150, blank=True, null=True, verbose_name=_("Type de sol d'amortissement"))
|
impact_surface_type = models.CharField(max_length=150, blank=True, null=True, verbose_name=_("Type de sol d'amortissement"))
|
||||||
critical_fall_height_m = models.FloatField(null=True, blank=True, verbose_name=_("Hauteur critique de chute (m)"))
|
critical_fall_height_m = models.FloatField(null=True, blank=True, verbose_name=_("Hauteur critique de chute (m)"))
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
if self.parent_asset and getattr(self.parent_asset, 'geom', None) and not self.geom:
|
||||||
|
self.geom = self.parent_asset.geom
|
||||||
|
self.lon = self.parent_asset.lon
|
||||||
|
self.lat = self.parent_asset.lat
|
||||||
|
self.geojson = self.parent_asset.geojson
|
||||||
|
if self.parent_asset and self.parent_asset.location and not self.location_id:
|
||||||
|
self.location = self.parent_asset.location
|
||||||
|
if self.parent_asset and self.parent_asset.site and not self.site_id:
|
||||||
|
self.site = self.parent_asset.site
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self.parent_asset and getattr(self.parent_asset, 'geom', None) and not self.geom:
|
||||||
|
self.geom = self.parent_asset.geom
|
||||||
|
self.lon = self.parent_asset.lon
|
||||||
|
self.lat = self.parent_asset.lat
|
||||||
|
self.geojson = self.parent_asset.geojson
|
||||||
|
if self.parent_asset and self.parent_asset.location and not self.location_id:
|
||||||
|
self.location = self.parent_asset.location
|
||||||
|
if self.parent_asset and self.parent_asset.site and not self.site_id:
|
||||||
|
self.site = self.parent_asset.site
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Asset / Équipement plein air")
|
verbose_name = _("Asset / Équipement plein air")
|
||||||
verbose_name_plural = _("Assets / Équipements plein air")
|
verbose_name_plural = _("Assets / Équipements plein air")
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,12 @@ class PublicLightingPole(AbstractGeoAsset):
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
for radar in self.radars.all():
|
for radar in self.radars.all():
|
||||||
radar.save()
|
radar.save()
|
||||||
|
for lf in self.light_fixtures.all():
|
||||||
|
lf.save()
|
||||||
|
for sp in self.signpanels_attached.all():
|
||||||
|
sp.save()
|
||||||
|
for its in self.its_assets.all():
|
||||||
|
its.save()
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('assets:publiclighting_assets_detail', kwargs={'asset_model': 'publiclightingpole', 'asset_id':self.id})
|
return reverse('assets:publiclighting_assets_detail', kwargs={'asset_model': 'publiclightingpole', 'asset_id':self.id})
|
||||||
|
|
@ -105,6 +111,28 @@ class PublicLightingLightFixture(AbstractGeoAsset):
|
||||||
traffic_light_pole = models.ForeignKey(TrafficLightPole, related_name="light_fixtures", on_delete=models.SET_NULL, null=True, blank=True)
|
traffic_light_pole = models.ForeignKey(TrafficLightPole, related_name="light_fixtures", on_delete=models.SET_NULL, null=True, blank=True)
|
||||||
street = models.ForeignKey(PublicLightingStreet, related_name="light_fixtures", on_delete=models.PROTECT)
|
street = models.ForeignKey(PublicLightingStreet, related_name="light_fixtures", on_delete=models.PROTECT)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
support = self.pole or self.traffic_light_pole
|
||||||
|
if support and getattr(support, 'geom', None):
|
||||||
|
self.geom = support.geom
|
||||||
|
self.lon = support.lon
|
||||||
|
self.lat = support.lat
|
||||||
|
self.geojson = support.geojson
|
||||||
|
if self.pole and self.pole.street and not self.street_id:
|
||||||
|
self.street = self.pole.street
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
support = self.pole or self.traffic_light_pole
|
||||||
|
if support and getattr(support, 'geom', None):
|
||||||
|
self.geom = support.geom
|
||||||
|
self.lon = support.lon
|
||||||
|
self.lat = support.lat
|
||||||
|
self.geojson = support.geojson
|
||||||
|
if self.pole and self.pole.street and not self.street_id:
|
||||||
|
self.street = self.pole.street
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=["code"]),
|
models.Index(fields=["code"]),
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,11 @@ class SignPole(AbstractGeoAsset):
|
||||||
models.Index(fields=["code"]),
|
models.Index(fields=["code"]),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
for panel in self.signpanels.all():
|
||||||
|
panel.save()
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('assets:sign_assets_detail', kwargs={'asset_model': 'signpole', 'asset_id':self.id})
|
return reverse('assets:sign_assets_detail', kwargs={'asset_model': 'signpole', 'asset_id':self.id})
|
||||||
|
|
||||||
|
|
@ -165,6 +170,28 @@ class SignPanel(AbstractGeoAsset):
|
||||||
trafficlight_pole = models.ForeignKey("assets.TrafficLightPole", related_name="signpanels_attached", on_delete=models.SET_NULL, null=True, blank=True)
|
trafficlight_pole = models.ForeignKey("assets.TrafficLightPole", related_name="signpanels_attached", on_delete=models.SET_NULL, null=True, blank=True)
|
||||||
publiclighting_pole = models.ForeignKey("assets.PublicLightingPole", related_name="signpanels_attached", on_delete=models.SET_NULL, null=True, blank=True)
|
publiclighting_pole = models.ForeignKey("assets.PublicLightingPole", related_name="signpanels_attached", on_delete=models.SET_NULL, null=True, blank=True)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
support = self.pole or self.trafficlight_pole or self.publiclighting_pole
|
||||||
|
if support and getattr(support, 'geom', None):
|
||||||
|
self.geom = support.geom
|
||||||
|
self.lon = support.lon
|
||||||
|
self.lat = support.lat
|
||||||
|
self.geojson = support.geojson
|
||||||
|
if self.pole and self.pole.street and not self.street_id:
|
||||||
|
self.street = self.pole.street
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
support = self.pole or self.trafficlight_pole or self.publiclighting_pole
|
||||||
|
if support and getattr(support, 'geom', None):
|
||||||
|
self.geom = support.geom
|
||||||
|
self.lon = support.lon
|
||||||
|
self.lat = support.lat
|
||||||
|
self.geojson = support.geojson
|
||||||
|
if self.pole and self.pole.street and not self.street_id:
|
||||||
|
self.street = self.pole.street
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=["code"]),
|
models.Index(fields=["code"]),
|
||||||
|
|
|
||||||
|
|
@ -169,6 +169,30 @@ class StructureGeoAsset(AbstractGeoAsset):
|
||||||
structure = models.ForeignKey(Structure, on_delete=models.SET_NULL, related_name="assets", null=True, blank=True)
|
structure = models.ForeignKey(Structure, on_delete=models.SET_NULL, related_name="assets", null=True, blank=True)
|
||||||
parent_asset = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name="child_assets")
|
parent_asset = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name="child_assets")
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
if self.parent_asset and getattr(self.parent_asset, 'geom', None) and not self.geom:
|
||||||
|
self.geom = self.parent_asset.geom
|
||||||
|
self.lon = self.parent_asset.lon
|
||||||
|
self.lat = self.parent_asset.lat
|
||||||
|
self.geojson = self.parent_asset.geojson
|
||||||
|
if self.parent_asset and self.parent_asset.location and not self.location_id:
|
||||||
|
self.location = self.parent_asset.location
|
||||||
|
if self.parent_asset and self.parent_asset.structure and not self.structure_id:
|
||||||
|
self.structure = self.parent_asset.structure
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self.parent_asset and getattr(self.parent_asset, 'geom', None) and not self.geom:
|
||||||
|
self.geom = self.parent_asset.geom
|
||||||
|
self.lon = self.parent_asset.lon
|
||||||
|
self.lat = self.parent_asset.lat
|
||||||
|
self.geojson = self.parent_asset.geojson
|
||||||
|
if self.parent_asset and self.parent_asset.location and not self.location_id:
|
||||||
|
self.location = self.parent_asset.location
|
||||||
|
if self.parent_asset and self.parent_asset.structure and not self.structure_id:
|
||||||
|
self.structure = self.parent_asset.structure
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=["code"]),
|
models.Index(fields=["code"]),
|
||||||
|
|
|
||||||
|
|
@ -297,6 +297,18 @@ class TrafficLightPole(TrafficLightAbstractGeoAsset):
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
for radar in self.radars.all():
|
for radar in self.radars.all():
|
||||||
radar.save()
|
radar.save()
|
||||||
|
for lantern in self.lanterns.all():
|
||||||
|
lantern.save()
|
||||||
|
for acc in self.accessories.all():
|
||||||
|
acc.save()
|
||||||
|
for det in self.detectors.all():
|
||||||
|
det.save()
|
||||||
|
for sp in self.signpanels_attached.all():
|
||||||
|
sp.save()
|
||||||
|
for lf in self.light_fixtures.all():
|
||||||
|
lf.save()
|
||||||
|
for its in self.its_assets.all():
|
||||||
|
its.save()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Traffic light pole")
|
verbose_name = _("Traffic light pole")
|
||||||
|
|
@ -323,11 +335,31 @@ class TrafficLightLanternModel(AbstractAssetModel):
|
||||||
voltage = models.IntegerField(verbose_name=_("Tension (V)"))
|
voltage = models.IntegerField(verbose_name=_("Tension (V)"))
|
||||||
|
|
||||||
# Traffic Light Lantern
|
# Traffic Light Lantern
|
||||||
class TrafficLightLantern(TrafficLightAbstractAsset):
|
class TrafficLightLantern(TrafficLightAbstractGeoAsset):
|
||||||
model = models.ForeignKey(TrafficLightLanternModel, on_delete=models.PROTECT)
|
model = models.ForeignKey(TrafficLightLanternModel, on_delete=models.PROTECT)
|
||||||
pole = models.ForeignKey(TrafficLightPole, related_name="lanterns", on_delete=models.PROTECT)
|
pole = models.ForeignKey(TrafficLightPole, related_name="lanterns", on_delete=models.PROTECT)
|
||||||
orientation = models.FloatField(null=True, blank=True)
|
orientation = models.FloatField(null=True, blank=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def intersection(self):
|
||||||
|
return self.pole.intersection if self.pole else None
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
if self.pole and getattr(self.pole, 'geom', None):
|
||||||
|
self.geom = self.pole.geom
|
||||||
|
self.lon = self.pole.lon
|
||||||
|
self.lat = self.pole.lat
|
||||||
|
self.geojson = self.pole.geojson
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self.pole and getattr(self.pole, 'geom', None):
|
||||||
|
self.geom = self.pole.geom
|
||||||
|
self.lon = self.pole.lon
|
||||||
|
self.lat = self.pole.lat
|
||||||
|
self.geojson = self.pole.geojson
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Lanterne")
|
verbose_name = _("Lanterne")
|
||||||
verbose_name_plural = _("Lanternes")
|
verbose_name_plural = _("Lanternes")
|
||||||
|
|
@ -366,6 +398,17 @@ class TrafficLightController(TrafficLightAbstractGeoAsset):
|
||||||
def sorted_voltage_hardwares(self):
|
def sorted_voltage_hardwares(self):
|
||||||
return self.voltage_hardwares.order_by('code')
|
return self.voltage_hardwares.order_by('code')
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
for hw in self.hardwares.all():
|
||||||
|
hw.save()
|
||||||
|
for det in self.detectors.all():
|
||||||
|
det.save()
|
||||||
|
for net in self.network_hardwares.all():
|
||||||
|
net.save()
|
||||||
|
for volt in self.voltage_hardwares.all():
|
||||||
|
volt.save()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Contrôleur")
|
verbose_name = _("Contrôleur")
|
||||||
verbose_name_plural = _("Contrôleurs")
|
verbose_name_plural = _("Contrôleurs")
|
||||||
|
|
@ -378,6 +421,22 @@ class TrafficLightControllerHardware(TrafficLightAbstractGeoAsset):
|
||||||
name = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Nom du matériel"))
|
name = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Nom du matériel"))
|
||||||
description = models.TextField(null=True, blank=True, verbose_name=_("Description du matériel"))
|
description = models.TextField(null=True, blank=True, verbose_name=_("Description du matériel"))
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
if self.controller and getattr(self.controller, 'geom', None):
|
||||||
|
self.geom = self.controller.geom
|
||||||
|
self.lon = self.controller.lon
|
||||||
|
self.lat = self.controller.lat
|
||||||
|
self.geojson = self.controller.geojson
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self.controller and getattr(self.controller, 'geom', None):
|
||||||
|
self.geom = self.controller.geom
|
||||||
|
self.lon = self.controller.lon
|
||||||
|
self.lat = self.controller.lat
|
||||||
|
self.geojson = self.controller.geojson
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Hardware contrôleur")
|
verbose_name = _("Hardware contrôleur")
|
||||||
verbose_name_plural = _("Hardwares contrôleur")
|
verbose_name_plural = _("Hardwares contrôleur")
|
||||||
|
|
@ -409,6 +468,32 @@ class TrafficLightDetector(TrafficLightAbstractGeoAsset):
|
||||||
phases = ArrayField(models.CharField(max_length=50), null=True, blank=True, verbose_name=_("Phases associées"))
|
phases = ArrayField(models.CharField(max_length=50), null=True, blank=True, verbose_name=_("Phases associées"))
|
||||||
mentionned_on_plan = models.BooleanField(default=False, verbose_name=_("Mentionné sur le plan"))
|
mentionned_on_plan = models.BooleanField(default=False, verbose_name=_("Mentionné sur le plan"))
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
support = self.pole or self.controller
|
||||||
|
if support and getattr(support, 'geom', None):
|
||||||
|
self.geom = support.geom
|
||||||
|
self.lon = support.lon
|
||||||
|
self.lat = support.lat
|
||||||
|
self.geojson = support.geojson
|
||||||
|
if self.pole and self.pole.intersection and not self.intersection_id:
|
||||||
|
self.intersection = self.pole.intersection
|
||||||
|
elif self.controller and self.controller.intersection and not self.intersection_id:
|
||||||
|
self.intersection = self.controller.intersection
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
support = self.pole or self.controller
|
||||||
|
if support and getattr(support, 'geom', None):
|
||||||
|
self.geom = support.geom
|
||||||
|
self.lon = support.lon
|
||||||
|
self.lat = support.lat
|
||||||
|
self.geojson = support.geojson
|
||||||
|
if self.pole and self.pole.intersection and not self.intersection_id:
|
||||||
|
self.intersection = self.pole.intersection
|
||||||
|
elif self.controller and self.controller.intersection and not self.intersection_id:
|
||||||
|
self.intersection = self.controller.intersection
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Détecteur")
|
verbose_name = _("Détecteur")
|
||||||
verbose_name_plural = _("Détecteurs")
|
verbose_name_plural = _("Détecteurs")
|
||||||
|
|
@ -422,6 +507,26 @@ class TrafficLightNetworkHardware(TrafficLightAbstractGeoAsset):
|
||||||
controller = models.ForeignKey(TrafficLightController, related_name="network_hardwares", on_delete=models.PROTECT, null=True, blank=True)
|
controller = models.ForeignKey(TrafficLightController, related_name="network_hardwares", on_delete=models.PROTECT, null=True, blank=True)
|
||||||
mentionned_on_plan = models.BooleanField(default=False, verbose_name=_("Mentionné sur le plan"))
|
mentionned_on_plan = models.BooleanField(default=False, verbose_name=_("Mentionné sur le plan"))
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
if self.controller and getattr(self.controller, 'geom', None):
|
||||||
|
self.geom = self.controller.geom
|
||||||
|
self.lon = self.controller.lon
|
||||||
|
self.lat = self.controller.lat
|
||||||
|
self.geojson = self.controller.geojson
|
||||||
|
if self.controller and self.controller.intersection and not self.intersection_id:
|
||||||
|
self.intersection = self.controller.intersection
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self.controller and getattr(self.controller, 'geom', None):
|
||||||
|
self.geom = self.controller.geom
|
||||||
|
self.lon = self.controller.lon
|
||||||
|
self.lat = self.controller.lat
|
||||||
|
self.geojson = self.controller.geojson
|
||||||
|
if self.controller and self.controller.intersection and not self.intersection_id:
|
||||||
|
self.intersection = self.controller.intersection
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name or f"Matériel {self.id}"
|
return self.name or f"Matériel {self.id}"
|
||||||
|
|
||||||
|
|
@ -438,6 +543,26 @@ class TrafficLightVoltageHardware(TrafficLightAbstractGeoAsset):
|
||||||
controller = models.ForeignKey(TrafficLightController, related_name="voltage_hardwares", on_delete=models.PROTECT, null=True, blank=True)
|
controller = models.ForeignKey(TrafficLightController, related_name="voltage_hardwares", on_delete=models.PROTECT, null=True, blank=True)
|
||||||
mentionned_on_plan = models.BooleanField(default=False, verbose_name=_("Mentionné sur le plan"))
|
mentionned_on_plan = models.BooleanField(default=False, verbose_name=_("Mentionné sur le plan"))
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
if self.controller and getattr(self.controller, 'geom', None):
|
||||||
|
self.geom = self.controller.geom
|
||||||
|
self.lon = self.controller.lon
|
||||||
|
self.lat = self.controller.lat
|
||||||
|
self.geojson = self.controller.geojson
|
||||||
|
if self.controller and self.controller.intersection and not self.intersection_id:
|
||||||
|
self.intersection = self.controller.intersection
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self.controller and getattr(self.controller, 'geom', None):
|
||||||
|
self.geom = self.controller.geom
|
||||||
|
self.lon = self.controller.lon
|
||||||
|
self.lat = self.controller.lat
|
||||||
|
self.geojson = self.controller.geojson
|
||||||
|
if self.controller and self.controller.intersection and not self.intersection_id:
|
||||||
|
self.intersection = self.controller.intersection
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name or f"Matériel {self.id}"
|
return self.name or f"Matériel {self.id}"
|
||||||
|
|
||||||
|
|
@ -494,6 +619,11 @@ class TrafficLightElectricalCabinet(TrafficLightAbstractGeoAsset):
|
||||||
visual_conformity = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Conformité visuelle"))
|
visual_conformity = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Conformité visuelle"))
|
||||||
cleanliness = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Propreté"))
|
cleanliness = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Propreté"))
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
for content in self.contents.all():
|
||||||
|
content.save()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Armoire")
|
verbose_name = _("Armoire")
|
||||||
verbose_name_plural = _("Armoires")
|
verbose_name_plural = _("Armoires")
|
||||||
|
|
@ -509,6 +639,22 @@ class TrafficLightElectricalCabinetContent(TrafficLightAbstractGeoAsset):
|
||||||
number = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Numéro de référence"))
|
number = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Numéro de référence"))
|
||||||
ean = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Code EAN"))
|
ean = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Code EAN"))
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
if self.cabinet and getattr(self.cabinet, 'geom', None):
|
||||||
|
self.geom = self.cabinet.geom
|
||||||
|
self.lon = self.cabinet.lon
|
||||||
|
self.lat = self.cabinet.lat
|
||||||
|
self.geojson = self.cabinet.geojson
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self.cabinet and getattr(self.cabinet, 'geom', None):
|
||||||
|
self.geom = self.cabinet.geom
|
||||||
|
self.lon = self.cabinet.lon
|
||||||
|
self.lat = self.cabinet.lat
|
||||||
|
self.geojson = self.cabinet.geojson
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Contenu d'armoire")
|
verbose_name = _("Contenu d'armoire")
|
||||||
verbose_name_plural = _("Contenus d'armoires")
|
verbose_name_plural = _("Contenus d'armoires")
|
||||||
|
|
@ -525,6 +671,26 @@ class TrafficLightAccessory(TrafficLightAbstractGeoAsset):
|
||||||
intersection = models.ForeignKey(TrafficLightIntersection, related_name="accessories", on_delete=models.PROTECT, null=True, blank=True)
|
intersection = models.ForeignKey(TrafficLightIntersection, related_name="accessories", on_delete=models.PROTECT, null=True, blank=True)
|
||||||
orientation = models.FloatField(null=True, blank=True)
|
orientation = models.FloatField(null=True, blank=True)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
if self.pole and getattr(self.pole, 'geom', None):
|
||||||
|
self.geom = self.pole.geom
|
||||||
|
self.lon = self.pole.lon
|
||||||
|
self.lat = self.pole.lat
|
||||||
|
self.geojson = self.pole.geojson
|
||||||
|
if self.pole and self.pole.intersection and not self.intersection_id:
|
||||||
|
self.intersection = self.pole.intersection
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self.pole and getattr(self.pole, 'geom', None):
|
||||||
|
self.geom = self.pole.geom
|
||||||
|
self.lon = self.pole.lon
|
||||||
|
self.lat = self.pole.lat
|
||||||
|
self.geojson = self.pole.geojson
|
||||||
|
if self.pole and self.pole.intersection and not self.intersection_id:
|
||||||
|
self.intersection = self.pole.intersection
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Accessoire")
|
verbose_name = _("Accessoire")
|
||||||
verbose_name_plural = _("Accessoires")
|
verbose_name_plural = _("Accessoires")
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% if location or can_edit_location %}
|
{% if location or parent_support or can_edit_location %}
|
||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||||
<h5 class="mb-0">
|
<h5 class="mb-0">
|
||||||
|
|
@ -8,19 +8,39 @@
|
||||||
{% translate "Rue associée" %}
|
{% translate "Rue associée" %}
|
||||||
{% elif location_type == 'intersection' %}
|
{% elif location_type == 'intersection' %}
|
||||||
{% translate "Carrefour associé" %}
|
{% translate "Carrefour associé" %}
|
||||||
|
{% elif location_type == 'building' %}
|
||||||
|
{% translate "Bâtiment associé" %}
|
||||||
|
{% elif location_type == 'site' %}
|
||||||
|
{% translate "Site associé" %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% translate "Localisation associée" %}
|
{% translate "Localisation associée" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h5>
|
</h5>
|
||||||
{% if can_edit_location %}
|
{% if can_edit_location and not parent_support %}
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary d-none" id="changeLocationBtn" data-bs-toggle="modal" data-bs-target="#changeLocationModal" data-edit-mode-only="true">
|
<button type="button" class="btn btn-sm btn-outline-primary d-none" id="changeLocationBtn" data-bs-toggle="modal" data-bs-target="#changeLocationModal" data-edit-mode-only="true">
|
||||||
<i class="bi bi-pencil me-1"></i>{% translate "Changer" %}
|
<i class="bi bi-pencil me-1"></i>{% translate "Changer" %}
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
{% if parent_support %}
|
||||||
|
<div class="mb-2">
|
||||||
|
<small class="text-muted d-block">{% translate "Support / Équipement parent" %} :</small>
|
||||||
|
{% if parent_support.get_absolute_url %}
|
||||||
|
<a href="{{ parent_support.get_absolute_url }}" class="fw-bold text-decoration-none"
|
||||||
|
title="{% translate 'Voir le détail du support' %}">
|
||||||
|
<i class="bi bi-layers-fill me-1 text-primary"></i>{{ parent_support.code }}
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<strong>{{ parent_support.code }}</strong>
|
||||||
|
{% endif %}
|
||||||
|
- {{ parent_support.get_name }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if location %}
|
{% if location %}
|
||||||
<p class="mb-2">
|
<div>
|
||||||
|
{% if parent_support %}<small class="text-muted d-block">{% translate "Localisation" %} :</small>{% endif %}
|
||||||
{% if location.get_absolute_url %}
|
{% if location.get_absolute_url %}
|
||||||
<a href="{{ location.get_absolute_url }}" class="fw-bold text-decoration-none"
|
<a href="{{ location.get_absolute_url }}" class="fw-bold text-decoration-none"
|
||||||
title="{% translate 'Voir le détail de la localisation' %}">
|
title="{% translate 'Voir le détail de la localisation' %}">
|
||||||
|
|
@ -30,8 +50,8 @@
|
||||||
<strong>{{ location.code }}</strong>
|
<strong>{{ location.code }}</strong>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
- {{ location.get_name }}
|
- {{ location.get_name }}
|
||||||
</p>
|
</div>
|
||||||
{% else %}
|
{% elif not parent_support %}
|
||||||
<p class="text-muted mb-0">{% translate "Aucune localisation associée" %}</p>
|
<p class="text-muted mb-0">{% translate "Aucune localisation associée" %}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -162,22 +162,45 @@ def get_asset_specific_fields(asset):
|
||||||
@register.inclusion_tag('assets/components/asset_location_link.html', takes_context=True)
|
@register.inclusion_tag('assets/components/asset_location_link.html', takes_context=True)
|
||||||
def asset_location_link(context, asset):
|
def asset_location_link(context, asset):
|
||||||
"""
|
"""
|
||||||
Display a link to the location associated with an asset.
|
Display a link to the location and/or parent support associated with an asset.
|
||||||
Works with assets that have a 'street', 'location', or 'intersection' ForeignKey.
|
Works with assets that have a direct location ('street', 'location', 'intersection', etc.)
|
||||||
|
or are attached to a parent support (pole, controller, cabinet, etc.).
|
||||||
"""
|
"""
|
||||||
# Try different possible location field names
|
|
||||||
location = None
|
location = None
|
||||||
location_type = None
|
location_type = None
|
||||||
|
|
||||||
if hasattr(asset, 'street') and getattr(asset, 'street', None):
|
if hasattr(asset, 'get_location'):
|
||||||
location = getattr(asset, 'street', None)
|
location = asset.get_location()
|
||||||
location_type = 'street'
|
|
||||||
elif hasattr(asset, 'location') and getattr(asset, 'location', None):
|
if not location:
|
||||||
location = getattr(asset, 'location', None)
|
if hasattr(asset, 'street') and getattr(asset, 'street', None):
|
||||||
location_type = 'location'
|
location = getattr(asset, 'street', None)
|
||||||
elif hasattr(asset, 'intersection') and getattr(asset, 'intersection', None):
|
elif hasattr(asset, 'location') and getattr(asset, 'location', None):
|
||||||
location = getattr(asset, 'intersection', None)
|
location = getattr(asset, 'location', None)
|
||||||
location_type = 'intersection'
|
elif hasattr(asset, 'intersection') and getattr(asset, 'intersection', None):
|
||||||
|
location = getattr(asset, 'intersection', None)
|
||||||
|
elif hasattr(asset, 'building') and getattr(asset, 'building', None):
|
||||||
|
location = getattr(asset, 'building', None)
|
||||||
|
elif hasattr(asset, 'site') and getattr(asset, 'site', None):
|
||||||
|
location = getattr(asset, 'site', None)
|
||||||
|
|
||||||
|
if location:
|
||||||
|
loc_model = getattr(location._meta, 'model_name', '').lower()
|
||||||
|
if 'street' in loc_model:
|
||||||
|
location_type = 'street'
|
||||||
|
elif 'intersection' in loc_model:
|
||||||
|
location_type = 'intersection'
|
||||||
|
elif 'building' in loc_model:
|
||||||
|
location_type = 'building'
|
||||||
|
elif 'outdoor' in loc_model:
|
||||||
|
location_type = 'site'
|
||||||
|
else:
|
||||||
|
location_type = 'location'
|
||||||
|
|
||||||
|
# Parent support (pole, controller, cabinet, parent_asset...)
|
||||||
|
parent_support = None
|
||||||
|
if hasattr(asset, 'get_parent_support'):
|
||||||
|
parent_support = asset.get_parent_support()
|
||||||
|
|
||||||
# Determine if user can edit location (similar to model editing)
|
# Determine if user can edit location (similar to model editing)
|
||||||
can_edit_location = False
|
can_edit_location = False
|
||||||
|
|
@ -198,12 +221,34 @@ def asset_location_link(context, asset):
|
||||||
thematic_code = 'trafficlights'
|
thematic_code = 'trafficlights'
|
||||||
elif 'publiclighting' in model_name.lower():
|
elif 'publiclighting' in model_name.lower():
|
||||||
thematic_code = 'publiclighting'
|
thematic_code = 'publiclighting'
|
||||||
# Add more thematic mappings as needed
|
elif 'sign' in model_name.lower():
|
||||||
|
thematic_code = 'sign'
|
||||||
|
elif 'clean' in model_name.lower():
|
||||||
|
thematic_code = 'clean'
|
||||||
|
elif 'nature' in model_name.lower():
|
||||||
|
thematic_code = 'nature'
|
||||||
|
elif 'building' in model_name.lower():
|
||||||
|
thematic_code = 'buildings'
|
||||||
|
elif 'outdoor' in model_name.lower():
|
||||||
|
thematic_code = 'outdoor'
|
||||||
|
elif 'its' in model_name.lower():
|
||||||
|
thematic_code = 'its'
|
||||||
|
elif 'controlcenter' in model_name.lower():
|
||||||
|
thematic_code = 'controlcenters'
|
||||||
|
elif 'parking' in model_name.lower():
|
||||||
|
thematic_code = 'parking'
|
||||||
|
elif 'road' in model_name.lower():
|
||||||
|
thematic_code = 'roads'
|
||||||
|
elif 'artwork' in model_name.lower():
|
||||||
|
thematic_code = 'artworks'
|
||||||
|
elif 'water' in model_name.lower():
|
||||||
|
thematic_code = 'water'
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'asset': asset,
|
'asset': asset,
|
||||||
'location': location,
|
'location': location,
|
||||||
'location_type': location_type,
|
'location_type': location_type,
|
||||||
|
'parent_support': parent_support,
|
||||||
'can_edit_location': can_edit_location,
|
'can_edit_location': can_edit_location,
|
||||||
'thematic_code': thematic_code,
|
'thematic_code': thematic_code,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2602,8 +2602,161 @@ class GeoSyncMixinTest(TestCase):
|
||||||
self.assertIn('4.36', pole.geojson)
|
self.assertIn('4.36', pole.geojson)
|
||||||
|
|
||||||
|
|
||||||
|
class SyncGeoJsonCommandTest(TestCase):
|
||||||
|
def test_sync_all_geojson_command(self):
|
||||||
|
from io import StringIO
|
||||||
|
from django.core.management import call_command
|
||||||
|
from django.contrib.gis.geos import Point
|
||||||
|
from assets.models.publiclighting import PublicLightingPole, PublicLightingPoleModel, PublicLightingStreet
|
||||||
|
street = PublicLightingStreet.objects.create(code="ST-TEST", name_fr="Rue Test")
|
||||||
|
pole_model = PublicLightingPoleModel.objects.create(code="PM-TEST", name_fr="Model Test")
|
||||||
|
pt = Point(4.3517, 50.8467, srid=4326).transform(3812, clone=True)
|
||||||
|
pole = PublicLightingPole.objects.create(
|
||||||
|
code="TEST-SYNC-GEOJSON",
|
||||||
|
street=street,
|
||||||
|
model=pole_model,
|
||||||
|
geom=pt,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
PublicLightingPole.objects.filter(id=pole.id).update(geojson=None)
|
||||||
|
|
||||||
|
pole.refresh_from_db()
|
||||||
|
self.assertIsNone(pole.geojson)
|
||||||
|
|
||||||
|
out = StringIO()
|
||||||
|
call_command("sync_all_geojson", "--dry-run", stdout=out)
|
||||||
|
self.assertIn("[DRY-RUN] Would update", out.getvalue())
|
||||||
|
|
||||||
|
pole.refresh_from_db()
|
||||||
|
self.assertIsNone(pole.geojson)
|
||||||
|
|
||||||
|
out = StringIO()
|
||||||
|
call_command("sync_all_geojson", stdout=out)
|
||||||
|
self.assertIn("Successfully updated", out.getvalue())
|
||||||
|
|
||||||
|
pole.refresh_from_db()
|
||||||
|
self.assertIsNotNone(pole.geojson)
|
||||||
|
self.assertIn("4.3517", pole.geojson)
|
||||||
|
|
||||||
|
|
||||||
|
class ParentChildAssetLocationAndGeoSyncTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
from django.contrib.gis.geos import Point
|
||||||
|
from assets.models.trafficlights import (
|
||||||
|
TrafficLightIntersection,
|
||||||
|
TrafficLightPole,
|
||||||
|
TrafficLightPoleModel,
|
||||||
|
TrafficLightLantern,
|
||||||
|
TrafficLightLanternModel,
|
||||||
|
)
|
||||||
|
from assets.models.sign import SignStreet, SignPole, SignPoleModel, SignPanel
|
||||||
|
from assets.models.publiclighting import (
|
||||||
|
PublicLightingStreet,
|
||||||
|
PublicLightingPole,
|
||||||
|
PublicLightingPoleModel,
|
||||||
|
PublicLightingLightFixture,
|
||||||
|
PublicLightingLightFixtureModel,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.point_3812 = Point(649529.6, 669829.8, srid=3812)
|
||||||
|
|
||||||
|
# Traffic light setup
|
||||||
|
self.intersection = TrafficLightIntersection.objects.create(
|
||||||
|
code="TL-INT-01",
|
||||||
|
name_fr="Carrefour Test",
|
||||||
|
geom=self.point_3812
|
||||||
|
)
|
||||||
|
self.tl_pole_model = TrafficLightPoleModel.objects.create(code="TLPM-01", name_fr="Modèle Poteau TL")
|
||||||
|
self.tl_pole = TrafficLightPole.objects.create(
|
||||||
|
code="TL-POL-01",
|
||||||
|
intersection=self.intersection,
|
||||||
|
model=self.tl_pole_model,
|
||||||
|
geom=self.point_3812
|
||||||
|
)
|
||||||
|
self.lantern_model = TrafficLightLanternModel.objects.create(
|
||||||
|
code="LAN-MOD-01",
|
||||||
|
name_fr="Modèle Lanterne",
|
||||||
|
voltage=230
|
||||||
|
)
|
||||||
|
|
||||||
|
# Sign setup
|
||||||
|
self.sign_street = SignStreet.objects.create(code="STR-01", name_fr="Rue du Test")
|
||||||
|
self.sign_pole_model = SignPoleModel.objects.create(code="SPM-01", name_fr="Modèle Poteau Sign")
|
||||||
|
self.sign_pole = SignPole.objects.create(
|
||||||
|
code="SP-01",
|
||||||
|
street=self.sign_street,
|
||||||
|
model=self.sign_pole_model,
|
||||||
|
geom=self.point_3812
|
||||||
|
)
|
||||||
|
|
||||||
|
# Public lighting setup
|
||||||
|
self.pl_street = PublicLightingStreet.objects.create(code="PL-STR-01", name_fr="Rue Eclairage")
|
||||||
|
self.pl_pole_model = PublicLightingPoleModel.objects.create(code="PLPM-01", name_fr="Modèle Poteau PL")
|
||||||
|
self.pl_pole = PublicLightingPole.objects.create(
|
||||||
|
code="PL-POL-01",
|
||||||
|
street=self.pl_street,
|
||||||
|
model=self.pl_pole_model,
|
||||||
|
geom=self.point_3812
|
||||||
|
)
|
||||||
|
self.fixture_model = PublicLightingLightFixtureModel.objects.create(code="LFM-01", name_fr="Modèle Luminaire")
|
||||||
|
|
||||||
|
def test_lantern_inherits_coordinates_and_location_from_pole(self):
|
||||||
|
from assets.models.trafficlights import TrafficLightLantern
|
||||||
|
lantern = TrafficLightLantern.objects.create(
|
||||||
|
code="TL-LAN-01",
|
||||||
|
pole=self.tl_pole,
|
||||||
|
model=self.lantern_model
|
||||||
|
)
|
||||||
|
lantern.refresh_from_db()
|
||||||
|
self.assertIsNotNone(lantern.geom)
|
||||||
|
self.assertEqual(lantern.geom, self.tl_pole.geom)
|
||||||
|
self.assertIsNotNone(lantern.lon)
|
||||||
|
self.assertIsNotNone(lantern.lat)
|
||||||
|
self.assertIsNotNone(lantern.geojson)
|
||||||
|
self.assertEqual(lantern.intersection, self.intersection)
|
||||||
|
self.assertEqual(lantern.get_location(), self.intersection)
|
||||||
|
self.assertEqual(lantern.get_parent_support(), self.tl_pole)
|
||||||
|
|
||||||
|
def test_signpanel_attached_to_trafficlight_pole_inherits_coords(self):
|
||||||
|
from assets.models.sign import SignPanel
|
||||||
|
panel = SignPanel.objects.create(
|
||||||
|
code="PANEL-01",
|
||||||
|
trafficlight_pole=self.tl_pole
|
||||||
|
)
|
||||||
|
panel.refresh_from_db()
|
||||||
|
self.assertIsNotNone(panel.geom)
|
||||||
|
self.assertEqual(panel.geom, self.tl_pole.geom)
|
||||||
|
self.assertEqual(panel.get_parent_support(), self.tl_pole)
|
||||||
|
self.assertEqual(panel.get_location(), self.intersection)
|
||||||
|
|
||||||
|
def test_publiclighting_fixture_inherits_coords_and_street(self):
|
||||||
|
from assets.models.publiclighting import PublicLightingLightFixture
|
||||||
|
fixture = PublicLightingLightFixture.objects.create(
|
||||||
|
code="FIXTURE-01",
|
||||||
|
pole=self.pl_pole,
|
||||||
|
street=self.pl_street,
|
||||||
|
model=self.fixture_model
|
||||||
|
)
|
||||||
|
fixture.refresh_from_db()
|
||||||
|
self.assertIsNotNone(fixture.geom)
|
||||||
|
self.assertEqual(fixture.geom, self.pl_pole.geom)
|
||||||
|
self.assertEqual(fixture.get_parent_support(), self.pl_pole)
|
||||||
|
self.assertEqual(fixture.get_location(), self.pl_street)
|
||||||
|
|
||||||
|
def test_pole_save_cascades_to_children(self):
|
||||||
|
from django.contrib.gis.geos import Point
|
||||||
|
from assets.models.trafficlights import TrafficLightLantern
|
||||||
|
|
||||||
|
lantern = TrafficLightLantern.objects.create(
|
||||||
|
code="TL-LAN-02",
|
||||||
|
pole=self.tl_pole,
|
||||||
|
model=self.lantern_model
|
||||||
|
)
|
||||||
|
|
||||||
|
new_point = Point(650000.0, 670000.0, srid=3812)
|
||||||
|
self.tl_pole.geom = new_point
|
||||||
|
self.tl_pole.save()
|
||||||
|
|
||||||
|
lantern.refresh_from_db()
|
||||||
|
self.assertEqual(lantern.geom, new_point)
|
||||||
|
|
|
||||||
|
|
@ -2437,7 +2437,9 @@ def create_intervention_for_asset(request, asset_model, asset_id):
|
||||||
content_type=content_type,
|
content_type=content_type,
|
||||||
object_id=asset.pk
|
object_id=asset.pk
|
||||||
)
|
)
|
||||||
loc_obj = getattr(asset, 'street', None) or getattr(asset, 'location', None) or getattr(asset, 'intersection', None)
|
loc_obj = (
|
||||||
|
asset.get_location() if hasattr(asset, 'get_location') else None
|
||||||
|
) or getattr(asset, 'street', None) or getattr(asset, 'location', None) or getattr(asset, 'intersection', None)
|
||||||
if loc_obj:
|
if loc_obj:
|
||||||
loc_ct = ContentType.objects.get_for_model(loc_obj)
|
loc_ct = ContentType.objects.get_for_model(loc_obj)
|
||||||
InterventionLocation.objects.get_or_create(
|
InterventionLocation.objects.get_or_create(
|
||||||
|
|
|
||||||
|
|
@ -1289,6 +1289,10 @@ def trafficlights_assets_detail(request, asset_model, asset_id):
|
||||||
# Build GeoJSON of sibling assets at the same intersection (excluding current asset)
|
# Build GeoJSON of sibling assets at the same intersection (excluding current asset)
|
||||||
intersection_assets_geojson = None
|
intersection_assets_geojson = None
|
||||||
asset_intersection = getattr(asset, 'intersection', None)
|
asset_intersection = getattr(asset, 'intersection', None)
|
||||||
|
if not asset_intersection and hasattr(asset, 'get_location'):
|
||||||
|
loc = asset.get_location()
|
||||||
|
if loc and getattr(loc._meta, 'model_name', '') == 'trafficlightintersection':
|
||||||
|
asset_intersection = loc
|
||||||
if asset_intersection:
|
if asset_intersection:
|
||||||
assets_features = []
|
assets_features = []
|
||||||
active_assets = asset_intersection.get_active_assets()
|
active_assets = asset_intersection.get_active_assets()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue