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"))
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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")):
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
Loading…
Reference in a new issue