feat: implement traffic light radar support including models, views, templates, and tests
This commit is contained in:
parent
6fe9908a11
commit
ba268f5e2a
4 changed files with 91 additions and 18 deletions
|
|
@ -3,6 +3,7 @@
|
||||||
# ---------------------
|
# ---------------------
|
||||||
|
|
||||||
from django.contrib.gis.db import models
|
from django.contrib.gis.db import models
|
||||||
|
from django.db.models import Q
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
@ -299,23 +300,23 @@ class TrafficLightPole(TrafficLightAbstractGeoAsset):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sorted_lanterns(self):
|
def sorted_lanterns(self):
|
||||||
return self.lanterns.order_by('code')
|
return self.lanterns.exclude(status='archived').order_by('code')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sorted_accessories(self):
|
def sorted_accessories(self):
|
||||||
return self.accessories.order_by('code')
|
return self.accessories.exclude(status='archived').order_by('code')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sorted_radars(self):
|
def sorted_radars(self):
|
||||||
return self.radars.order_by('code')
|
return self.radars.exclude(status='archived').order_by('code')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sorted_detectors(self):
|
def sorted_detectors(self):
|
||||||
return self.detectors.order_by('code')
|
return self.detectors.exclude(status='archived').order_by('code')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sorted_signpanels(self):
|
def sorted_signpanels(self):
|
||||||
return self.signpanels_attached.order_by('code')
|
return self.signpanels_attached.exclude(status='archived').order_by('code')
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
@ -408,19 +409,24 @@ class TrafficLightController(TrafficLightAbstractGeoAsset):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sorted_hardwares(self):
|
def sorted_hardwares(self):
|
||||||
return self.hardwares.order_by('code')
|
return self.hardwares.exclude(status='archived').order_by('code')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sorted_detectors(self):
|
def sorted_detectors(self):
|
||||||
return self.detectors.order_by('code')
|
from assets.models.trafficlights import TrafficLightDetector
|
||||||
|
if self.intersection_id:
|
||||||
|
return TrafficLightDetector.objects.filter(
|
||||||
|
Q(controller=self) | Q(intersection_id=self.intersection_id)
|
||||||
|
).exclude(status='archived').distinct().order_by('code')
|
||||||
|
return self.detectors.exclude(status='archived').order_by('code')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sorted_network_hardwares(self):
|
def sorted_network_hardwares(self):
|
||||||
return self.network_hardwares.order_by('code')
|
return self.network_hardwares.exclude(status='archived').order_by('code')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sorted_voltage_hardwares(self):
|
def sorted_voltage_hardwares(self):
|
||||||
return self.voltage_hardwares.order_by('code')
|
return self.voltage_hardwares.exclude(status='archived').order_by('code')
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
@ -516,6 +522,10 @@ class TrafficLightDetector(TrafficLightAbstractGeoAsset):
|
||||||
self.intersection = self.pole.intersection
|
self.intersection = self.pole.intersection
|
||||||
elif self.controller and self.controller.intersection and not self.intersection_id:
|
elif self.controller and self.controller.intersection and not self.intersection_id:
|
||||||
self.intersection = self.controller.intersection
|
self.intersection = self.controller.intersection
|
||||||
|
if not self.controller_id and self.intersection_id:
|
||||||
|
active_ctrl = TrafficLightController.objects.filter(intersection_id=self.intersection_id, status='active').first()
|
||||||
|
if active_ctrl:
|
||||||
|
self.controller = active_ctrl
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
@ -760,7 +770,7 @@ class TrafficLightRadarPole(TrafficLightAbstractGeoAsset):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sorted_radars(self):
|
def sorted_radars(self):
|
||||||
return self.radars.order_by('code')
|
return self.radars.exclude(status='archived').order_by('code')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sorted_signpanels(self):
|
def sorted_signpanels(self):
|
||||||
|
|
|
||||||
|
|
@ -677,7 +677,7 @@
|
||||||
aria-controls="collapseHardware{{ ctrl.id }}">
|
aria-controls="collapseHardware{{ ctrl.id }}">
|
||||||
<i class="bi bi-motherboard me-2 text-secondary"></i>
|
<i class="bi bi-motherboard me-2 text-secondary"></i>
|
||||||
{% translate "Hardwares du contrôleur" %}
|
{% translate "Hardwares du contrôleur" %}
|
||||||
<span class="badge bg-secondary ms-2">{{ ctrl.hardwares.count }}</span>
|
<span class="badge bg-secondary ms-2">{{ ctrl.sorted_hardwares|length }}</span>
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
<div id="collapseHardware{{ ctrl.id }}" class="accordion-collapse collapse"
|
<div id="collapseHardware{{ ctrl.id }}" class="accordion-collapse collapse"
|
||||||
|
|
@ -717,7 +717,7 @@
|
||||||
aria-controls="collapseDetectors{{ ctrl.id }}">
|
aria-controls="collapseDetectors{{ ctrl.id }}">
|
||||||
<i class="bi bi-radar me-2 text-secondary"></i>
|
<i class="bi bi-radar me-2 text-secondary"></i>
|
||||||
{% translate "Détecteurs" %}
|
{% translate "Détecteurs" %}
|
||||||
<span class="badge bg-secondary ms-2">{{ ctrl.detectors.count }}</span>
|
<span class="badge bg-secondary ms-2">{{ ctrl.sorted_detectors|length }}</span>
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
<div id="collapseDetectors{{ ctrl.id }}" class="accordion-collapse collapse"
|
<div id="collapseDetectors{{ ctrl.id }}" class="accordion-collapse collapse"
|
||||||
|
|
@ -732,6 +732,7 @@
|
||||||
<i class="bi bi-radar text-secondary small"></i>
|
<i class="bi bi-radar text-secondary small"></i>
|
||||||
<a href="{% url 'assets:trafficlights_assets_detail' 'trafficlightdetector' det.id %}" class="fw-medium text-decoration-none">{{ det.code }}</a>
|
<a href="{% url 'assets:trafficlights_assets_detail' 'trafficlightdetector' det.id %}" class="fw-medium text-decoration-none">{{ det.code }}</a>
|
||||||
{% if det.name %}<span class="text-muted small ms-1">{{ det.name }}</span>{% endif %}
|
{% if det.name %}<span class="text-muted small ms-1">{{ det.name }}</span>{% endif %}
|
||||||
|
{% if det.pole %}<span class="badge bg-light text-secondary border small"><i class="bi bi-sign-stop-lights me-1"></i>{{ det.pole.code }}</span>{% endif %}
|
||||||
<span class="ms-auto d-flex gap-1 align-items-center">
|
<span class="ms-auto d-flex gap-1 align-items-center">
|
||||||
{% if det.hardware %}<span class="badge bg-light text-dark border">{{ det.hardware }}</span>{% endif %}
|
{% if det.hardware %}<span class="badge bg-light text-dark border">{{ det.hardware }}</span>{% endif %}
|
||||||
{% if det.status != 'active' %}<span class="badge {% if det.status == 'archived' %}bg-secondary{% else %}bg-warning text-dark{% endif %}">{{ det.get_status_display }}</span>{% endif %}
|
{% if det.status != 'active' %}<span class="badge {% if det.status == 'archived' %}bg-secondary{% else %}bg-warning text-dark{% endif %}">{{ det.get_status_display }}</span>{% endif %}
|
||||||
|
|
@ -757,7 +758,7 @@
|
||||||
aria-controls="collapseNetwork{{ ctrl.id }}">
|
aria-controls="collapseNetwork{{ ctrl.id }}">
|
||||||
<i class="bi bi-hdd-network me-2 text-secondary"></i>
|
<i class="bi bi-hdd-network me-2 text-secondary"></i>
|
||||||
{% translate "Hardware réseau" %}
|
{% translate "Hardware réseau" %}
|
||||||
<span class="badge bg-secondary ms-2">{{ ctrl.network_hardwares.count }}</span>
|
<span class="badge bg-secondary ms-2">{{ ctrl.sorted_network_hardwares|length }}</span>
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
<div id="collapseNetwork{{ ctrl.id }}" class="accordion-collapse collapse"
|
<div id="collapseNetwork{{ ctrl.id }}" class="accordion-collapse collapse"
|
||||||
|
|
@ -797,7 +798,7 @@
|
||||||
aria-controls="collapseVoltage{{ ctrl.id }}">
|
aria-controls="collapseVoltage{{ ctrl.id }}">
|
||||||
<i class="bi bi-lightning-charge me-2 text-secondary"></i>
|
<i class="bi bi-lightning-charge me-2 text-secondary"></i>
|
||||||
{% translate "Hardware Tension" %}
|
{% translate "Hardware Tension" %}
|
||||||
<span class="badge bg-secondary ms-2">{{ ctrl.voltage_hardwares.count }}</span>
|
<span class="badge bg-secondary ms-2">{{ ctrl.sorted_voltage_hardwares|length }}</span>
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
<div id="collapseVoltage{{ ctrl.id }}" class="accordion-collapse collapse"
|
<div id="collapseVoltage{{ ctrl.id }}" class="accordion-collapse collapse"
|
||||||
|
|
|
||||||
|
|
@ -1323,6 +1323,51 @@ class TrafficLightCrossPlanImportTest(TestCase):
|
||||||
self.assertIsNotNone(new_active)
|
self.assertIsNotNone(new_active)
|
||||||
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):
|
||||||
|
from assets.models import TrafficLightPole, TrafficLightLantern, TrafficLightDetector
|
||||||
|
pole = TrafficLightPole.objects.create(
|
||||||
|
intersection=self.intersection,
|
||||||
|
code="SWB01_A01",
|
||||||
|
status="active"
|
||||||
|
)
|
||||||
|
old_lantern = TrafficLightLantern.objects.create(
|
||||||
|
pole=pole,
|
||||||
|
code="SWB01_A01_LAN01",
|
||||||
|
model=self.lantern_model,
|
||||||
|
status="active"
|
||||||
|
)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
import os
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
|
|
||||||
|
real_excel_path = os.path.join(os.path.dirname(settings.BASE_DIR), 'external_files', '18ac47df841-2808f-DOC-23270.xlsx')
|
||||||
|
if not os.path.exists(real_excel_path):
|
||||||
|
self.skipTest("Real Excel file for test not found")
|
||||||
|
|
||||||
|
with open(real_excel_path, 'rb') as f:
|
||||||
|
excel_content = f.read()
|
||||||
|
|
||||||
|
uploaded_file = SimpleUploadedFile("test_cross_plan.xlsx", excel_content, content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||||||
|
url_import = reverse("assets:import_cross_plan", args=[self.intersection.id])
|
||||||
|
response = self.client.post(url_import, {"cross_plan_file_upload": uploaded_file})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
# Post integrate with action="modify"
|
||||||
|
url_integrate = reverse("assets:integrate_cross_plan", args=[self.intersection.id])
|
||||||
|
response_integrate = self.client.post(url_integrate, {
|
||||||
|
"cross_plan_filename": response.context["cross_plan_filename"],
|
||||||
|
"selected_lantern_SWB01_A01_LAN01_T1": "on",
|
||||||
|
"action_lantern_SWB01_A01_LAN01_T1": "modify",
|
||||||
|
"db_id_lantern_SWB01_A01_LAN01_T1": str(old_lantern.id)
|
||||||
|
})
|
||||||
|
self.assertEqual(response_integrate.status_code, 302)
|
||||||
|
|
||||||
|
# Old lantern must have its code updated in-place without creating a duplicate
|
||||||
|
old_lantern.refresh_from_db()
|
||||||
|
self.assertEqual(old_lantern.status, "active")
|
||||||
|
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 TrafficLightPole, TrafficLightController, TrafficLightControllerHardware, TrafficLightAccessory
|
||||||
from assets.models.sign import SignPanel
|
from assets.models.sign import SignPanel
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,16 @@ from common.models import Thematic, UserConfig, UserContractAccess, UserThematic
|
||||||
from common.utils import check_thematic_access
|
from common.utils import check_thematic_access
|
||||||
from interventions.models import InterventionLocation
|
from interventions.models import InterventionLocation
|
||||||
from interventions.views.drafts import InterventionDraftManager
|
from interventions.views.drafts import InterventionDraftManager
|
||||||
from assets.models import TrafficLightIntersection, TrafficLightIntersectionDocument, TrafficLightController, TrafficLightPole, TrafficLightCable, TrafficLightLantern, TrafficLightDetector, TrafficLightElectricalCabinet, TrafficLightRadar, TrafficLightRadarPole, TrafficLightProgramming
|
from assets.models import (
|
||||||
from assets.models import AssetCategory, TrafficLightCableModel, TrafficLightControllerModel, TrafficLightDetectorModel, TrafficLightLanternModel, TrafficLightPoleModel, TrafficLightElectricalCabinetModel, TrafficLightContract, TrafficLightRadarModel
|
TrafficLightIntersection, TrafficLightIntersectionDocument, TrafficLightController,
|
||||||
|
TrafficLightPole, TrafficLightCable, TrafficLightLantern, TrafficLightDetector,
|
||||||
|
TrafficLightElectricalCabinet, TrafficLightRadar, TrafficLightRadarPole, TrafficLightProgramming,
|
||||||
|
TrafficLightAccessory, TrafficLightAccessoryModel, TrafficLightNetworkHardware,
|
||||||
|
TrafficLightVoltageHardware, TrafficLightControllerHardware,
|
||||||
|
AssetCategory, TrafficLightCableModel, TrafficLightControllerModel,
|
||||||
|
TrafficLightDetectorModel, TrafficLightLanternModel, TrafficLightPoleModel,
|
||||||
|
TrafficLightElectricalCabinetModel, TrafficLightContract, TrafficLightRadarModel
|
||||||
|
)
|
||||||
from assets.forms import IntersectionDocumentForm
|
from assets.forms import IntersectionDocumentForm
|
||||||
from assets.permissions import get_allowed_actions_for_thematic, can_edit_location_for_thematic
|
from assets.permissions import get_allowed_actions_for_thematic, can_edit_location_for_thematic
|
||||||
from contracts.models import Contract
|
from contracts.models import Contract
|
||||||
|
|
@ -4411,7 +4419,7 @@ def import_cross_plan(request, intersection_id):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for acc in TrafficLightAccessory.objects.filter(intersection=intersection, status='active').select_related('model', 'pole'):
|
for acc in TrafficLightAccessory.objects.filter(Q(intersection=intersection) | Q(pole__intersection=intersection), status='active').distinct().select_related('model', 'pole'):
|
||||||
other_assets.append({
|
other_assets.append({
|
||||||
'type': 'accessory',
|
'type': 'accessory',
|
||||||
'category_label': _("Accessoire"),
|
'category_label': _("Accessoire"),
|
||||||
|
|
@ -4430,7 +4438,7 @@ def import_cross_plan(request, intersection_id):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for rad in TrafficLightRadar.objects.filter(intersection=intersection, status='active').select_related('model', 'trafficlights_pole', 'pole'):
|
for rad in TrafficLightRadar.objects.filter(Q(intersection=intersection) | Q(trafficlights_pole__intersection=intersection) | Q(pole__intersection=intersection), status='active').distinct().select_related('model', 'trafficlights_pole', 'pole'):
|
||||||
sup = rad.trafficlights_pole.code if rad.trafficlights_pole else (rad.pole.code if rad.pole else _("Carrefour"))
|
sup = rad.trafficlights_pole.code if rad.trafficlights_pole else (rad.pole.code if rad.pole else _("Carrefour"))
|
||||||
other_assets.append({
|
other_assets.append({
|
||||||
'type': 'radar',
|
'type': 'radar',
|
||||||
|
|
@ -4798,6 +4806,7 @@ def integrate_cross_plan(request, intersection_id):
|
||||||
p.save()
|
p.save()
|
||||||
created_poles[code] = p
|
created_poles[code] = p
|
||||||
elif action == "modify" and existing:
|
elif action == "modify" and existing:
|
||||||
|
existing.code = code
|
||||||
existing.model = db_model
|
existing.model = db_model
|
||||||
existing.name_fr = custom_name
|
existing.name_fr = custom_name
|
||||||
existing.name_nl = custom_name
|
existing.name_nl = custom_name
|
||||||
|
|
@ -4902,6 +4911,7 @@ def integrate_cross_plan(request, intersection_id):
|
||||||
update_cable_geometry(c, code)
|
update_cable_geometry(c, code)
|
||||||
c.save()
|
c.save()
|
||||||
elif action == "modify" and existing:
|
elif action == "modify" and existing:
|
||||||
|
existing.code = code
|
||||||
existing.model = db_model
|
existing.model = db_model
|
||||||
existing.name_fr = custom_name
|
existing.name_fr = custom_name
|
||||||
existing.name_nl = custom_name
|
existing.name_nl = custom_name
|
||||||
|
|
@ -4983,6 +4993,9 @@ def integrate_cross_plan(request, intersection_id):
|
||||||
updated_by=request.user
|
updated_by=request.user
|
||||||
)
|
)
|
||||||
elif action == "modify" and existing:
|
elif action == "modify" and existing:
|
||||||
|
existing.code = code
|
||||||
|
if pole:
|
||||||
|
existing.pole = pole
|
||||||
existing.model = db_model
|
existing.model = db_model
|
||||||
existing.name_fr = custom_name
|
existing.name_fr = custom_name
|
||||||
existing.name_nl = custom_name
|
existing.name_nl = custom_name
|
||||||
|
|
@ -5061,8 +5074,12 @@ def integrate_cross_plan(request, intersection_id):
|
||||||
updated_by=request.user
|
updated_by=request.user
|
||||||
)
|
)
|
||||||
elif action == "modify" and existing:
|
elif action == "modify" and existing:
|
||||||
|
existing.code = code
|
||||||
|
if pole:
|
||||||
|
existing.pole = pole
|
||||||
existing.model = db_model
|
existing.model = db_model
|
||||||
existing.name = custom_name
|
existing.name = custom_name
|
||||||
|
existing.phases = dd["phases"]
|
||||||
existing.updated_by = request.user
|
existing.updated_by = request.user
|
||||||
existing.save()
|
existing.save()
|
||||||
elif action == "replace" and existing:
|
elif action == "replace" and existing:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue