feat: implement traffic light asset management models, views, and cross-plan parsing utilities

This commit is contained in:
Karl Determe 2026-08-26 09:33:40 +02:00
parent ba268f5e2a
commit 4135c6ef47
4 changed files with 117 additions and 5 deletions

View file

@ -498,8 +498,18 @@ class TrafficLightDetector(TrafficLightAbstractGeoAsset):
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"))
def get_name(self):
return self.name or self.name_fr or super().get_name()
def clean(self):
super().clean()
if self.name:
if not self.name_fr:
self.name_fr = self.name
if not self.name_nl:
self.name_nl = self.name
elif self.name_fr and not self.name:
self.name = self.name_fr
support = self.pole or self.controller
if support and getattr(support, 'geom', None):
self.geom = support.geom
@ -512,6 +522,13 @@ class TrafficLightDetector(TrafficLightAbstractGeoAsset):
self.intersection = self.controller.intersection
def save(self, *args, **kwargs):
if self.name:
if not self.name_fr:
self.name_fr = self.name
if not self.name_nl:
self.name_nl = self.name
elif self.name_fr and not self.name:
self.name = self.name_fr
support = self.pole or self.controller
if support and getattr(support, 'geom', None):
self.geom = support.geom

View file

@ -1324,16 +1324,19 @@ class TrafficLightCrossPlanImportTest(TestCase):
self.assertEqual(old_lantern.replaced_by, new_active)
def test_modify_action_updates_code_and_properties(self):
from assets.models import TrafficLightPole, TrafficLightLantern, TrafficLightDetector
from assets.models import TrafficLightPole, TrafficLightLantern, TrafficLightDetector, TrafficLightPoleModel, TrafficLightLanternModel
pole_model = TrafficLightPoleModel.objects.first()
lantern_model = TrafficLightLanternModel.objects.first()
pole = TrafficLightPole.objects.create(
intersection=self.intersection,
code="SWB01_A01",
model=pole_model,
status="active"
)
old_lantern = TrafficLightLantern.objects.create(
pole=pole,
code="SWB01_A01_LAN01",
model=self.lantern_model,
model=lantern_model,
status="active"
)
self.client.force_login(self.user)
@ -1369,18 +1372,27 @@ class TrafficLightCrossPlanImportTest(TestCase):
self.assertEqual(old_lantern.code, "SWB01_A01_LAN01_T1")
def test_other_associated_assets_preview_and_integration(self):
from assets.models import TrafficLightPole, TrafficLightController, TrafficLightControllerHardware, TrafficLightAccessory
from assets.models.sign import SignPanel
from assets.models.its import ITSGeoAsset
from assets.models import (
TrafficLightPole, TrafficLightController, TrafficLightControllerHardware,
TrafficLightAccessory, TrafficLightPoleModel, TrafficLightControllerModel
)
from assets.models.sign import SignPanel, SignPanelModel
from assets.models.its import ITSGeoAsset, ITSAssetModel
pole_model = TrafficLightPoleModel.objects.first()
ctrl_model, _ = TrafficLightControllerModel.objects.get_or_create(code="ctrl_mod", defaults={"name_fr": "Model Ctrl"})
sign_model, _ = SignPanelModel.objects.get_or_create(code="sp_mod", defaults={"name_fr": "Model SP"})
its_model, _ = ITSAssetModel.objects.get_or_create(code="its_mod", defaults={"name_fr": "Model ITS"})
pole = TrafficLightPole.objects.create(
intersection=self.intersection,
code="SWB01_A01",
model=pole_model,
status="active"
)
ctrl = TrafficLightController.objects.create(
intersection=self.intersection,
code="SWB01_C01",
model=ctrl_model,
status="active"
)
hw = TrafficLightControllerHardware.objects.create(
@ -1391,11 +1403,13 @@ class TrafficLightCrossPlanImportTest(TestCase):
sign = SignPanel.objects.create(
trafficlight_pole=pole,
code="SWB01_SP01",
model=sign_model,
status="active"
)
its = ITSGeoAsset.objects.create(
trafficlights_pole=pole,
code="SWB01_ITS01",
model=its_model,
status="active"
)
acc = TrafficLightAccessory.objects.create(
@ -2232,6 +2246,73 @@ class TrafficLightCrossPlanImportTest(TestCase):
if os.path.exists(tmp_path):
os.remove(tmp_path)
def test_parse_cross_plan_radar_power_ignored_and_detector_names(self):
import openpyxl
import tempfile
from assets.utils.cross_plan_parser import parse_cross_plan
from assets.models import TrafficLightDetector
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "kruisjesplan"
ws.cell(row=1, column=3, value="sleutel:")
ws.cell(row=1, column=4, value="SAT110")
ws.cell(row=2, column=7, value="Câble 01 SVAVB 30x1,5mm²")
ws.cell(row=4, column=1, value="Klem controller")
ws.cell(row=4, column=2, value="Klemnummer")
ws.cell(row=4, column=3, value="Richting")
ws.cell(row=4, column=4, value="Kringen")
ws.cell(row=5, column=5, value="A01")
# Row 6: RADAR power line -> should be IGNORED
ws.cell(row=6, column=2, value=1)
ws.cell(row=6, column=3, value="RADAR")
ws.cell(row=6, column=4, value="RADAR")
ws.cell(row=6, column=5, value="X")
# Row 7: RB2 radar detector -> should be parsed as detector
ws.cell(row=7, column=2, value=2)
ws.cell(row=7, column=3, value="RB2")
ws.cell(row=7, column=4, value="RB2")
ws.cell(row=7, column=5, value="X")
# Row 8: Push button BPb1 -> should be parsed as push button with name and phase
ws.cell(row=8, column=2, value=3)
ws.cell(row=8, column=3, value="BPb1")
ws.cell(row=8, column=4, value="DK b1")
ws.cell(row=8, column=5, value="X")
with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as tmp:
tmp_path = tmp.name
wb.save(tmp_path)
try:
deduced = parse_cross_plan(tmp_path)
detectors = deduced["detectors"]
self.assertEqual(len(detectors), 2)
# Verify RADAR was NOT included
codes = [d["code"] for d in detectors]
self.assertNotIn("SAT110_A01_RADAR", codes)
self.assertIn("SAT110_A01_RB2", codes)
self.assertIn("SAT110_A01_BPb1", codes)
bp = next(d for d in detectors if d["code"] == "SAT110_A01_BPb1")
self.assertEqual(bp["phases"], ["b"])
self.assertIn("bouton poussoir", bp["name"])
# Test model get_name synchronization
from assets.models import TrafficLightDetectorModel
det_model = TrafficLightDetectorModel.objects.first()
det = TrafficLightDetector(intersection=self.intersection, name=bp["name"], code=bp["code"], model=det_model)
det.save()
self.assertEqual(det.name_fr, bp["name"])
self.assertEqual(det.get_name(), bp["name"])
finally:
import os
if os.path.exists(tmp_path):
os.remove(tmp_path)
class AssetDefaultPositionTest(TestCase):
def test_assign_default_position_hierarchy(self):

View file

@ -287,6 +287,10 @@ def parse_cross_plan(excel_path):
normalized_upper = richting.upper().replace(" ", "")
# Check if RADAR power supply (Alimentation radar, not a detector)
if normalized_upper == "RADAR" or normalized_upper.startswith("RADAR") or (bool(re.search(r'\bradar\b', richting, re.IGNORECASE)) and not bool(re.match(r'^(?:RA|RB|RC|RD|RM|RP)\d*$', normalized_upper, re.IGNORECASE))):
continue
if conn.get("is_detector", False):
is_detector = True
elif normalized_upper in ("RA", "RAB", "RB", "RC", "RD", "VD", "VC", "VT1", "VT2") or normalized_upper.startswith(("RM", "RP", "RA", "RB", "RC", "RD", "VD", "VC", "VT")):

View file

@ -4751,6 +4751,8 @@ def integrate_cross_plan(request, intersection_id):
pole=pole,
code=dd["code"],
name=custom_name,
name_fr=custom_name,
name_nl=custom_name,
model=db_model,
phases=dd["phases"],
status='active',
@ -5067,6 +5069,8 @@ def integrate_cross_plan(request, intersection_id):
pole=pole,
code=code,
name=custom_name,
name_fr=custom_name,
name_nl=custom_name,
model=db_model,
phases=dd["phases"],
status='active',
@ -5079,6 +5083,8 @@ def integrate_cross_plan(request, intersection_id):
existing.pole = pole
existing.model = db_model
existing.name = custom_name
existing.name_fr = custom_name
existing.name_nl = custom_name
existing.phases = dd["phases"]
existing.updated_by = request.user
existing.save()
@ -5090,6 +5096,8 @@ def integrate_cross_plan(request, intersection_id):
pole=pole,
code=code,
name=custom_name,
name_fr=custom_name,
name_nl=custom_name,
model=db_model,
phases=dd["phases"],
status='active',
@ -5102,6 +5110,8 @@ def integrate_cross_plan(request, intersection_id):
# 'keep'
if existing:
existing.name = custom_name
existing.name_fr = custom_name
existing.name_nl = custom_name
existing.save()
for key in request.POST.keys():