feat: implement traffic light asset management models, views, and cross-plan parsing utilities
This commit is contained in:
parent
ba268f5e2a
commit
4135c6ef47
4 changed files with 117 additions and 5 deletions
|
|
@ -498,8 +498,18 @@ 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 get_name(self):
|
||||||
|
return self.name or self.name_fr or super().get_name()
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
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
|
support = self.pole or self.controller
|
||||||
if support and getattr(support, 'geom', None):
|
if support and getattr(support, 'geom', None):
|
||||||
self.geom = support.geom
|
self.geom = support.geom
|
||||||
|
|
@ -512,6 +522,13 @@ class TrafficLightDetector(TrafficLightAbstractGeoAsset):
|
||||||
self.intersection = self.controller.intersection
|
self.intersection = self.controller.intersection
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
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
|
support = self.pole or self.controller
|
||||||
if support and getattr(support, 'geom', None):
|
if support and getattr(support, 'geom', None):
|
||||||
self.geom = support.geom
|
self.geom = support.geom
|
||||||
|
|
|
||||||
|
|
@ -1324,16 +1324,19 @@ class TrafficLightCrossPlanImportTest(TestCase):
|
||||||
self.assertEqual(old_lantern.replaced_by, new_active)
|
self.assertEqual(old_lantern.replaced_by, new_active)
|
||||||
|
|
||||||
def test_modify_action_updates_code_and_properties(self):
|
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(
|
pole = TrafficLightPole.objects.create(
|
||||||
intersection=self.intersection,
|
intersection=self.intersection,
|
||||||
code="SWB01_A01",
|
code="SWB01_A01",
|
||||||
|
model=pole_model,
|
||||||
status="active"
|
status="active"
|
||||||
)
|
)
|
||||||
old_lantern = TrafficLightLantern.objects.create(
|
old_lantern = TrafficLightLantern.objects.create(
|
||||||
pole=pole,
|
pole=pole,
|
||||||
code="SWB01_A01_LAN01",
|
code="SWB01_A01_LAN01",
|
||||||
model=self.lantern_model,
|
model=lantern_model,
|
||||||
status="active"
|
status="active"
|
||||||
)
|
)
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
|
@ -1369,18 +1372,27 @@ class TrafficLightCrossPlanImportTest(TestCase):
|
||||||
self.assertEqual(old_lantern.code, "SWB01_A01_LAN01_T1")
|
self.assertEqual(old_lantern.code, "SWB01_A01_LAN01_T1")
|
||||||
|
|
||||||
def test_other_associated_assets_preview_and_integration(self):
|
def test_other_associated_assets_preview_and_integration(self):
|
||||||
from assets.models import TrafficLightPole, TrafficLightController, TrafficLightControllerHardware, TrafficLightAccessory
|
from assets.models import (
|
||||||
from assets.models.sign import SignPanel
|
TrafficLightPole, TrafficLightController, TrafficLightControllerHardware,
|
||||||
from assets.models.its import ITSGeoAsset
|
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(
|
pole = TrafficLightPole.objects.create(
|
||||||
intersection=self.intersection,
|
intersection=self.intersection,
|
||||||
code="SWB01_A01",
|
code="SWB01_A01",
|
||||||
|
model=pole_model,
|
||||||
status="active"
|
status="active"
|
||||||
)
|
)
|
||||||
ctrl = TrafficLightController.objects.create(
|
ctrl = TrafficLightController.objects.create(
|
||||||
intersection=self.intersection,
|
intersection=self.intersection,
|
||||||
code="SWB01_C01",
|
code="SWB01_C01",
|
||||||
|
model=ctrl_model,
|
||||||
status="active"
|
status="active"
|
||||||
)
|
)
|
||||||
hw = TrafficLightControllerHardware.objects.create(
|
hw = TrafficLightControllerHardware.objects.create(
|
||||||
|
|
@ -1391,11 +1403,13 @@ class TrafficLightCrossPlanImportTest(TestCase):
|
||||||
sign = SignPanel.objects.create(
|
sign = SignPanel.objects.create(
|
||||||
trafficlight_pole=pole,
|
trafficlight_pole=pole,
|
||||||
code="SWB01_SP01",
|
code="SWB01_SP01",
|
||||||
|
model=sign_model,
|
||||||
status="active"
|
status="active"
|
||||||
)
|
)
|
||||||
its = ITSGeoAsset.objects.create(
|
its = ITSGeoAsset.objects.create(
|
||||||
trafficlights_pole=pole,
|
trafficlights_pole=pole,
|
||||||
code="SWB01_ITS01",
|
code="SWB01_ITS01",
|
||||||
|
model=its_model,
|
||||||
status="active"
|
status="active"
|
||||||
)
|
)
|
||||||
acc = TrafficLightAccessory.objects.create(
|
acc = TrafficLightAccessory.objects.create(
|
||||||
|
|
@ -2232,6 +2246,73 @@ class TrafficLightCrossPlanImportTest(TestCase):
|
||||||
if os.path.exists(tmp_path):
|
if os.path.exists(tmp_path):
|
||||||
os.remove(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):
|
class AssetDefaultPositionTest(TestCase):
|
||||||
def test_assign_default_position_hierarchy(self):
|
def test_assign_default_position_hierarchy(self):
|
||||||
|
|
|
||||||
|
|
@ -287,6 +287,10 @@ def parse_cross_plan(excel_path):
|
||||||
|
|
||||||
normalized_upper = richting.upper().replace(" ", "")
|
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):
|
if conn.get("is_detector", False):
|
||||||
is_detector = True
|
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")):
|
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")):
|
||||||
|
|
|
||||||
|
|
@ -4751,6 +4751,8 @@ def integrate_cross_plan(request, intersection_id):
|
||||||
pole=pole,
|
pole=pole,
|
||||||
code=dd["code"],
|
code=dd["code"],
|
||||||
name=custom_name,
|
name=custom_name,
|
||||||
|
name_fr=custom_name,
|
||||||
|
name_nl=custom_name,
|
||||||
model=db_model,
|
model=db_model,
|
||||||
phases=dd["phases"],
|
phases=dd["phases"],
|
||||||
status='active',
|
status='active',
|
||||||
|
|
@ -5067,6 +5069,8 @@ def integrate_cross_plan(request, intersection_id):
|
||||||
pole=pole,
|
pole=pole,
|
||||||
code=code,
|
code=code,
|
||||||
name=custom_name,
|
name=custom_name,
|
||||||
|
name_fr=custom_name,
|
||||||
|
name_nl=custom_name,
|
||||||
model=db_model,
|
model=db_model,
|
||||||
phases=dd["phases"],
|
phases=dd["phases"],
|
||||||
status='active',
|
status='active',
|
||||||
|
|
@ -5079,6 +5083,8 @@ def integrate_cross_plan(request, intersection_id):
|
||||||
existing.pole = pole
|
existing.pole = pole
|
||||||
existing.model = db_model
|
existing.model = db_model
|
||||||
existing.name = custom_name
|
existing.name = custom_name
|
||||||
|
existing.name_fr = custom_name
|
||||||
|
existing.name_nl = custom_name
|
||||||
existing.phases = dd["phases"]
|
existing.phases = dd["phases"]
|
||||||
existing.updated_by = request.user
|
existing.updated_by = request.user
|
||||||
existing.save()
|
existing.save()
|
||||||
|
|
@ -5090,6 +5096,8 @@ def integrate_cross_plan(request, intersection_id):
|
||||||
pole=pole,
|
pole=pole,
|
||||||
code=code,
|
code=code,
|
||||||
name=custom_name,
|
name=custom_name,
|
||||||
|
name_fr=custom_name,
|
||||||
|
name_nl=custom_name,
|
||||||
model=db_model,
|
model=db_model,
|
||||||
phases=dd["phases"],
|
phases=dd["phases"],
|
||||||
status='active',
|
status='active',
|
||||||
|
|
@ -5102,6 +5110,8 @@ def integrate_cross_plan(request, intersection_id):
|
||||||
# 'keep'
|
# 'keep'
|
||||||
if existing:
|
if existing:
|
||||||
existing.name = custom_name
|
existing.name = custom_name
|
||||||
|
existing.name_fr = custom_name
|
||||||
|
existing.name_nl = custom_name
|
||||||
existing.save()
|
existing.save()
|
||||||
|
|
||||||
for key in request.POST.keys():
|
for key in request.POST.keys():
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue