feat: add support for TrafficLightProgramming management and textarea fields in generic asset templates
This commit is contained in:
parent
80cbb80d06
commit
94cb7148a0
10 changed files with 638 additions and 18 deletions
|
|
@ -0,0 +1,22 @@
|
||||||
|
# Generated by Django 6.0.7 on 2026-08-19 09:20
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('assets', '0021_sync_asset_parent_data'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='trafficlightprogramming',
|
||||||
|
options={'ordering': ['-commissioning_date'], 'verbose_name': 'Programmation', 'verbose_name_plural': 'Programmations'},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='trafficlightprogramming',
|
||||||
|
name='index',
|
||||||
|
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Index'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def create_programming_category(apps, schema_editor):
|
||||||
|
Thematic = apps.get_model('common', 'Thematic')
|
||||||
|
AssetCategory = apps.get_model('assets', 'AssetCategory')
|
||||||
|
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||||
|
|
||||||
|
try:
|
||||||
|
thematic = Thematic.objects.get(code='trafficlights')
|
||||||
|
except Thematic.DoesNotExist:
|
||||||
|
thematic, _ = Thematic.objects.get_or_create(
|
||||||
|
code='trafficlights',
|
||||||
|
defaults={
|
||||||
|
'name_fr': 'Feux de signalisation',
|
||||||
|
'name_nl': 'Verkeerslichten',
|
||||||
|
'description': 'Gestion des carrefours à feux et équipements associés',
|
||||||
|
'icon': 'bi-stoplights',
|
||||||
|
'color': '#ffc107',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
category, _cat_created = AssetCategory.objects.get_or_create(
|
||||||
|
code='TL_PROGRAMMING',
|
||||||
|
defaults={
|
||||||
|
'thematic': thematic,
|
||||||
|
'name_fr': 'Programmation',
|
||||||
|
'name_nl': 'Programmatie',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Link ContentType for TrafficLightProgramming
|
||||||
|
try:
|
||||||
|
prog_ct = ContentType.objects.get(app_label='assets', model='trafficlightprogramming')
|
||||||
|
category.allowed_models.add(prog_ct)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def remove_programming_category(apps, schema_editor):
|
||||||
|
AssetCategory = apps.get_model('assets', 'AssetCategory')
|
||||||
|
AssetCategory.objects.filter(code='TL_PROGRAMMING').delete()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('assets', '0022_alter_trafficlightprogramming_options_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(create_programming_category, remove_programming_category),
|
||||||
|
]
|
||||||
|
|
@ -97,6 +97,9 @@ class TrafficLightIntersection(AbstractLocation):
|
||||||
# Retrieve associated radars
|
# Retrieve associated radars
|
||||||
radars = self.radars.filter(status='active').order_by('code')
|
radars = self.radars.filter(status='active').order_by('code')
|
||||||
|
|
||||||
|
# Retrieve associated programmations
|
||||||
|
programmations = self.programmations.filter(status='active').order_by('code')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"poles": all_poles,
|
"poles": all_poles,
|
||||||
"cables": cables,
|
"cables": cables,
|
||||||
|
|
@ -105,6 +108,7 @@ class TrafficLightIntersection(AbstractLocation):
|
||||||
"detectors": detectors,
|
"detectors": detectors,
|
||||||
"electrical_cabinets": electrical_cabinets,
|
"electrical_cabinets": electrical_cabinets,
|
||||||
"radars": radars,
|
"radars": radars,
|
||||||
|
"programmations": programmations,
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_all_assets(self):
|
def get_all_assets(self):
|
||||||
|
|
@ -132,6 +136,9 @@ class TrafficLightIntersection(AbstractLocation):
|
||||||
# Retrieve associated radars
|
# Retrieve associated radars
|
||||||
radars = self.radars.all().order_by('code')
|
radars = self.radars.all().order_by('code')
|
||||||
|
|
||||||
|
# Retrieve associated programmations
|
||||||
|
programmations = self.programmations.all().order_by('code')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"poles": all_poles,
|
"poles": all_poles,
|
||||||
"cables": cables,
|
"cables": cables,
|
||||||
|
|
@ -140,6 +147,7 @@ class TrafficLightIntersection(AbstractLocation):
|
||||||
"detectors": detectors,
|
"detectors": detectors,
|
||||||
"electrical_cabinets": electrical_cabinets,
|
"electrical_cabinets": electrical_cabinets,
|
||||||
"radars": radars,
|
"radars": radars,
|
||||||
|
"programmations": programmations,
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_direct_child_assets(self):
|
def get_direct_child_assets(self):
|
||||||
|
|
@ -167,6 +175,9 @@ class TrafficLightIntersection(AbstractLocation):
|
||||||
# Retrieve associated radars
|
# Retrieve associated radars
|
||||||
radars = self.radars.all().order_by('code')
|
radars = self.radars.all().order_by('code')
|
||||||
|
|
||||||
|
# Retrieve associated programmations
|
||||||
|
programmations = self.programmations.all().order_by('code')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"poles": all_poles,
|
"poles": all_poles,
|
||||||
"cables": cables,
|
"cables": cables,
|
||||||
|
|
@ -175,6 +186,7 @@ class TrafficLightIntersection(AbstractLocation):
|
||||||
"detectors": detectors,
|
"detectors": detectors,
|
||||||
"electrical_cabinets": electrical_cabinets,
|
"electrical_cabinets": electrical_cabinets,
|
||||||
"radars": radars,
|
"radars": radars,
|
||||||
|
"programmations": programmations,
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_controller_contract(self):
|
def get_controller_contract(self):
|
||||||
|
|
@ -709,19 +721,21 @@ class TrafficLightAccessory(TrafficLightAbstractGeoAsset):
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Treffic Light Programmation
|
# Traffic Light Programmation
|
||||||
class TrafficLightProgramming(TrafficLightAbstractAsset):
|
class TrafficLightProgramming(TrafficLightAbstractAsset):
|
||||||
intersection = models.ForeignKey(TrafficLightIntersection, on_delete=models.CASCADE, related_name="programmations")
|
intersection = models.ForeignKey(TrafficLightIntersection, on_delete=models.CASCADE, related_name="programmations")
|
||||||
commissioning_date = models.DateTimeField(null=True, blank=True, verbose_name=_("Date de mise en service"))
|
commissioning_date = models.DateTimeField(null=True, blank=True, verbose_name=_("Date de mise en service"))
|
||||||
uncommissioning_date = models.DateTimeField(null=True, blank=True, verbose_name=_("Date de mise hors service"))
|
uncommissioning_date = models.DateTimeField(null=True, blank=True, verbose_name=_("Date de mise hors service"))
|
||||||
plan = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Plan"))
|
plan = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("Plan"))
|
||||||
index = models.CharField(max_length=100, verbose_name=_("Index"))
|
index = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Index"))
|
||||||
info = models.TextField(null=True, blank=True, verbose_name=_("Informations"))
|
info = models.TextField(null=True, blank=True, verbose_name=_("Informations"))
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.index} - {self.intersection.code}"
|
return f"{self.index or self.code} - {self.intersection.code}"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
verbose_name = _("Programmation")
|
||||||
|
verbose_name_plural = _("Programmations")
|
||||||
ordering = ['-commissioning_date']
|
ordering = ['-commissioning_date']
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -462,6 +462,8 @@
|
||||||
<input type="number" step="any" class="form-control form-control-sm" value="{{ field.input_value }}">
|
<input type="number" step="any" class="form-control form-control-sm" value="{{ field.input_value }}">
|
||||||
{% elif field.field_type == 'date' %}
|
{% elif field.field_type == 'date' %}
|
||||||
<input type="date" class="form-control form-control-sm" value="{{ field.input_value }}">
|
<input type="date" class="form-control form-control-sm" value="{{ field.input_value }}">
|
||||||
|
{% elif field.field_type == 'textarea' %}
|
||||||
|
<textarea class="form-control form-control-sm" rows="3">{{ field.input_value }}</textarea>
|
||||||
{% else %}
|
{% else %}
|
||||||
<input type="text" class="form-control form-control-sm" value="{{ field.input_value }}">
|
<input type="text" class="form-control form-control-sm" value="{{ field.input_value }}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
||||||
|
|
@ -850,7 +850,12 @@
|
||||||
<div class="card-header bg-light d-flex align-items-center gap-2 py-2">
|
<div class="card-header bg-light d-flex align-items-center gap-2 py-2">
|
||||||
<i class="bi bi-kanban text-secondary"></i>
|
<i class="bi bi-kanban text-secondary"></i>
|
||||||
<span class="fw-semibold">{% translate "Programmations" %}</span>
|
<span class="fw-semibold">{% translate "Programmations" %}</span>
|
||||||
<span class="badge bg-secondary ms-auto">{{ prog_list|length }}</span>
|
<span class="badge bg-secondary">{{ prog_list|length }}</span>
|
||||||
|
{% if 'create' in allowed_location_actions %}
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary ms-auto" data-bs-toggle="modal" data-bs-target="#addProgrammingModal" title="{% translate 'Ajouter une programmation' %}">
|
||||||
|
<i class="bi bi-plus-circle me-1"></i>{% translate "Ajouter" %}
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<ul class="list-group list-group-flush">
|
<ul class="list-group list-group-flush">
|
||||||
{% for prog in prog_list %}
|
{% for prog in prog_list %}
|
||||||
|
|
@ -876,7 +881,12 @@
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="text-center text-muted py-4">
|
<div class="text-center text-muted py-4">
|
||||||
<i class="bi bi-kanban fs-3 opacity-50 d-block mb-2"></i>
|
<i class="bi bi-kanban fs-3 opacity-50 d-block mb-2"></i>
|
||||||
{% translate "Aucune programmation" %}
|
<p class="mb-2">{% translate "Aucune programmation" %}</p>
|
||||||
|
{% if 'create' in allowed_location_actions %}
|
||||||
|
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#addProgrammingModal">
|
||||||
|
<i class="bi bi-plus-circle me-1"></i>{% translate "Ajouter une programmation" %}
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
@ -1025,6 +1035,76 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Programming Modal -->
|
||||||
|
<div class="modal fade" id="addProgrammingModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title"><i class="bi bi-kanban me-2 text-primary"></i>{% translate "Ajouter une programmation" %}</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<form id="addProgrammingForm" enctype="multipart/form-data" method="POST" action="{% url 'assets:trafficlights_add_programming' intersection.id %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="modal-body">
|
||||||
|
<div id="addProgrammingAlert" class="alert d-none" role="alert"></div>
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">{% translate "Code" %}</label>
|
||||||
|
<input type="text" name="code" class="form-control" placeholder="{% translate 'Laisser vide pour auto-génération' %}">
|
||||||
|
<small class="text-muted">{% translate "Ex: " %}{{ intersection.code }}-PRG-001</small>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">{% translate "Index" %}</label>
|
||||||
|
<input type="text" name="index" class="form-control" value="{{ intersection.current_index|default:'' }}" placeholder="Ex: 01">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">{% translate "Plan" %}</label>
|
||||||
|
<input type="text" name="plan" class="form-control" placeholder="Ex: Plan 1A">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">{% translate "Statut" %}</label>
|
||||||
|
<select name="status" class="form-select">
|
||||||
|
<option value="active" selected>{% translate "Actif" %}</option>
|
||||||
|
<option value="in_preparation">{% translate "En préparation" %}</option>
|
||||||
|
<option value="inactive">{% translate "Inactif" %}</option>
|
||||||
|
<option value="removed">{% translate "Retiré" %}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">{% translate "Date de mise en service" %}</label>
|
||||||
|
<input type="date" name="commissioning_date" class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">{% translate "Date de mise hors service" %}</label>
|
||||||
|
<input type="date" name="uncommissioning_date" class="form-control">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label">{% translate "Fichier de programmation" %}</label>
|
||||||
|
<input type="file" name="file" class="form-control">
|
||||||
|
<small class="text-muted">{% translate "Optionnel. Fichier joint à la programmation." %}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label">{% translate "Informations" %}</label>
|
||||||
|
<textarea name="info" rows="3" class="form-control" placeholder="{% translate 'Notes ou informations complémentaires...' %}"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% translate "Annuler" %}</button>
|
||||||
|
<button type="submit" class="btn btn-primary" id="addProgrammingSubmitBtn">
|
||||||
|
<i class="bi bi-plus-circle me-1"></i>{% translate "Créer la programmation" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Georeferenced Plans Selection Modal -->
|
<!-- Georeferenced Plans Selection Modal -->
|
||||||
|
|
@ -1385,5 +1465,55 @@
|
||||||
: 'bi bi-chevron-expand small';
|
: 'bi bi-chevron-expand small';
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
// Handle Add Programming form submission
|
||||||
|
(function () {
|
||||||
|
const form = document.getElementById('addProgrammingForm');
|
||||||
|
if (!form) return;
|
||||||
|
form.addEventListener('submit', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const alertEl = document.getElementById('addProgrammingAlert');
|
||||||
|
const submitBtn = document.getElementById('addProgrammingSubmitBtn');
|
||||||
|
alertEl.className = 'alert d-none';
|
||||||
|
|
||||||
|
if (submitBtn) submitBtn.disabled = true;
|
||||||
|
|
||||||
|
const formData = new FormData(form);
|
||||||
|
const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]')?.value || '{{ csrf_token }}';
|
||||||
|
|
||||||
|
fetch(form.action, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': csrfToken,
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(r => r.json().then(data => ({ status: r.status, ok: r.ok, data })))
|
||||||
|
.then(({ ok, data }) => {
|
||||||
|
if (ok && data.success) {
|
||||||
|
alertEl.textContent = data.message || "{% translate 'Programmation ajoutée avec succès.' %}";
|
||||||
|
alertEl.className = 'alert alert-success';
|
||||||
|
setTimeout(() => {
|
||||||
|
if (data.redirect_url) {
|
||||||
|
window.location.href = data.redirect_url;
|
||||||
|
} else {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
alertEl.textContent = data.error || "{% translate 'Erreur lors de la création de la programmation.' %}";
|
||||||
|
alertEl.className = 'alert alert-danger';
|
||||||
|
if (submitBtn) submitBtn.disabled = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Error creating programming:', err);
|
||||||
|
alertEl.textContent = "{% translate 'Une erreur inattendue est survenue.' %}";
|
||||||
|
alertEl.className = 'alert alert-danger';
|
||||||
|
if (submitBtn) submitBtn.disabled = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
{% endblock script %}
|
{% endblock script %}
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ def get_asset_specific_fields(asset):
|
||||||
from django.contrib.gis.db.models import GeometryField
|
from django.contrib.gis.db.models import GeometryField
|
||||||
from django.db.models import (ForeignKey, OneToOneField, ManyToManyField, AutoField,
|
from django.db.models import (ForeignKey, OneToOneField, ManyToManyField, AutoField,
|
||||||
BooleanField, FloatField, DecimalField, IntegerField,
|
BooleanField, FloatField, DecimalField, IntegerField,
|
||||||
DateField, DateTimeField)
|
DateField, DateTimeField, TextField)
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from assets.models import AbstractAsset, AbstractGeoAsset
|
from assets.models import AbstractAsset, AbstractGeoAsset
|
||||||
|
|
||||||
|
|
@ -136,6 +136,10 @@ def get_asset_specific_fields(asset):
|
||||||
field_type = 'date'
|
field_type = 'date'
|
||||||
display_value = value.strftime('%d/%m/%Y') if value else '-'
|
display_value = value.strftime('%d/%m/%Y') if value else '-'
|
||||||
input_value = value.strftime('%Y-%m-%d') if value else ''
|
input_value = value.strftime('%Y-%m-%d') if value else ''
|
||||||
|
elif isinstance(field, TextField):
|
||||||
|
field_type = 'textarea'
|
||||||
|
display_value = str(value) if value is not None else '-'
|
||||||
|
input_value = str(value) if value is not None else ''
|
||||||
elif field.choices:
|
elif field.choices:
|
||||||
field_type = 'select'
|
field_type = 'select'
|
||||||
choices_dict = dict(field.choices)
|
choices_dict = dict(field.choices)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ from django.test import TestCase, tag
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.gis.geos import MultiPolygon, Polygon
|
from django.contrib.gis.geos import MultiPolygon, Polygon
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from unittest import skipUnless
|
from unittest import skipUnless
|
||||||
|
|
@ -3333,4 +3334,276 @@ class TrafficLightIntersectionControllerModelTest(TestCase):
|
||||||
self.assertIn("Siemens Sitraffic C1000 NL", content_nl)
|
self.assertIn("Siemens Sitraffic C1000 NL", content_nl)
|
||||||
|
|
||||||
|
|
||||||
|
class TrafficLightProgrammingTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
from django.contrib.gis.geos import Point
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from assets.models.trafficlights import TrafficLightProgramming
|
||||||
|
|
||||||
|
self.thematic, _ = Thematic.objects.get_or_create(
|
||||||
|
code="trafficlights",
|
||||||
|
defaults={
|
||||||
|
"name_fr": "Feux de signalisation",
|
||||||
|
"name_nl": "Verkeerslichten"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
from common.models import Role
|
||||||
|
self.user = User.objects.create_user(username="proguser", password="password123")
|
||||||
|
self.config = UserConfig.objects.create(user=self.user, limit_assets_to_contracts=False)
|
||||||
|
admin_role, _ = Role.objects.get_or_create(name="admin")
|
||||||
|
self.config.roles.add(admin_role)
|
||||||
|
|
||||||
|
UserThematics.objects.create(
|
||||||
|
user_config=self.config,
|
||||||
|
thematic=self.thematic,
|
||||||
|
can_view_assets=True,
|
||||||
|
can_edit_assets=True
|
||||||
|
)
|
||||||
|
|
||||||
|
self.category, _ = AssetCategory.objects.get_or_create(
|
||||||
|
code="TL_PROGRAMMING",
|
||||||
|
defaults={
|
||||||
|
"name_fr": "Programmation",
|
||||||
|
"name_nl": "Programmatie",
|
||||||
|
"thematic": self.thematic
|
||||||
|
}
|
||||||
|
)
|
||||||
|
prog_ct = ContentType.objects.get_for_model(TrafficLightProgramming)
|
||||||
|
self.category.allowed_models.add(prog_ct)
|
||||||
|
|
||||||
|
self.intersection = TrafficLightIntersection.objects.create(
|
||||||
|
code="K999",
|
||||||
|
name_fr="Carrefour Test Prog",
|
||||||
|
geom=Point(648000, 670000, srid=3812),
|
||||||
|
lon=4.3508,
|
||||||
|
lat=50.8465,
|
||||||
|
current_index="01"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_programming_category_exists_and_linked(self):
|
||||||
|
from assets.models.trafficlights import TrafficLightProgramming
|
||||||
|
prog_ct = ContentType.objects.get_for_model(TrafficLightProgramming)
|
||||||
|
self.assertTrue(self.category.allowed_models.filter(id=prog_ct.id).exists())
|
||||||
|
|
||||||
|
def test_intersection_get_assets_includes_programmations(self):
|
||||||
|
from assets.models.trafficlights import TrafficLightProgramming
|
||||||
|
prog_active = TrafficLightProgramming.objects.create(
|
||||||
|
code="K999-PRG-001",
|
||||||
|
intersection=self.intersection,
|
||||||
|
status="active",
|
||||||
|
index="01",
|
||||||
|
plan="Plan A"
|
||||||
|
)
|
||||||
|
prog_inactive = TrafficLightProgramming.objects.create(
|
||||||
|
code="K999-PRG-002",
|
||||||
|
intersection=self.intersection,
|
||||||
|
status="removed",
|
||||||
|
index="00",
|
||||||
|
plan="Plan Old"
|
||||||
|
)
|
||||||
|
|
||||||
|
active_assets = self.intersection.get_active_assets()
|
||||||
|
all_assets = self.intersection.get_all_assets()
|
||||||
|
|
||||||
|
self.assertIn("programmations", active_assets)
|
||||||
|
self.assertIn("programmations", all_assets)
|
||||||
|
self.assertEqual(len(active_assets["programmations"]), 1)
|
||||||
|
self.assertEqual(active_assets["programmations"][0].id, prog_active.id)
|
||||||
|
self.assertEqual(len(all_assets["programmations"]), 2)
|
||||||
|
|
||||||
|
def test_create_programming_via_dedicated_endpoint(self):
|
||||||
|
from assets.models.trafficlights import TrafficLightProgramming
|
||||||
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
|
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
test_file = SimpleUploadedFile("prog_test.txt", b"programming binary data", content_type="text/plain")
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("assets:trafficlights_add_programming", kwargs={"intersection_id": self.intersection.id}),
|
||||||
|
{
|
||||||
|
"code": "K999-PRG-NEW",
|
||||||
|
"index": "02",
|
||||||
|
"plan": "Plan 2B",
|
||||||
|
"commissioning_date": "2026-05-01",
|
||||||
|
"uncommissioning_date": "2026-12-31",
|
||||||
|
"status": "active",
|
||||||
|
"info": "Notes sur la programmation",
|
||||||
|
"file": test_file
|
||||||
|
},
|
||||||
|
HTTP_X_REQUESTED_WITH="XMLHttpRequest"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json()
|
||||||
|
self.assertTrue(data["success"])
|
||||||
|
self.assertEqual(data["asset_code"], "K999-PRG-NEW")
|
||||||
|
|
||||||
|
prog = TrafficLightProgramming.objects.get(code="K999-PRG-NEW")
|
||||||
|
self.assertEqual(prog.intersection, self.intersection)
|
||||||
|
self.assertEqual(prog.index, "02")
|
||||||
|
self.assertEqual(prog.plan, "Plan 2B")
|
||||||
|
self.assertEqual(prog.info, "Notes sur la programmation")
|
||||||
|
from django.utils import timezone
|
||||||
|
self.assertEqual(timezone.localtime(prog.commissioning_date).strftime("%Y-%m-%d"), "2026-05-01")
|
||||||
|
self.assertEqual(timezone.localtime(prog.uncommissioning_date).strftime("%Y-%m-%d"), "2026-12-31")
|
||||||
|
|
||||||
|
# Check document attached
|
||||||
|
doc = prog.documents.first()
|
||||||
|
self.assertIsNotNone(doc)
|
||||||
|
self.assertEqual(doc.document_type, "programming_file")
|
||||||
|
|
||||||
|
def test_create_programming_via_create_asset(self):
|
||||||
|
from assets.models.trafficlights import TrafficLightProgramming
|
||||||
|
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
response = self.client.post(
|
||||||
|
f"/assets/trafficlights/trafficlightintersection/{self.intersection.id}/create-asset/",
|
||||||
|
data=json.dumps({
|
||||||
|
"category_id": self.category.id,
|
||||||
|
"asset_type": "trafficlightprogramming",
|
||||||
|
"data": {}
|
||||||
|
}),
|
||||||
|
content_type="application/json",
|
||||||
|
HTTP_X_REQUESTED_WITH="XMLHttpRequest"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json()
|
||||||
|
self.assertTrue(data["success"])
|
||||||
|
self.assertEqual(data["asset_type"], "trafficlightprogramming")
|
||||||
|
self.assertTrue(data["asset_code"].startswith("K999-PRG-"))
|
||||||
|
|
||||||
|
created_prog = TrafficLightProgramming.objects.get(id=data["asset_id"])
|
||||||
|
self.assertEqual(created_prog.intersection, self.intersection)
|
||||||
|
self.assertEqual(created_prog.category, self.category)
|
||||||
|
|
||||||
|
def test_create_programming_via_create_asset_api(self):
|
||||||
|
from assets.models.trafficlights import TrafficLightProgramming
|
||||||
|
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("assets:create_asset_api"),
|
||||||
|
data=json.dumps({
|
||||||
|
"thematic_code": "trafficlights",
|
||||||
|
"category_id": self.category.id,
|
||||||
|
"location_type": "trafficlightintersection",
|
||||||
|
"location_id": self.intersection.id,
|
||||||
|
"asset_type": "trafficlightprogramming",
|
||||||
|
"code": "K999-PRG-API",
|
||||||
|
"data": {
|
||||||
|
"index": "05",
|
||||||
|
"plan": "Plan API",
|
||||||
|
"info": "Created from API"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
content_type="application/json",
|
||||||
|
HTTP_X_REQUESTED_WITH="XMLHttpRequest"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json()
|
||||||
|
self.assertTrue(data["success"])
|
||||||
|
self.assertEqual(data["asset_type"], "trafficlightprogramming")
|
||||||
|
|
||||||
|
prog = TrafficLightProgramming.objects.get(code="K999-PRG-API")
|
||||||
|
self.assertEqual(prog.intersection, self.intersection)
|
||||||
|
self.assertEqual(prog.index, "05")
|
||||||
|
self.assertEqual(prog.plan, "Plan API")
|
||||||
|
self.assertEqual(prog.info, "Created from API")
|
||||||
|
|
||||||
|
def test_update_programming_fields_via_api(self):
|
||||||
|
from assets.models.trafficlights import TrafficLightProgramming
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
prog = TrafficLightProgramming.objects.create(
|
||||||
|
code="K999-PRG-UPD",
|
||||||
|
intersection=self.intersection,
|
||||||
|
status="in_preparation",
|
||||||
|
index="01",
|
||||||
|
plan="Plan Init",
|
||||||
|
info="Initial Info"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
# Update index, plan, info, status, and dates
|
||||||
|
updates = {
|
||||||
|
"index": "02",
|
||||||
|
"plan": "Plan Updated",
|
||||||
|
"info": "Updated Info Notes",
|
||||||
|
"status": "active",
|
||||||
|
"commissioning_date": "2026-06-15",
|
||||||
|
"uncommissioning_date": "2027-01-01"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("assets:update_asset", kwargs={
|
||||||
|
"asset_model": "trafficlightprogramming",
|
||||||
|
"asset_id": prog.id
|
||||||
|
}),
|
||||||
|
data=json.dumps(updates),
|
||||||
|
content_type="application/json",
|
||||||
|
HTTP_X_REQUESTED_WITH="XMLHttpRequest"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200, f"Failed updating programming: {response.content}")
|
||||||
|
data = response.json()
|
||||||
|
self.assertTrue(data["success"])
|
||||||
|
|
||||||
|
prog.refresh_from_db()
|
||||||
|
self.assertEqual(prog.index, "02")
|
||||||
|
self.assertEqual(prog.plan, "Plan Updated")
|
||||||
|
self.assertEqual(prog.info, "Updated Info Notes")
|
||||||
|
self.assertEqual(prog.status, "active")
|
||||||
|
self.assertEqual(timezone.localtime(prog.commissioning_date).strftime("%Y-%m-%d"), "2026-06-15")
|
||||||
|
self.assertEqual(timezone.localtime(prog.uncommissioning_date).strftime("%Y-%m-%d"), "2027-01-01")
|
||||||
|
|
||||||
|
def test_programming_detail_view_renders_cms_fields(self):
|
||||||
|
from assets.models.trafficlights import TrafficLightProgramming
|
||||||
|
|
||||||
|
prog = TrafficLightProgramming.objects.create(
|
||||||
|
code="K999-PRG-VIEW",
|
||||||
|
intersection=self.intersection,
|
||||||
|
status="active",
|
||||||
|
index="03",
|
||||||
|
plan="Plan Detail",
|
||||||
|
info="Detailed Programming Info"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("assets:trafficlights_assets_detail", kwargs={
|
||||||
|
"asset_model": "trafficlightprogramming",
|
||||||
|
"asset_id": prog.id
|
||||||
|
})
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
content = response.content.decode("utf-8")
|
||||||
|
self.assertIn("K999-PRG-VIEW", content)
|
||||||
|
self.assertIn("Plan Detail", content)
|
||||||
|
self.assertIn("Detailed Programming Info", content)
|
||||||
|
self.assertIn("K999", content)
|
||||||
|
|
||||||
|
def test_intersection_detail_view_renders_programmations_tab(self):
|
||||||
|
from assets.models.trafficlights import TrafficLightProgramming
|
||||||
|
|
||||||
|
prog = TrafficLightProgramming.objects.create(
|
||||||
|
code="K999-PRG-TAB",
|
||||||
|
intersection=self.intersection,
|
||||||
|
status="active",
|
||||||
|
index="01",
|
||||||
|
plan="Plan Tab"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("assets:trafficlights_intersections_detail", kwargs={
|
||||||
|
"intersection_id": self.intersection.id
|
||||||
|
})
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
content = response.content.decode("utf-8")
|
||||||
|
self.assertIn("K999-PRG-TAB", content)
|
||||||
|
self.assertIn("assets-programmation", content)
|
||||||
|
self.assertIn("addProgrammingModal", content)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,7 @@ urlpatterns = [
|
||||||
path('trafficlights/intersections/<int:intersection_id>/document-config/', trafficlights.get_document_config, name='trafficlights_intersections_document_config'),
|
path('trafficlights/intersections/<int:intersection_id>/document-config/', trafficlights.get_document_config, name='trafficlights_intersections_document_config'),
|
||||||
path('trafficlights/intersections/<str:intersection_code>/', trafficlights.trafficlights_intersections_get_by_code, name='trafficlights_interstrafficlights_intersections_get_by_codeections_detail'),
|
path('trafficlights/intersections/<str:intersection_code>/', trafficlights.trafficlights_intersections_get_by_code, name='trafficlights_interstrafficlights_intersections_get_by_codeections_detail'),
|
||||||
path('trafficlights/intersections/<int:intersection_id>/add-document/', trafficlights.trafficlights_intersections_add_document, name='trafficlights_intersections_add_document'),
|
path('trafficlights/intersections/<int:intersection_id>/add-document/', trafficlights.trafficlights_intersections_add_document, name='trafficlights_intersections_add_document'),
|
||||||
|
path('trafficlights/intersections/<int:intersection_id>/add-programming/', trafficlights.trafficlights_add_programming, name='trafficlights_add_programming'),
|
||||||
path('trafficlights/intersections/<int:document_id>/delete-document/', trafficlights.trafficlights_intersections_delete_document, name='trafficlights_intersections_delete_document'),
|
path('trafficlights/intersections/<int:document_id>/delete-document/', trafficlights.trafficlights_intersections_delete_document, name='trafficlights_intersections_delete_document'),
|
||||||
path('trafficlights/assets/', trafficlights.trafficlights_assets, name='trafficlights_assets'),
|
path('trafficlights/assets/', trafficlights.trafficlights_assets, name='trafficlights_assets'),
|
||||||
path('trafficlights/assets/geojson/', trafficlights.trafficlights_assets_geojson, name='trafficlights_assets_geojson'),
|
path('trafficlights/assets/geojson/', trafficlights.trafficlights_assets_geojson, name='trafficlights_assets_geojson'),
|
||||||
|
|
|
||||||
|
|
@ -457,6 +457,13 @@ def _update_asset_field(asset, field_name, new_value, model_class):
|
||||||
parsed = datetime.strptime(str(new_value), "%Y-%m-%d")
|
parsed = datetime.strptime(str(new_value), "%Y-%m-%d")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return False, _("Format de date invalide pour %(field)s") % {"field": field_name}
|
return False, _("Format de date invalide pour %(field)s") % {"field": field_name}
|
||||||
|
if isinstance(field_obj, DateTimeField):
|
||||||
|
from datetime import datetime, time
|
||||||
|
from django.utils.timezone import make_aware, get_current_timezone, is_aware
|
||||||
|
if not isinstance(parsed, datetime):
|
||||||
|
parsed = datetime.combine(parsed, time.min)
|
||||||
|
if not is_aware(parsed):
|
||||||
|
parsed = make_aware(parsed, get_current_timezone())
|
||||||
setattr(asset, field_name, parsed)
|
setattr(asset, field_name, parsed)
|
||||||
else:
|
else:
|
||||||
setattr(asset, field_name, None)
|
setattr(asset, field_name, None)
|
||||||
|
|
@ -858,12 +865,13 @@ def create_asset(request, thematic_code, location_type, location_id):
|
||||||
'success': False,
|
'success': False,
|
||||||
'error': _("Le modèle d'asset est requis pour ce type d'asset")
|
'error': _("Le modèle d'asset est requis pour ce type d'asset")
|
||||||
}, status=400)
|
}, status=400)
|
||||||
except model_class._meta.get_field('model').related_model.DoesNotExist:
|
except Exception as e:
|
||||||
return JsonResponse({
|
# Check if it was a DoesNotExist on model_id
|
||||||
'success': False,
|
if model_id and 'DoesNotExist' in e.__class__.__name__:
|
||||||
'error': _("Modèle d'asset non trouvé")
|
return JsonResponse({
|
||||||
}, status=400)
|
'success': False,
|
||||||
except Exception:
|
'error': _("Modèle d'asset non trouvé")
|
||||||
|
}, status=400)
|
||||||
# No 'model' field on this asset type, that's OK
|
# No 'model' field on this asset type, that's OK
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
@ -896,6 +904,7 @@ def create_asset(request, thematic_code, location_type, location_id):
|
||||||
'success': True,
|
'success': True,
|
||||||
'message': _("Asset créé avec succès"),
|
'message': _("Asset créé avec succès"),
|
||||||
'asset_id': new_asset.id,
|
'asset_id': new_asset.id,
|
||||||
|
'asset_type': model_class._meta.model_name,
|
||||||
'asset_code': new_asset.code
|
'asset_code': new_asset.code
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -905,7 +914,10 @@ def _generate_asset_code(model_class, location):
|
||||||
location_code = getattr(location, 'code', 'AST')
|
location_code = getattr(location, 'code', 'AST')
|
||||||
|
|
||||||
# Get count of existing assets at this location
|
# Get count of existing assets at this location
|
||||||
model_name = model_class._meta.model_name.upper()[:3]
|
if model_class._meta.model_name.lower() == 'trafficlightprogramming':
|
||||||
|
model_name = 'PRG'
|
||||||
|
else:
|
||||||
|
model_name = model_class._meta.model_name.upper()[:3]
|
||||||
base_code = f"{location_code}-{model_name}"
|
base_code = f"{location_code}-{model_name}"
|
||||||
|
|
||||||
# Find next available number
|
# Find next available number
|
||||||
|
|
@ -1369,6 +1381,14 @@ CATEGORY_ASSET_MAPPING = {
|
||||||
'TL_DETEC_OTHER': ('TrafficLightDetector', 'TrafficLightDetectorModel'),
|
'TL_DETEC_OTHER': ('TrafficLightDetector', 'TrafficLightDetectorModel'),
|
||||||
'TL_BORNIER': ('TrafficLightAccessory', 'TrafficLightAccessoryModel'),
|
'TL_BORNIER': ('TrafficLightAccessory', 'TrafficLightAccessoryModel'),
|
||||||
'TL_DIVERS': ('TrafficLightAccessory', 'TrafficLightAccessoryModel'),
|
'TL_DIVERS': ('TrafficLightAccessory', 'TrafficLightAccessoryModel'),
|
||||||
|
'trafficlights_programming': ('TrafficLightProgramming', None),
|
||||||
|
'trafficlights_programmation': ('TrafficLightProgramming', None),
|
||||||
|
'trafficlights_tl_programming': ('TrafficLightProgramming', None),
|
||||||
|
'trafficlights_tl_programmation': ('TrafficLightProgramming', None),
|
||||||
|
'tl_programming': ('TrafficLightProgramming', None),
|
||||||
|
'tl_programmation': ('TrafficLightProgramming', None),
|
||||||
|
'programming': ('TrafficLightProgramming', None),
|
||||||
|
'programmation': ('TrafficLightProgramming', None),
|
||||||
# Public Lighting categories
|
# Public Lighting categories
|
||||||
'publiclighting_pole': ('PublicLightingPole', 'PublicLightingPoleModel'),
|
'publiclighting_pole': ('PublicLightingPole', 'PublicLightingPoleModel'),
|
||||||
'LI_poles': ('PublicLightingPole', 'PublicLightingPoleModel'),
|
'LI_poles': ('PublicLightingPole', 'PublicLightingPoleModel'),
|
||||||
|
|
@ -1441,7 +1461,7 @@ def _import_asset_classes(thematic_code, class_names):
|
||||||
import importlib
|
import importlib
|
||||||
module = importlib.import_module(module_name)
|
module = importlib.import_module(module_name)
|
||||||
asset_class = getattr(module, asset_class_name, None)
|
asset_class = getattr(module, asset_class_name, None)
|
||||||
model_class = getattr(module, model_class_name, None)
|
model_class = getattr(module, model_class_name, None) if model_class_name else None
|
||||||
return asset_class, model_class
|
return asset_class, model_class
|
||||||
except (ImportError, AttributeError):
|
except (ImportError, AttributeError):
|
||||||
return None, None
|
return None, None
|
||||||
|
|
@ -1652,7 +1672,8 @@ def create_asset_api(request):
|
||||||
if hasattr(new_asset, 'category'):
|
if hasattr(new_asset, 'category'):
|
||||||
new_asset.category = category
|
new_asset.category = category
|
||||||
|
|
||||||
new_asset.model = asset_model_instance
|
if hasattr(new_asset, 'model'):
|
||||||
|
new_asset.model = asset_model_instance
|
||||||
new_asset.status = 'active'
|
new_asset.status = 'active'
|
||||||
new_asset.validation_status = 'to_be_validated'
|
new_asset.validation_status = 'to_be_validated'
|
||||||
new_asset.installation_date = timezone.now()
|
new_asset.installation_date = timezone.now()
|
||||||
|
|
@ -1709,6 +1730,13 @@ def create_asset_api(request):
|
||||||
except TrafficLightPole.DoesNotExist:
|
except TrafficLightPole.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Set additional data
|
||||||
|
extra_data = data.get('data', {})
|
||||||
|
if isinstance(extra_data, dict):
|
||||||
|
for field_name, value in extra_data.items():
|
||||||
|
if hasattr(new_asset, field_name) and field_name not in ('id', 'code'):
|
||||||
|
setattr(new_asset, field_name, value)
|
||||||
|
|
||||||
# Assign position from location / hierarchy
|
# Assign position from location / hierarchy
|
||||||
location = getattr(new_asset, 'location', None) or getattr(new_asset, 'intersection', None)
|
location = getattr(new_asset, 'location', None) or getattr(new_asset, 'intersection', None)
|
||||||
_assign_default_position(new_asset, location, thematic_code)
|
_assign_default_position(new_asset, location, thematic_code)
|
||||||
|
|
@ -1749,6 +1777,7 @@ def create_asset_api(request):
|
||||||
'success': True,
|
'success': True,
|
||||||
'message': success_message,
|
'message': success_message,
|
||||||
'asset_id': new_asset.id,
|
'asset_id': new_asset.id,
|
||||||
|
'asset_type': asset_class._meta.model_name,
|
||||||
'asset_code': new_asset.code,
|
'asset_code': new_asset.code,
|
||||||
'intervention_linked': intervention_linked,
|
'intervention_linked': intervention_linked,
|
||||||
'operations_created': operations_created,
|
'operations_created': operations_created,
|
||||||
|
|
@ -1797,11 +1826,13 @@ def get_available_locations_for_asset(request, thematic_code, asset_id):
|
||||||
elif thematic_code == 'trafficlights':
|
elif thematic_code == 'trafficlights':
|
||||||
from assets.models.trafficlights import (
|
from assets.models.trafficlights import (
|
||||||
TrafficLightPole, TrafficLightCable, TrafficLightLantern,
|
TrafficLightPole, TrafficLightCable, TrafficLightLantern,
|
||||||
TrafficLightController, TrafficLightDetector, TrafficLightElectricalCabinet
|
TrafficLightController, TrafficLightDetector, TrafficLightElectricalCabinet,
|
||||||
|
TrafficLightProgramming, TrafficLightRadar, TrafficLightRadarPole, TrafficLightAccessory
|
||||||
)
|
)
|
||||||
# Try each possible class
|
# Try each possible class
|
||||||
for cls in [TrafficLightPole, TrafficLightCable, TrafficLightLantern,
|
for cls in [TrafficLightPole, TrafficLightCable, TrafficLightLantern,
|
||||||
TrafficLightController, TrafficLightDetector, TrafficLightElectricalCabinet]:
|
TrafficLightController, TrafficLightDetector, TrafficLightElectricalCabinet,
|
||||||
|
TrafficLightProgramming, TrafficLightRadar, TrafficLightRadarPole, TrafficLightAccessory]:
|
||||||
try:
|
try:
|
||||||
asset = cls.objects.get(pk=asset_id)
|
asset = cls.objects.get(pk=asset_id)
|
||||||
break
|
break
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ 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
|
from assets.models import TrafficLightIntersection, TrafficLightIntersectionDocument, TrafficLightController, TrafficLightPole, TrafficLightCable, TrafficLightLantern, TrafficLightDetector, TrafficLightElectricalCabinet, TrafficLightRadar, TrafficLightRadarPole, TrafficLightProgramming
|
||||||
from assets.models import AssetCategory, TrafficLightCableModel, TrafficLightControllerModel, TrafficLightDetectorModel, TrafficLightLanternModel, TrafficLightPoleModel, TrafficLightElectricalCabinetModel, TrafficLightContract, TrafficLightRadarModel
|
from assets.models import 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
|
||||||
|
|
@ -758,6 +758,97 @@ def trafficlights_intersections_delete_document(request, document_id):
|
||||||
return redirect('assets:trafficlights_intersections_detail', intersection_id=intersection_id)
|
return redirect('assets:trafficlights_intersections_detail', intersection_id=intersection_id)
|
||||||
|
|
||||||
|
|
||||||
|
@require_POST
|
||||||
|
@check_thematic_access('trafficlights')
|
||||||
|
def trafficlights_add_programming(request, intersection_id):
|
||||||
|
intersection = get_object_or_404(TrafficLightIntersection, pk=intersection_id)
|
||||||
|
|
||||||
|
# Check create permission
|
||||||
|
thematic = Thematic.objects.get(code='trafficlights')
|
||||||
|
allowed_actions = get_allowed_actions_for_thematic(request.user, thematic)
|
||||||
|
if 'create' not in allowed_actions:
|
||||||
|
return JsonResponse({'success': False, 'error': _("Permission refusée")}, status=403)
|
||||||
|
|
||||||
|
from assets.views.asset_editing import _generate_asset_code
|
||||||
|
from django.db import transaction
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
code = request.POST.get('code', '').strip()
|
||||||
|
name_fr = request.POST.get('name_fr', '').strip()
|
||||||
|
name_nl = request.POST.get('name_nl', '').strip()
|
||||||
|
index = request.POST.get('index', '').strip()
|
||||||
|
plan = request.POST.get('plan', '').strip()
|
||||||
|
commissioning_date_str = request.POST.get('commissioning_date', '').strip()
|
||||||
|
uncommissioning_date_str = request.POST.get('uncommissioning_date', '').strip()
|
||||||
|
status = request.POST.get('status', 'active').strip() or 'active'
|
||||||
|
info = request.POST.get('info', '').strip()
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
category = AssetCategory.objects.filter(thematic=thematic, code__in=['TL_PROGRAMMING', 'trafficlights_programming']).first()
|
||||||
|
|
||||||
|
prog = TrafficLightProgramming(
|
||||||
|
intersection=intersection,
|
||||||
|
category=category,
|
||||||
|
status=status,
|
||||||
|
validation_status='to_be_validated',
|
||||||
|
name_fr=name_fr if name_fr else None,
|
||||||
|
name_nl=name_nl if name_nl else None,
|
||||||
|
index=index if index else None,
|
||||||
|
plan=plan if plan else None,
|
||||||
|
info=info if info else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not code:
|
||||||
|
prog.code = _generate_asset_code(TrafficLightProgramming, intersection)
|
||||||
|
else:
|
||||||
|
if TrafficLightProgramming.objects.filter(code=code).exists():
|
||||||
|
return JsonResponse({'success': False, 'error': _("Un équipement avec ce code existe déjà")}, status=400)
|
||||||
|
prog.code = code
|
||||||
|
|
||||||
|
if commissioning_date_str:
|
||||||
|
from django.utils.dateparse import parse_datetime, parse_date
|
||||||
|
parsed = parse_datetime(commissioning_date_str) or parse_date(commissioning_date_str)
|
||||||
|
if parsed:
|
||||||
|
if not getattr(parsed, 'tzinfo', None) and hasattr(parsed, 'hour'):
|
||||||
|
parsed = make_aware(parsed, get_current_timezone())
|
||||||
|
prog.commissioning_date = parsed
|
||||||
|
prog.installation_date = parsed
|
||||||
|
else:
|
||||||
|
prog.installation_date = timezone.now()
|
||||||
|
|
||||||
|
if uncommissioning_date_str:
|
||||||
|
from django.utils.dateparse import parse_datetime, parse_date
|
||||||
|
parsed = parse_datetime(uncommissioning_date_str) or parse_date(uncommissioning_date_str)
|
||||||
|
if parsed:
|
||||||
|
if not getattr(parsed, 'tzinfo', None) and hasattr(parsed, 'hour'):
|
||||||
|
parsed = make_aware(parsed, get_current_timezone())
|
||||||
|
prog.uncommissioning_date = parsed
|
||||||
|
|
||||||
|
prog.save()
|
||||||
|
|
||||||
|
# Handle optional programming file upload
|
||||||
|
uploaded_file = request.FILES.get('file')
|
||||||
|
if uploaded_file:
|
||||||
|
doc = TrafficLightIntersectionDocument(
|
||||||
|
intersection=intersection,
|
||||||
|
programming=prog,
|
||||||
|
document_type='programming_file',
|
||||||
|
uploaded_by=request.user,
|
||||||
|
)
|
||||||
|
doc.file.save(uploaded_file.name, uploaded_file, save=True)
|
||||||
|
|
||||||
|
if request.headers.get('x-requested-with') != 'XMLHttpRequest' and request.content_type and 'multipart/form-data' in request.content_type:
|
||||||
|
return redirect(f"{reverse('assets:trafficlights_intersections_detail', kwargs={'intersection_id': intersection.id})}#assets-programmation")
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'success': True,
|
||||||
|
'message': _("Programmation ajoutée avec succès"),
|
||||||
|
'asset_id': prog.id,
|
||||||
|
'asset_code': prog.code,
|
||||||
|
'redirect_url': prog.get_absolute_url()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@check_thematic_access('trafficlights')
|
@check_thematic_access('trafficlights')
|
||||||
def fetch_document_history(request):
|
def fetch_document_history(request):
|
||||||
intersection_id = request.GET.get('intersection_id')
|
intersection_id = request.GET.get('intersection_id')
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue