Compare commits
No commits in common. "73738b675739341cf276965946627e26d61d16bc" and "d73a9e482dd8f345cb53c5c98bd3fe60ac11b329" have entirely different histories.
73738b6757
...
d73a9e482d
16 changed files with 27 additions and 1576 deletions
|
|
@ -1,125 +0,0 @@
|
|||
"""
|
||||
Management command to load baseline asset categories (from loko-bm) and
|
||||
associate existing asset models and assets with their corresponding categories.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.management.base import BaseCommand
|
||||
from common.models import Thematic
|
||||
from assets.models import (
|
||||
AssetCategory,
|
||||
CleanLitterBin, CleanLitterBinModel,
|
||||
CleanGlassContainer, CleanGlassContainerModel,
|
||||
RoadDrain, RoadDrainModel,
|
||||
Roadway, RoadwayModel,
|
||||
RoadSidewalk, RoadSidewalkModel,
|
||||
RoadFurniture, RoadFurnitureModel,
|
||||
StructureAssetModel,
|
||||
)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Charge les catégories d'assets de base (définies dans loko-bm) et lie les modèles existants."
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--file',
|
||||
type=str,
|
||||
help="Chemin vers le fichier JSON des catégories (défaut: common/fixtures/asset_categories.json)",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
# 1. Localiser le fichier JSON
|
||||
json_path = options.get('file')
|
||||
candidates = []
|
||||
if not json_path:
|
||||
base_dir = getattr(settings, 'BASE_DIR', None)
|
||||
candidates = [
|
||||
os.path.join(base_dir, 'common', 'fixtures', 'asset_categories.json') if base_dir else None,
|
||||
os.path.join(base_dir, 'loko', 'common', 'fixtures', 'asset_categories.json') if base_dir else None,
|
||||
'/app/common/fixtures/asset_categories.json',
|
||||
'/app/loko/common/fixtures/asset_categories.json',
|
||||
os.path.join(os.path.dirname(__file__), '..', '..', '..', 'common', 'fixtures', 'asset_categories.json'),
|
||||
]
|
||||
for c in candidates:
|
||||
if c and os.path.exists(c):
|
||||
json_path = os.path.abspath(c)
|
||||
break
|
||||
|
||||
if not json_path or not os.path.exists(json_path):
|
||||
self.stderr.write(self.style.ERROR(f"Fichier asset_categories.json introuvable. Candidats testés: {candidates}"))
|
||||
return
|
||||
|
||||
self.stdout.write(f"Chargement des catégories depuis : {json_path}")
|
||||
with open(json_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
created_count = 0
|
||||
updated_count = 0
|
||||
|
||||
# Cache thématiques
|
||||
thematics_by_code = {t.code: t for t in Thematic.objects.all()}
|
||||
|
||||
# 2. Création / Mise à jour des catégories
|
||||
for item in data:
|
||||
code = item['code']
|
||||
name_fr = item['name_fr']
|
||||
name_nl = item.get('name_nl') or name_fr
|
||||
thematic_code = item.get('thematic_code')
|
||||
thematic = thematics_by_code.get(thematic_code)
|
||||
|
||||
cat, created = AssetCategory.objects.update_or_create(
|
||||
code=code,
|
||||
defaults={
|
||||
'name_fr': name_fr,
|
||||
'name_nl': name_nl,
|
||||
'thematic': thematic,
|
||||
}
|
||||
)
|
||||
if created:
|
||||
created_count += 1
|
||||
else:
|
||||
updated_count += 1
|
||||
|
||||
# Modèles Django autorisés (ContentType)
|
||||
allowed_models = item.get('allowed_models', [])
|
||||
if allowed_models:
|
||||
cts = ContentType.objects.filter(model__in=allowed_models)
|
||||
cat.allowed_models.set(cts)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f"✅ Catégories d'assets : {created_count} créées, {updated_count} mises à jour (Total: {len(data)})."
|
||||
))
|
||||
|
||||
# 3. Association automatique des modèles d'assets existants aux catégories correspondantes
|
||||
self.link_models_to_categories()
|
||||
|
||||
def link_models_to_categories(self):
|
||||
links = [
|
||||
('CleanLitterBinModel', CleanLitterBinModel, 'CL_BASKET'),
|
||||
('CleanLitterBin', CleanLitterBin, 'CL_BASKET'),
|
||||
('CleanGlassContainerModel', CleanGlassContainerModel, 'CL_BUBBLE'),
|
||||
('CleanGlassContainer', CleanGlassContainer, 'CL_BUBBLE'),
|
||||
('RoadDrainModel', RoadDrainModel, 'RD_DRAIN'),
|
||||
('RoadDrain', RoadDrain, 'RD_DRAIN'),
|
||||
('RoadwayModel', RoadwayModel, 'RD_ROADWAY'),
|
||||
('Roadway', Roadway, 'RD_ROADWAY'),
|
||||
('RoadSidewalkModel', RoadSidewalkModel, 'RD_SIDEWALK'),
|
||||
('RoadSidewalk', RoadSidewalk, 'RD_SIDEWALK'),
|
||||
('RoadFurnitureModel', RoadFurnitureModel, 'RD_STREET_FURNITURE'),
|
||||
('RoadFurniture', RoadFurniture, 'RD_STREET_FURNITURE'),
|
||||
('StructureAssetModel', StructureAssetModel, 'TU_CIVIL_ENGINEERING'),
|
||||
]
|
||||
|
||||
for name, model_cls, cat_code in links:
|
||||
cat = AssetCategory.objects.filter(code=cat_code).first()
|
||||
if not cat:
|
||||
continue
|
||||
try:
|
||||
updated = model_cls.objects.filter(category__isnull=True).update(category=cat)
|
||||
if updated:
|
||||
self.stdout.write(f" • {name} : {updated} enregistrement(s) lié(s) à la catégorie {cat.name_fr} ({cat_code}).")
|
||||
except Exception as e:
|
||||
self.stderr.write(f" ⚠️ Erreur liaison {name} -> {cat_code}: {e}")
|
||||
|
|
@ -796,92 +796,3 @@ def can_edit_exceptional_transport(user):
|
|||
and getattr(user_config, 'exceptional_transport_can_edit', False)
|
||||
)
|
||||
|
||||
|
||||
|
||||
def get_user_accessible_municipalities(user):
|
||||
"""
|
||||
Retourne le queryset des communes (Municipality) auxquelles l'utilisateur a accès.
|
||||
- Si superuser ou (utilisateur interne sans limitation aux contrats) :
|
||||
retourne toutes les communes (Municipality.objects.all()).
|
||||
- Si utilisateur externe ou avec limit_assets_to_contracts=True :
|
||||
retourne uniquement les communes associées à ses contrats accessibles
|
||||
(via Contract.municipalities ou localisations liées).
|
||||
"""
|
||||
from common.models import Municipality, UserConfig, UserContractAccess
|
||||
from django.utils.translation import get_language
|
||||
|
||||
if not user or not user.is_authenticated:
|
||||
return Municipality.objects.none()
|
||||
|
||||
if getattr(user, 'is_superuser', False):
|
||||
lang = get_language() or 'fr'
|
||||
order_field = 'name_nl' if lang.startswith('nl') else 'name_fr'
|
||||
return Municipality.objects.all().order_by(order_field)
|
||||
|
||||
user_config = getattr(user, 'config', None)
|
||||
if not user_config:
|
||||
try:
|
||||
user_config = UserConfig.objects.get(user=user)
|
||||
except UserConfig.DoesNotExist:
|
||||
return Municipality.objects.none()
|
||||
|
||||
if user_config.has_role('admin') or user_config.has_role('top_manager'):
|
||||
lang = get_language() or 'fr'
|
||||
order_field = 'name_nl' if lang.startswith('nl') else 'name_fr'
|
||||
return Municipality.objects.all().order_by(order_field)
|
||||
|
||||
is_external = not user_config.is_intern
|
||||
must_limit = is_external or user_config.limit_assets_to_contracts
|
||||
|
||||
if not must_limit:
|
||||
lang = get_language() or 'fr'
|
||||
order_field = 'name_nl' if lang.startswith('nl') else 'name_fr'
|
||||
return Municipality.objects.all().order_by(order_field)
|
||||
|
||||
# Utilisateur restreint par contrats :
|
||||
accessible_contract_ids = set(
|
||||
UserContractAccess.objects.filter(
|
||||
user_config=user_config,
|
||||
can_view_assets=True
|
||||
).values_list('contract_id', flat=True)
|
||||
)
|
||||
if user_config.default_contract_id:
|
||||
accessible_contract_ids.add(user_config.default_contract_id)
|
||||
|
||||
if not accessible_contract_ids:
|
||||
return Municipality.objects.none()
|
||||
|
||||
# Communes associées via ManyToMany Contract.municipalities
|
||||
municipality_ids = set(
|
||||
Municipality.objects.filter(
|
||||
contracts__id__in=accessible_contract_ids
|
||||
).values_list('id', flat=True)
|
||||
)
|
||||
|
||||
# Communes associées via les localisations liées aux contrats
|
||||
try:
|
||||
from assets.models import CleanLocationContract
|
||||
clean_muni_ids = CleanLocationContract.objects.filter(
|
||||
contract_id__in=accessible_contract_ids,
|
||||
location__municipality__isnull=False
|
||||
).values_list('location__municipality_id', flat=True)
|
||||
municipality_ids.update(clean_muni_ids)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
from assets.models import TrafficLightContract
|
||||
tl_muni_ids = TrafficLightContract.objects.filter(
|
||||
contract_id__in=accessible_contract_ids,
|
||||
intersection__municipality__isnull=False
|
||||
).values_list('intersection__municipality_id', flat=True)
|
||||
municipality_ids.update(tl_muni_ids)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not municipality_ids:
|
||||
return Municipality.objects.none()
|
||||
|
||||
lang = get_language() or 'fr'
|
||||
order_field = 'name_nl' if lang.startswith('nl') else 'name_fr'
|
||||
return Municipality.objects.filter(id__in=municipality_ids).order_by(order_field)
|
||||
|
|
|
|||
|
|
@ -2313,185 +2313,6 @@ class TrafficLightCrossPlanImportTest(TestCase):
|
|||
if os.path.exists(tmp_path):
|
||||
os.remove(tmp_path)
|
||||
|
||||
def test_integrate_cross_plan_update_all(self):
|
||||
import openpyxl
|
||||
import tempfile
|
||||
from django.contrib.gis.geos import Point as DjangoPoint
|
||||
from assets.models import TrafficLightPole, TrafficLightPoleModel, TrafficLightLantern
|
||||
|
||||
pole_model = TrafficLightPoleModel.objects.first()
|
||||
|
||||
# 1. Existing pole B01 in DB (will be updated)
|
||||
existing_pole = TrafficLightPole.objects.create(
|
||||
intersection=self.intersection,
|
||||
code="SWB01_B01",
|
||||
name_fr="Ancien B01",
|
||||
geom=DjangoPoint(149250.0, 170550.0, srid=3812),
|
||||
lon=4.35,
|
||||
lat=50.84,
|
||||
model=pole_model,
|
||||
status="active"
|
||||
)
|
||||
# 2. Existing untouched pole B99 in DB (should remain active after update_all)
|
||||
untouched_pole = TrafficLightPole.objects.create(
|
||||
intersection=self.intersection,
|
||||
code="SWB01_B99",
|
||||
name_fr="Poteau Untouched",
|
||||
model=pole_model,
|
||||
status="active"
|
||||
)
|
||||
|
||||
# 3. Create cross plan workbook containing B01 (with new coordinates) and B02 (new pole)
|
||||
wb = openpyxl.Workbook()
|
||||
ws = wb.active
|
||||
ws.title = "kruisjesplan"
|
||||
ws.cell(row=1, column=1, value="Sleutel:")
|
||||
ws.cell(row=1, column=2, value="SWB01")
|
||||
ws.cell(row=5, column=1, value="Câble 01 SVAVB")
|
||||
ws.cell(row=6, column=3, value="richting")
|
||||
ws.cell(row=6, column=4, value="kringen")
|
||||
ws.cell(row=7, column=5, value="B01")
|
||||
ws.cell(row=7, column=6, value="B02")
|
||||
ws.cell(row=8, column=1, value="com")
|
||||
ws.cell(row=8, column=2, value="1")
|
||||
ws.cell(row=8, column=3, value="A")
|
||||
ws.cell(row=8, column=5, value="X")
|
||||
ws.cell(row=8, column=6, value="X")
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as tmp:
|
||||
tmp_path = tmp.name
|
||||
wb.save(tmp_path)
|
||||
|
||||
try:
|
||||
self.client.force_login(self.user)
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
|
||||
with open(tmp_path, 'rb') as f:
|
||||
uploaded_file = SimpleUploadedFile("update_plan.xlsx", f.read(), 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)
|
||||
|
||||
# Call integrate with global_action="update_all"
|
||||
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"],
|
||||
"global_action": "update_all",
|
||||
"name_pole_SWB01_B01": "B01 Mis à jour",
|
||||
"x_3812_pole_SWB01_B01": "149260.0",
|
||||
"y_3812_pole_SWB01_B01": "170560.0",
|
||||
"lon_pole_SWB01_B01": "4.351",
|
||||
"lat_pole_SWB01_B01": "50.841",
|
||||
"name_pole_SWB01_B02": "Nouveau B02",
|
||||
"x_3812_pole_SWB01_B02": "149300.0",
|
||||
"y_3812_pole_SWB01_B02": "170600.0",
|
||||
})
|
||||
self.assertEqual(response_integrate.status_code, 302)
|
||||
|
||||
# Verify:
|
||||
# - existing_pole should still have status="active", ID unchanged, name updated, geometry updated
|
||||
existing_pole.refresh_from_db()
|
||||
self.assertEqual(existing_pole.status, "active")
|
||||
self.assertEqual(existing_pole.name_fr, "B01 Mis à jour")
|
||||
self.assertEqual(existing_pole.geom.x, 149260.0)
|
||||
self.assertEqual(existing_pole.geom.y, 170560.0)
|
||||
|
||||
# - untouched_pole should still be active
|
||||
untouched_pole.refresh_from_db()
|
||||
self.assertEqual(untouched_pole.status, "active")
|
||||
|
||||
# - new pole B02 should be created active
|
||||
new_pole = TrafficLightPole.objects.filter(intersection=self.intersection, code="SWB01_B02", status="active").first()
|
||||
self.assertIsNotNone(new_pole)
|
||||
self.assertEqual(new_pole.name_fr, "Nouveau B02")
|
||||
self.assertEqual(new_pole.geom.x, 149300.0)
|
||||
|
||||
# - lantern on B01 and lantern on B02 should be active
|
||||
l1 = TrafficLightLantern.objects.filter(pole=existing_pole, status="active").first()
|
||||
self.assertIsNotNone(l1)
|
||||
l2 = TrafficLightLantern.objects.filter(pole=new_pole, status="active").first()
|
||||
self.assertIsNotNone(l2)
|
||||
finally:
|
||||
import os
|
||||
if os.path.exists(tmp_path):
|
||||
os.remove(tmp_path)
|
||||
|
||||
def test_import_cross_plan_dxf_geometry_diff(self):
|
||||
import openpyxl
|
||||
import tempfile
|
||||
from django.contrib.gis.geos import Point as DjangoPoint
|
||||
from unittest.mock import patch
|
||||
from assets.models import TrafficLightPole, TrafficLightPoleModel
|
||||
|
||||
pole_model = TrafficLightPoleModel.objects.first()
|
||||
|
||||
# Existing pole B01 with initial coordinates in DB
|
||||
existing_pole = TrafficLightPole.objects.create(
|
||||
intersection=self.intersection,
|
||||
code="SWB01_B01",
|
||||
geom=DjangoPoint(149200.0, 170500.0, srid=3812),
|
||||
lon=4.350,
|
||||
lat=50.840,
|
||||
model=pole_model,
|
||||
status="active"
|
||||
)
|
||||
|
||||
# Cross plan workbook with pole B01
|
||||
wb = openpyxl.Workbook()
|
||||
ws = wb.active
|
||||
ws.title = "kruisjesplan"
|
||||
ws.cell(row=1, column=1, value="Sleutel:")
|
||||
ws.cell(row=1, column=2, value="SWB01")
|
||||
ws.cell(row=5, column=1, value="Câble 01 SVAVB")
|
||||
ws.cell(row=6, column=3, value="richting")
|
||||
ws.cell(row=6, column=4, value="kringen")
|
||||
ws.cell(row=7, column=5, value="B01")
|
||||
ws.cell(row=8, column=3, value="A")
|
||||
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)
|
||||
|
||||
# Mock DXF poles returned with a 2.5m shift (x + 2.0, y + 1.5 -> distance = 2.5m)
|
||||
mock_dxf_poles = [{
|
||||
'label': 'B01',
|
||||
'lon': 4.35003,
|
||||
'lat': 50.84002,
|
||||
'x_3812': 149202.0,
|
||||
'y_3812': 170501.5
|
||||
}]
|
||||
|
||||
try:
|
||||
self.client.force_login(self.user)
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
|
||||
with open(tmp_path, 'rb') as f:
|
||||
uploaded_file = SimpleUploadedFile("dxf_diff_plan.xlsx", f.read(), content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||||
|
||||
with patch('assets.views.trafficlights.resolve_dxf_file_path', return_value='/dummy/path.dxf'), \
|
||||
patch('assets.views.trafficlights.parse_dxf_poles', return_value=mock_dxf_poles):
|
||||
url_import = reverse("assets:import_cross_plan", args=[self.intersection.id])
|
||||
response = self.client.post(url_import, {
|
||||
"cross_plan_file_upload": uploaded_file,
|
||||
"selected_dxf": "test.dxf"
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
preview_poles = response.context["poles"]
|
||||
b01 = next(p for p in preview_poles if p["code"] == "SWB01_B01")
|
||||
self.assertTrue(b01["geom_modified"])
|
||||
self.assertEqual(b01["distance_diff"], 2.5)
|
||||
self.assertEqual(b01["status"], "modified")
|
||||
self.assertEqual(b01["db_x_3812"], 149200.0)
|
||||
self.assertEqual(b01["new_x_3812"], 149202.0)
|
||||
self.assertTrue(any("Position modifiée (DXF)" in d for d in b01["details_diff"]))
|
||||
finally:
|
||||
import os
|
||||
if os.path.exists(tmp_path):
|
||||
os.remove(tmp_path)
|
||||
|
||||
|
||||
class AssetDefaultPositionTest(TestCase):
|
||||
def test_assign_default_position_hierarchy(self):
|
||||
|
|
@ -4319,84 +4140,3 @@ class ExportAssetsTest(TestCase):
|
|||
|
||||
|
||||
|
||||
|
||||
|
||||
class UserAccessibleMunicipalitiesTest(TestCase):
|
||||
def setUp(self):
|
||||
from common.models import Municipality, UserConfig, UserContractAccess, Thematic, UserThematics
|
||||
from contracts.models import Company, Contract
|
||||
self.muni_a = Municipality.objects.create(name_fr="Commune A", name_nl="Gemeente A", fid="1001")
|
||||
self.muni_b = Municipality.objects.create(name_fr="Commune B", name_nl="Gemeente B", fid="1002")
|
||||
self.muni_c = Municipality.objects.create(name_fr="Commune C", name_nl="Gemeente C", fid="1003")
|
||||
|
||||
self.superuser = User.objects.create_superuser(username="admin_super", password="pass123")
|
||||
self.user_intern = User.objects.create_user(username="user_intern", password="pass123")
|
||||
UserConfig.objects.create(user=self.user_intern, is_intern=True, limit_assets_to_contracts=False)
|
||||
|
||||
self.user_ext = User.objects.create_user(username="user_ext", password="pass123")
|
||||
self.ext_config = UserConfig.objects.create(user=self.user_ext, is_intern=False, limit_assets_to_contracts=True)
|
||||
|
||||
self.company = Company.objects.create(name="Test Company")
|
||||
self.contract = Contract.objects.create(contract_number="CTR-001", company=self.company, start_date="2026-01-01", end_date="2026-12-31")
|
||||
self.contract.municipalities.add(self.muni_a, self.muni_b)
|
||||
|
||||
UserContractAccess.objects.create(
|
||||
user_config=self.ext_config,
|
||||
contract=self.contract,
|
||||
can_view_assets=True
|
||||
)
|
||||
|
||||
thematic_roads, _ = Thematic.objects.get_or_create(code="roads", defaults={"name_fr": "Voiries"})
|
||||
UserThematics.objects.create(user_config=self.ext_config, thematic=thematic_roads, can_view_assets=True)
|
||||
|
||||
def test_superuser_sees_all_municipalities(self):
|
||||
from assets.permissions import get_user_accessible_municipalities
|
||||
munis = list(get_user_accessible_municipalities(self.superuser))
|
||||
self.assertEqual(len(munis), 3)
|
||||
|
||||
def test_intern_unrestricted_sees_all_municipalities(self):
|
||||
from assets.permissions import get_user_accessible_municipalities
|
||||
munis = list(get_user_accessible_municipalities(self.user_intern))
|
||||
self.assertEqual(len(munis), 3)
|
||||
|
||||
def test_restricted_user_sees_only_contracted_municipalities(self):
|
||||
from assets.permissions import get_user_accessible_municipalities
|
||||
munis = list(get_user_accessible_municipalities(self.user_ext))
|
||||
muni_ids = {m.id for m in munis}
|
||||
self.assertIn(self.muni_a.id, muni_ids)
|
||||
self.assertIn(self.muni_b.id, muni_ids)
|
||||
self.assertNotIn(self.muni_c.id, muni_ids)
|
||||
|
||||
def test_restricted_user_without_contracts_sees_none(self):
|
||||
from assets.permissions import get_user_accessible_municipalities
|
||||
user_empty = User.objects.create_user(username="user_empty", password="pass123")
|
||||
UserConfig.objects.create(user=user_empty, is_intern=False, limit_assets_to_contracts=True)
|
||||
munis = list(get_user_accessible_municipalities(user_empty))
|
||||
self.assertEqual(len(munis), 0)
|
||||
|
||||
def test_build_conditional_filter_config_restricted(self):
|
||||
from assets.views.core import build_conditional_filter_config
|
||||
thematic_roads = Thematic.objects.get(code="roads")
|
||||
cfg = build_conditional_filter_config([thematic_roads], user=self.user_ext)
|
||||
roads_filters = cfg.get('roads', [])
|
||||
muni_filter = next((f for f in roads_filters if f['key'] == 'municipality'), None)
|
||||
self.assertIsNotNone(muni_filter)
|
||||
opt_values = {opt['value'] for opt in muni_filter['options']}
|
||||
self.assertIn(str(self.muni_a.id), opt_values)
|
||||
self.assertIn(str(self.muni_b.id), opt_values)
|
||||
self.assertNotIn(str(self.muni_c.id), opt_values)
|
||||
|
||||
def test_roads_streets_geojson_restriction(self):
|
||||
from assets.models.roads import RoadStreet
|
||||
from django.urls import reverse
|
||||
|
||||
street_a = RoadStreet.objects.create(code="ST-A", name_fr="Rue A", municipality=self.muni_a)
|
||||
street_c = RoadStreet.objects.create(code="ST-C", name_fr="Rue C", municipality=self.muni_c)
|
||||
|
||||
self.client.force_login(self.user_ext)
|
||||
response = self.client.get(reverse('assets:roads_streets_geojson'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = response.json()
|
||||
feature_ids = [f['id'] for f in data.get('features', [])]
|
||||
self.assertIn(street_a.id, feature_ids)
|
||||
self.assertNotIn(street_c.id, feature_ids)
|
||||
|
|
|
|||
|
|
@ -65,8 +65,12 @@ def artworks_index(request):
|
|||
"icon": next((t.icon for t in thematics if t.code == thematic), None),
|
||||
}
|
||||
|
||||
from assets.permissions import get_user_accessible_municipalities
|
||||
municipalities = get_user_accessible_municipalities(request.user)
|
||||
municipality_order_field = 'name_nl' if lang == 'nl' else 'name_fr'
|
||||
municipalities = (
|
||||
Municipality.objects
|
||||
.all()
|
||||
.order_by(municipality_order_field)
|
||||
)
|
||||
municipality_choices = [(m.id, m.name) for m in municipalities]
|
||||
|
||||
# Status choices for filters
|
||||
|
|
|
|||
|
|
@ -573,8 +573,8 @@ def build_conditional_filter_config(accessible_thematics, user=None):
|
|||
config = {}
|
||||
|
||||
# Shared lists used by multiple thematics
|
||||
from assets.permissions import get_user_accessible_municipalities
|
||||
municipalities = get_user_accessible_municipalities(user)
|
||||
from common.models import Municipality
|
||||
municipalities = Municipality.objects.all().order_by('name_fr')
|
||||
municipality_options = [
|
||||
{'value': str(m.id), 'label': m.name}
|
||||
for m in municipalities
|
||||
|
|
|
|||
|
|
@ -62,8 +62,7 @@ def publiclighting_index(request):
|
|||
"icon": next((t.icon for t in thematics if t.code == thematic), None),
|
||||
}
|
||||
|
||||
from assets.permissions import get_user_accessible_municipalities
|
||||
municipalities = list(get_user_accessible_municipalities(request.user))
|
||||
municipalities = list(Municipality.objects.all())
|
||||
municipalities.sort(key=lambda m: m.name.lower())
|
||||
municipality_choices = [(m.id, m.name) for m in municipalities]
|
||||
|
||||
|
|
|
|||
|
|
@ -65,8 +65,7 @@ def roads_index(request):
|
|||
"icon": next((t.icon for t in thematics if t.code == thematic), None),
|
||||
}
|
||||
|
||||
from assets.permissions import get_user_accessible_municipalities
|
||||
municipalities = list(get_user_accessible_municipalities(request.user))
|
||||
municipalities = list(Municipality.objects.all())
|
||||
municipalities.sort(key=lambda m: m.name.lower())
|
||||
municipality_choices = [(m.id, m.name) for m in municipalities]
|
||||
|
||||
|
|
@ -143,26 +142,11 @@ def roads_streets_geojson(request):
|
|||
municipalities = request.GET.getlist('municipality')
|
||||
filter_geom = None
|
||||
|
||||
from assets.permissions import get_user_accessible_municipalities
|
||||
user = request.user
|
||||
user_config = getattr(user, 'config', None)
|
||||
is_admin = getattr(user, 'is_superuser', False) or (user_config and (user_config.has_role('admin') or user_config.has_role('top_manager')))
|
||||
is_external = (user_config is None) or (not user_config.is_intern)
|
||||
must_limit = is_external or (user_config and user_config.limit_assets_to_contracts)
|
||||
|
||||
# --------- Point d'entrée du queryset
|
||||
qs = RoadStreet.objects.all()
|
||||
|
||||
# --------- Filtre par communes et restriction contractuelle
|
||||
if not is_admin and must_limit:
|
||||
accessible_munis = get_user_accessible_municipalities(user)
|
||||
accessible_muni_ids = list(accessible_munis.values_list('id', flat=True))
|
||||
if municipalities:
|
||||
valid_munis = [mid for mid in municipalities if (int(mid) if str(mid).isdigit() else mid) in accessible_muni_ids]
|
||||
qs = qs.filter(municipality_id__in=valid_munis)
|
||||
else:
|
||||
qs = qs.filter(municipality_id__in=accessible_muni_ids)
|
||||
elif municipalities:
|
||||
# --------- Filtre par communes
|
||||
if municipalities:
|
||||
qs = qs.filter(municipality_id__in=municipalities)
|
||||
|
||||
# --------- Filtre géométrique
|
||||
|
|
@ -366,29 +350,7 @@ def roads_assets_geojson(request):
|
|||
drains_qs = drains_qs.filter(street__id__in=roads_location_ids)
|
||||
concrete_block_sets_qs = concrete_block_sets_qs.none()
|
||||
|
||||
from assets.permissions import get_user_accessible_municipalities
|
||||
user = request.user
|
||||
user_config = getattr(user, 'config', None)
|
||||
is_admin = getattr(user, 'is_superuser', False) or (user_config and (user_config.has_role('admin') or user_config.has_role('top_manager')))
|
||||
is_external = (user_config is None) or (not user_config.is_intern)
|
||||
must_limit = is_external or (user_config and user_config.limit_assets_to_contracts)
|
||||
|
||||
if not is_admin and must_limit:
|
||||
accessible_munis = get_user_accessible_municipalities(user)
|
||||
accessible_muni_ids = list(accessible_munis.values_list('id', flat=True))
|
||||
if municipalities:
|
||||
valid_munis = [mid for mid in municipalities if (int(mid) if str(mid).isdigit() else mid) in accessible_muni_ids]
|
||||
roadways_qs = roadways_qs.filter(street__municipality_id__in=valid_munis)
|
||||
sidewalks_qs = sidewalks_qs.filter(street__municipality_id__in=valid_munis)
|
||||
furnitures_qs = furnitures_qs.filter(street__municipality_id__in=valid_munis)
|
||||
drains_qs = drains_qs.filter(street__municipality_id__in=valid_munis)
|
||||
else:
|
||||
roadways_qs = roadways_qs.filter(street__municipality_id__in=accessible_muni_ids)
|
||||
sidewalks_qs = sidewalks_qs.filter(street__municipality_id__in=accessible_muni_ids)
|
||||
furnitures_qs = furnitures_qs.filter(street__municipality_id__in=accessible_muni_ids)
|
||||
drains_qs = drains_qs.filter(street__municipality_id__in=accessible_muni_ids)
|
||||
concrete_block_sets_qs = concrete_block_sets_qs.none()
|
||||
elif municipalities:
|
||||
if municipalities:
|
||||
roadways_qs = roadways_qs.filter(street__municipality_id__in=municipalities)
|
||||
sidewalks_qs = sidewalks_qs.filter(street__municipality_id__in=municipalities)
|
||||
furnitures_qs = furnitures_qs.filter(street__municipality_id__in=municipalities)
|
||||
|
|
|
|||
|
|
@ -70,8 +70,7 @@ def sign_index(request):
|
|||
"icon": next((t.icon for t in thematics if t.code == thematic), None),
|
||||
}
|
||||
|
||||
from assets.permissions import get_user_accessible_municipalities
|
||||
municipalities = list(get_user_accessible_municipalities(request.user))
|
||||
municipalities = list(Municipality.objects.all())
|
||||
municipalities.sort(key=lambda m: m.name.lower())
|
||||
municipality_choices = [(m.id, m.name) for m in municipalities]
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import os
|
|||
from typing import Iterable, List
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.core.cache import cache
|
||||
from django.urls import resolve
|
||||
|
||||
from .models import UserConfig, AppView
|
||||
|
|
@ -12,7 +11,6 @@ from .models import UserConfig, AppView
|
|||
|
||||
SESSION_ALLOWED_IDS_KEY = "account_switch_allowed_user_ids"
|
||||
SESSION_ORIGIN_ID_KEY = "account_switch_origin_user_id"
|
||||
SESSION_MAP_BOUNDS_KEY = "user_map_bounds"
|
||||
|
||||
|
||||
from django.conf import settings
|
||||
|
|
@ -57,7 +55,6 @@ def current_app_view(request):
|
|||
|
||||
Only 'sign' and 'documents' have their own dedicated views.
|
||||
All other apps (assets, interventions, controls, contracts, etc.) use 'asset_management' view.
|
||||
Results are cached to avoid hitting the database on every page render.
|
||||
"""
|
||||
current_view = None
|
||||
|
||||
|
|
@ -70,20 +67,13 @@ def current_app_view(request):
|
|||
namespace = resolved.namespace # e.g., 'sign', 'home', 'assets'
|
||||
|
||||
if namespace:
|
||||
cache_key = f"appview_for_{namespace}"
|
||||
cached_view = cache.get(cache_key)
|
||||
if cached_view is not None:
|
||||
current_view = None if cached_view is False else cached_view
|
||||
if namespace in DEDICATED_VIEW_APPS:
|
||||
# These apps have their own dedicated view
|
||||
current_view = AppView.objects.filter(url_name__startswith=f"{namespace}:").first()
|
||||
else:
|
||||
if namespace in DEDICATED_VIEW_APPS:
|
||||
# These apps have their own dedicated view
|
||||
current_view = AppView.objects.filter(url_name__startswith=f"{namespace}:").first()
|
||||
else:
|
||||
# All other apps (home, assets, interventions, controls, contracts, etc.)
|
||||
# should show 'asset_management' view
|
||||
current_view = AppView.objects.filter(code='asset_management').first()
|
||||
|
||||
cache.set(cache_key, current_view if current_view is not None else False, 3600)
|
||||
# All other apps (home, assets, interventions, controls, contracts, etc.)
|
||||
# should show 'asset_management' view
|
||||
current_view = AppView.objects.filter(code='asset_management').first()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
|
@ -98,7 +88,6 @@ def linked_accounts(request):
|
|||
if not user or isinstance(user, AnonymousUser) or not user.is_authenticated:
|
||||
request.session.pop(SESSION_ALLOWED_IDS_KEY, None)
|
||||
request.session.pop(SESSION_ORIGIN_ID_KEY, None)
|
||||
request.session.pop(SESSION_MAP_BOUNDS_KEY, None)
|
||||
return {
|
||||
"switchable_accounts": [],
|
||||
"account_switch_origin_user_id": None,
|
||||
|
|
@ -109,7 +98,6 @@ def linked_accounts(request):
|
|||
except UserConfig.DoesNotExist:
|
||||
request.session.pop(SESSION_ALLOWED_IDS_KEY, None)
|
||||
request.session.pop(SESSION_ORIGIN_ID_KEY, None)
|
||||
request.session.pop(SESSION_MAP_BOUNDS_KEY, None)
|
||||
return {
|
||||
"switchable_accounts": [],
|
||||
"account_switch_origin_user_id": None,
|
||||
|
|
@ -152,83 +140,15 @@ def linked_accounts(request):
|
|||
def organization_info(request):
|
||||
"""
|
||||
Context processor that exposes organization details from settings.
|
||||
Dynamically adapts map_default_center, map_default_zoom and map_default_extent
|
||||
when the authenticated user has an assigned contract associated with a municipality.
|
||||
Caches calculated bounds in session to avoid heavy GIS and SQL processing on every page.
|
||||
"""
|
||||
center = [getattr(settings, 'MAP_DEFAULT_CENTER_LON', 4.3572), getattr(settings, 'MAP_DEFAULT_CENTER_LAT', 50.8477)]
|
||||
zoom = getattr(settings, 'MAP_DEFAULT_ZOOM', 11)
|
||||
extent = getattr(settings, 'MAP_DEFAULT_EXTENT', None)
|
||||
|
||||
if hasattr(request, 'user') and request.user.is_authenticated:
|
||||
session = getattr(request, 'session', None)
|
||||
cached_bounds = session.get(SESSION_MAP_BOUNDS_KEY) if session is not None else None
|
||||
|
||||
if cached_bounds and cached_bounds.get('user_id') == request.user.pk:
|
||||
center = cached_bounds.get('center', center)
|
||||
zoom = cached_bounds.get('zoom', zoom)
|
||||
extent = cached_bounds.get('extent', extent)
|
||||
else:
|
||||
try:
|
||||
user_config = getattr(request.user, 'config', None)
|
||||
if user_config:
|
||||
# Récupérer les contrats accessibles ayant des communes associées
|
||||
contracts_to_check = []
|
||||
if user_config.default_contract and user_config.default_contract.municipalities.exists():
|
||||
contracts_to_check.append(user_config.default_contract)
|
||||
else:
|
||||
contracts_to_check = list(user_config.contracts.prefetch_related('municipalities').filter(municipalities__isnull=False).distinct())
|
||||
|
||||
geoms_4326 = []
|
||||
for c in contracts_to_check:
|
||||
for m in c.municipalities.exclude(geom=None):
|
||||
g = m.geom.transform(4326, clone=True) if m.geom.srid != 4326 else m.geom
|
||||
geoms_4326.append(g)
|
||||
|
||||
if geoms_4326:
|
||||
min_lon = min(g.extent[0] for g in geoms_4326)
|
||||
min_lat = min(g.extent[1] for g in geoms_4326)
|
||||
max_lon = max(g.extent[2] for g in geoms_4326)
|
||||
max_lat = max(g.extent[3] for g in geoms_4326)
|
||||
|
||||
# Léger buffer (5% d'expansion de chaque côté pour aérer le cadre)
|
||||
delta_lon = (max_lon - min_lon) * 0.05
|
||||
delta_lat = (max_lat - min_lat) * 0.05
|
||||
|
||||
center = [round((min_lon + max_lon) / 2.0, 5), round((min_lat + max_lat) / 2.0, 5)]
|
||||
extent = [
|
||||
[round(min_lon - delta_lon, 5), round(min_lat - delta_lat, 5)],
|
||||
[round(max_lon + delta_lon, 5), round(max_lat + delta_lat, 5)]
|
||||
]
|
||||
|
||||
span = max(max_lon - min_lon, max_lat - min_lat)
|
||||
if span < 0.08:
|
||||
zoom = 14
|
||||
elif span < 0.2:
|
||||
zoom = 13
|
||||
elif span < 0.5:
|
||||
zoom = 12
|
||||
else:
|
||||
zoom = 11
|
||||
|
||||
if session is not None:
|
||||
session[SESSION_MAP_BOUNDS_KEY] = {
|
||||
'user_id': request.user.pk,
|
||||
'center': center,
|
||||
'zoom': zoom,
|
||||
'extent': extent,
|
||||
}
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {
|
||||
'product_name': getattr(settings, 'PRODUCT_NAME', 'Loko'),
|
||||
'organization_name': getattr(settings, 'ORGANIZATION_NAME', 'Loko'),
|
||||
'organization_logo_header': getattr(settings, 'ORGANIZATION_LOGO_HEADER', 'common/android-192.png'),
|
||||
'organization_primary_color': getattr(settings, 'ORGANIZATION_PRIMARY_COLOR_HEX', '#2f3e8b'),
|
||||
'map_default_center': center,
|
||||
'map_default_zoom': zoom,
|
||||
'map_default_extent': extent,
|
||||
'map_default_center': [getattr(settings, 'MAP_DEFAULT_CENTER_LON', 4.3572), getattr(settings, 'MAP_DEFAULT_CENTER_LAT', 50.8477)],
|
||||
'map_default_zoom': getattr(settings, 'MAP_DEFAULT_ZOOM', 11),
|
||||
'map_default_extent': getattr(settings, 'MAP_DEFAULT_EXTENT', None),
|
||||
'map_overlay_wms_url': getattr(settings, 'MAP_OVERLAY_WMS_URL', ''),
|
||||
'map_ortho_wms_url': getattr(settings, 'MAP_ORTHO_WMS_URL', ''),
|
||||
'map_ortho_wms_name': getattr(settings, 'MAP_ORTHO_WMS_NAME', ''),
|
||||
|
|
|
|||
|
|
@ -1,828 +0,0 @@
|
|||
[
|
||||
{
|
||||
"code": "TL_ARMOIRE",
|
||||
"name_fr": "Armoire Tricolore",
|
||||
"name_nl": "Armoire Tricolore",
|
||||
"thematic_code": "trafficlights",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"trafficlightelectricalcabinet"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "TL_BORNIER",
|
||||
"name_fr": "Bornier Tricolore",
|
||||
"name_nl": "Bornier Tricolore",
|
||||
"thematic_code": "trafficlights",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"trafficlightaccessory"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "TL_CABLE",
|
||||
"name_fr": "Cable de Carrefour",
|
||||
"name_nl": "Cable de Carrefour",
|
||||
"thematic_code": "trafficlights",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"trafficlightcable"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "TL_CARREFOUR",
|
||||
"name_fr": "Carrefour Tricolore",
|
||||
"name_nl": "Carrefour Tricolore",
|
||||
"thematic_code": "trafficlights",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "TL_CONE",
|
||||
"name_fr": "Cône Balisage",
|
||||
"name_nl": "Cône Balisage",
|
||||
"thematic_code": "trafficlights",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "TL_DETEC_OTHER",
|
||||
"name_fr": "Détecteurs autres",
|
||||
"name_nl": "Andere detectoren",
|
||||
"thematic_code": "trafficlights",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"trafficlightdetector"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "TL_DETEC_BP",
|
||||
"name_fr": "Détection Bouton Poussoir",
|
||||
"name_nl": "Détection Bouton Poussoir",
|
||||
"thematic_code": "trafficlights",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"trafficlightdetector"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "TL_DETEC_RADAR",
|
||||
"name_fr": "Détection Radar",
|
||||
"name_nl": "Détection Radar",
|
||||
"thematic_code": "trafficlights",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"trafficlightdetector"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "TL_DIVERS",
|
||||
"name_fr": "Equipement divers",
|
||||
"name_nl": "Equipement divers",
|
||||
"thematic_code": "trafficlights",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"trafficlightaccessory"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "TL_LANELIGHT",
|
||||
"name_fr": "Lanelight Balisage",
|
||||
"name_nl": "Lanelight Balisage",
|
||||
"thematic_code": "trafficlights",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "TL_LANTERNE",
|
||||
"name_fr": "Lanterne Tricolore",
|
||||
"name_nl": "Lanterne Tricolore",
|
||||
"thematic_code": "trafficlights",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"trafficlightlantern"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "TL_NETWERK",
|
||||
"name_fr": "Réseau",
|
||||
"name_nl": "Réseau",
|
||||
"thematic_code": "trafficlights",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "TL_PANNEAU",
|
||||
"name_fr": "Panneau signalisation",
|
||||
"name_nl": "Panneau signalisation",
|
||||
"thematic_code": "trafficlights",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "TL_POTEAU",
|
||||
"name_fr": "Poteau Tricolore",
|
||||
"name_nl": "Poteau Tricolore",
|
||||
"thematic_code": "trafficlights",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"trafficlightpole"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "TL_RADAR_PREVENTIF",
|
||||
"name_fr": "Radar Préventif",
|
||||
"name_nl": "Radar Préventif",
|
||||
"thematic_code": "trafficlights",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"trafficlightradar"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "TL_REGELAAR",
|
||||
"name_fr": "Contrôleur de trafic",
|
||||
"name_nl": "Traffic regelaar",
|
||||
"thematic_code": "trafficlights",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"trafficlightcontroller"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "TL_SPANNING",
|
||||
"name_fr": "Transformateur électrique",
|
||||
"name_nl": "Transformateur électrique",
|
||||
"thematic_code": "trafficlights",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "LI_poles",
|
||||
"name_fr": "Poteaux d'éclairage",
|
||||
"name_nl": "Verlichtingsmasten",
|
||||
"thematic_code": "publiclighting",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"publiclightingpole"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "TU_BAL",
|
||||
"name_fr": "Balisage",
|
||||
"name_nl": "Balisage",
|
||||
"thematic_code": "structures",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "TU_BAR",
|
||||
"name_fr": "Barrière",
|
||||
"name_nl": "Barrière",
|
||||
"thematic_code": "structures",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"structuregeoasset"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "TU_BDI",
|
||||
"name_fr": "Detection et protection incendie",
|
||||
"name_nl": "Detection et protection incendie",
|
||||
"thematic_code": "structures",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"structuregeoasset"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "TU_BTS",
|
||||
"name_fr": "Basse tension",
|
||||
"name_nl": "Basse tension",
|
||||
"thematic_code": "structures",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"structuregeoasset"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "TU_CAB",
|
||||
"name_fr": "Cables",
|
||||
"name_nl": "Cables",
|
||||
"thematic_code": "structures",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "TU_CAC",
|
||||
"name_fr": "Contrôle d'accès",
|
||||
"name_nl": "Contrôle d'accès",
|
||||
"thematic_code": "structures",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"structuregeoasset"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "TU_CPT",
|
||||
"name_fr": "Comptage vehicules",
|
||||
"name_nl": "Comptage vehicules",
|
||||
"thematic_code": "structures",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"structuregeoasset"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "TU_CTV",
|
||||
"name_fr": "Videosurveillance",
|
||||
"name_nl": "Videosurveillance",
|
||||
"thematic_code": "structures",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"structuregeoasset"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "TU_ECL",
|
||||
"name_fr": "Eclairage",
|
||||
"name_nl": "Eclairage",
|
||||
"thematic_code": "structures",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"structuregeoasset"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "TU_GAB",
|
||||
"name_fr": "Detection hors gabarit",
|
||||
"name_nl": "Detection hors gabarit",
|
||||
"thematic_code": "structures",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "TU_GTC",
|
||||
"name_fr": "Telegestion",
|
||||
"name_nl": "Telegestion",
|
||||
"thematic_code": "structures",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"structuregeoasset"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "TU_HTS",
|
||||
"name_fr": "Haute tension",
|
||||
"name_nl": "Haute tension",
|
||||
"thematic_code": "structures",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"structuregeoasset"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "TU_LOC",
|
||||
"name_fr": "Locaux techniques",
|
||||
"name_nl": "Locaux techniques",
|
||||
"thematic_code": "structures",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"structuregeoasset"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "TU_MET",
|
||||
"name_fr": "Métalleries",
|
||||
"name_nl": "Métalleries",
|
||||
"thematic_code": "structures",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "TU_NBK",
|
||||
"name_fr": "Groupes no break",
|
||||
"name_nl": "Groupes no break",
|
||||
"thematic_code": "structures",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "TU_PMP",
|
||||
"name_fr": "Pompage",
|
||||
"name_nl": "Pompage",
|
||||
"thematic_code": "structures",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"structuregeoasset"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "TU_PMV",
|
||||
"name_fr": "Panneaux à Messages Variables",
|
||||
"name_nl": "Panneaux à Messages Variables",
|
||||
"thematic_code": "structures",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "TU_RAD",
|
||||
"name_fr": "Retransmission radio",
|
||||
"name_nl": "Retransmission radio",
|
||||
"thematic_code": "structures",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"structuregeoasset"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "TU_SIG",
|
||||
"name_fr": "Signalisation routiere",
|
||||
"name_nl": "Signalisation routiere",
|
||||
"thematic_code": "structures",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"structuregeoasset"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "TU_SON",
|
||||
"name_fr": "Sonorisation",
|
||||
"name_nl": "Sonorisation",
|
||||
"thematic_code": "structures",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"structuregeoasset"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "TU_TEL",
|
||||
"name_fr": "Telephonie",
|
||||
"name_nl": "Telephonie",
|
||||
"thematic_code": "structures",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"structuregeoasset"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "TU_VEN",
|
||||
"name_fr": "Ventilation",
|
||||
"name_nl": "Ventilation",
|
||||
"thematic_code": "structures",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"structuregeoasset"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "CC_BUILDING",
|
||||
"name_fr": "Bâtiment",
|
||||
"name_nl": "Gebouw",
|
||||
"thematic_code": "controlcenters",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "CC_MAINTENANCE",
|
||||
"name_fr": "Maintenance",
|
||||
"name_nl": "Onderhoud",
|
||||
"thematic_code": "controlcenters",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "CC_OFFICE_EQMNT",
|
||||
"name_fr": "Bureautique",
|
||||
"name_nl": "Bureautique",
|
||||
"thematic_code": "controlcenters",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "CC_SYSTEM",
|
||||
"name_fr": "Système",
|
||||
"name_nl": "Systeem",
|
||||
"thematic_code": "controlcenters",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "CC_TELEPHONY",
|
||||
"name_fr": "Téléphonie",
|
||||
"name_nl": "Telefonie",
|
||||
"thematic_code": "controlcenters",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "CC_VIDEO",
|
||||
"name_fr": "Vidéo",
|
||||
"name_nl": "Video",
|
||||
"thematic_code": "controlcenters",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "LI_lightfixtures",
|
||||
"name_fr": "Luminaires",
|
||||
"name_nl": "Verlichtingsarmaturen",
|
||||
"thematic_code": "publiclighting",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"publiclightinglightfixture"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "LI_electrical_cabinets",
|
||||
"name_fr": "Armoires électriques",
|
||||
"name_nl": "Elektrische kasten",
|
||||
"thematic_code": "publiclighting",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"publiclightingelectricalcabinet"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "LI_cables",
|
||||
"name_fr": "Câbles",
|
||||
"name_nl": "Kabels",
|
||||
"thematic_code": "publiclighting",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "ITS_CAMERA",
|
||||
"name_fr": "Caméras",
|
||||
"name_nl": "Cameras",
|
||||
"thematic_code": "its",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "ITS_COUNTING",
|
||||
"name_fr": "Comptages",
|
||||
"name_nl": "Tellingen",
|
||||
"thematic_code": "its",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "ITS_LIDAR",
|
||||
"name_fr": "Lidar",
|
||||
"name_nl": "Lidar",
|
||||
"thematic_code": "its",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "ITS_NETWORK",
|
||||
"name_fr": "Réseau",
|
||||
"name_nl": "Netwerk",
|
||||
"thematic_code": "its",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "ITS_PMV",
|
||||
"name_fr": "PMV",
|
||||
"name_nl": "PMV",
|
||||
"thematic_code": "its",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "ITS_SYSTEM",
|
||||
"name_fr": "Systèmes",
|
||||
"name_nl": "Systemen",
|
||||
"thematic_code": "its",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "NAT_GREEN_AREAS",
|
||||
"name_fr": "Espaces verts",
|
||||
"name_nl": "Groene ruimtes",
|
||||
"thematic_code": "nature",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"naturegreensurface"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "NAT_TREES",
|
||||
"name_fr": "Arbres",
|
||||
"name_nl": "Bomen",
|
||||
"thematic_code": "nature",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"naturetree"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "RD_ROADWAY",
|
||||
"name_fr": "Chaussée",
|
||||
"name_nl": "Rijbaan",
|
||||
"thematic_code": "roads",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "RD_SIDEWALK",
|
||||
"name_fr": "Trottoir",
|
||||
"name_nl": "Voetpad",
|
||||
"thematic_code": "roads",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "RD_CYCLEROAD",
|
||||
"name_fr": "Piste cyclable",
|
||||
"name_nl": "Fietspad",
|
||||
"thematic_code": "roads",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "TU_ROADWAY",
|
||||
"name_fr": "Chaussée en tunnel",
|
||||
"name_nl": "Tunnelweg",
|
||||
"thematic_code": "structures",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "TU_CIVIL_ENGINEERING",
|
||||
"name_fr": "Génie civil",
|
||||
"name_nl": "Burgerlijke bouwkunde",
|
||||
"thematic_code": "structures",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"structuregeoasset"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "TU_HYDRANT",
|
||||
"name_fr": "Hydrant",
|
||||
"name_nl": "Hydrant",
|
||||
"thematic_code": "structures",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"structuregeoasset"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "LI_CONE",
|
||||
"name_fr": "Cône",
|
||||
"name_nl": "Kegel",
|
||||
"thematic_code": "publiclighting",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "ART_FONTAIN",
|
||||
"name_fr": "Fontaines",
|
||||
"name_nl": "Fonteinen",
|
||||
"thematic_code": "artworks",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"artworkfountain"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "ART_MONUMENT",
|
||||
"name_fr": "Monument",
|
||||
"name_nl": "Monument",
|
||||
"thematic_code": "artworks",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"artworkmonument"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "RD_DRAIN",
|
||||
"name_fr": "Avaloir",
|
||||
"name_nl": "Rioolput",
|
||||
"thematic_code": "roads",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "RD_MANHOLE",
|
||||
"name_fr": "Taque",
|
||||
"name_nl": "Riooldeksel",
|
||||
"thematic_code": "roads",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "SI_POLE",
|
||||
"name_fr": "Poteau",
|
||||
"name_nl": "Paal",
|
||||
"thematic_code": "sign",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "SI_PANEL",
|
||||
"name_fr": "Panneau",
|
||||
"name_nl": "Paneel",
|
||||
"thematic_code": "sign",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "SI_BARRIER",
|
||||
"name_fr": "Barrière",
|
||||
"name_nl": "Barrière",
|
||||
"thematic_code": "sign",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "SI_MARKING",
|
||||
"name_fr": "Marquage",
|
||||
"name_nl": "Markering",
|
||||
"thematic_code": "sign",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "RD_STREET_FURNITURE",
|
||||
"name_fr": "Mobilier urbain",
|
||||
"name_nl": "Straatmeubilair",
|
||||
"thematic_code": "roads",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "TL_SIBELGA_METER",
|
||||
"name_fr": "Compteur Sibelga",
|
||||
"name_nl": "Sibelga-meter",
|
||||
"thematic_code": "trafficlights",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "SI_TAXI",
|
||||
"name_fr": "Emplacement Taxi",
|
||||
"name_nl": "Taxistandplaats",
|
||||
"thematic_code": "sign",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "TL_DETEC_VIDEO",
|
||||
"name_fr": "Détection Vidéo",
|
||||
"name_nl": "Videodetectie",
|
||||
"thematic_code": "trafficlights",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"trafficlightdetector"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "ITS_RADARS",
|
||||
"name_fr": "Radars",
|
||||
"name_nl": "Radars",
|
||||
"thematic_code": "its",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "NAT_FURNITURES",
|
||||
"name_fr": "Mobilier (Bacs à fleurs, ...)",
|
||||
"name_nl": "Meubilair (bloembakken, ...)",
|
||||
"thematic_code": "nature",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"naturebench",
|
||||
"natureshrub"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "CL_MATERIALS",
|
||||
"name_fr": "Matériaux",
|
||||
"name_nl": "Materialen",
|
||||
"thematic_code": "clean",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "CL_DRAIN",
|
||||
"name_fr": "Avaloir",
|
||||
"name_nl": "Rioolput",
|
||||
"thematic_code": "clean",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "CL_OTHER",
|
||||
"name_fr": "Autre",
|
||||
"name_nl": "Andere",
|
||||
"thematic_code": "clean",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "CL_ROADWAY",
|
||||
"name_fr": "Chaussée",
|
||||
"name_nl": "Rijbaan",
|
||||
"thematic_code": "clean",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "CL_BUBBLE",
|
||||
"name_fr": "Bulle",
|
||||
"name_nl": "Bubbel",
|
||||
"thematic_code": "clean",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"cleanglasscontainer"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "CL_BASKET",
|
||||
"name_fr": "Corbeille",
|
||||
"name_nl": "Prullenbak",
|
||||
"thematic_code": "clean",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"cleanlitterbin"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "ITS_POWER_SUPPLY",
|
||||
"name_fr": "Alimentation électrique",
|
||||
"name_nl": "Elektrische voeding",
|
||||
"thematic_code": "its",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "CL_SIDEWALK",
|
||||
"name_fr": "Trottoir",
|
||||
"name_nl": "Voetpad",
|
||||
"thematic_code": "clean",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "CL_BIKELANE",
|
||||
"name_fr": "Piste cyclable",
|
||||
"name_nl": "Fietspad",
|
||||
"thematic_code": "clean",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "NAT_RWI",
|
||||
"name_fr": "Gestion intégrée des eaux pluviales (GIEP)",
|
||||
"name_nl": "Geïntegreerd regenwaterbeheer (GRWB)",
|
||||
"thematic_code": "water",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"naturerwiasset"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "TL_RADAR_POLE",
|
||||
"name_fr": "Poteau radar",
|
||||
"name_nl": "Radarpaal",
|
||||
"thematic_code": "trafficlights",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"trafficlightradarpole"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "parking_spot",
|
||||
"name_fr": "Emplacements de stationnement",
|
||||
"name_nl": "Parkeerplaatsen",
|
||||
"thematic_code": "parking",
|
||||
"parent_code": null,
|
||||
"allowed_models": []
|
||||
},
|
||||
{
|
||||
"code": "bike_parking_spot",
|
||||
"name_fr": "Emplacement vélos",
|
||||
"name_nl": "Fietsenstalling",
|
||||
"thematic_code": "parking",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"parkinglocation",
|
||||
"parkingspot"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "TL_PROGRAMMING",
|
||||
"name_fr": "Programmation",
|
||||
"name_nl": "Programmatie",
|
||||
"thematic_code": "trafficlights",
|
||||
"parent_code": null,
|
||||
"allowed_models": [
|
||||
"trafficlightprogramming"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -390,55 +390,8 @@ class ScheduledCommandResolveOutputFileTests(TestCase):
|
|||
|
||||
mock_attach.assert_called_once_with(fake_path)
|
||||
mock_send.assert_called_once()
|
||||
from django.test import RequestFactory
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.core.cache import cache
|
||||
from common.context_processors import (
|
||||
current_app_view,
|
||||
organization_info,
|
||||
SESSION_MAP_BOUNDS_KEY,
|
||||
)
|
||||
from common.models import AppView
|
||||
|
||||
|
||||
class ContextProcessorsCachingTests(TestCase):
|
||||
def setUp(self):
|
||||
cache.clear()
|
||||
self.factory = RequestFactory()
|
||||
self.user = User.objects.create_user(username="context_user", password="password123")
|
||||
self.config = UserConfig.objects.create(user=self.user)
|
||||
|
||||
def _add_session_to_request(self, request):
|
||||
middleware = SessionMiddleware(lambda req: None)
|
||||
middleware.process_request(request)
|
||||
request.session.save()
|
||||
|
||||
def test_current_app_view_caches_result(self):
|
||||
AppView.objects.create(code="asset_management", name_fr="Gestion des actifs", name_nl="Activa beheer", url_name="home:index")
|
||||
request = self.factory.get("/")
|
||||
|
||||
with self.assertNumQueries(1):
|
||||
ctx1 = current_app_view(request)
|
||||
self.assertIsNotNone(ctx1["current_view"])
|
||||
|
||||
# Second call should use cache and perform 0 database queries
|
||||
with self.assertNumQueries(0):
|
||||
ctx2 = current_app_view(request)
|
||||
self.assertEqual(ctx1["current_view"], ctx2["current_view"])
|
||||
|
||||
def test_organization_info_caches_in_session(self):
|
||||
request = self.factory.get("/")
|
||||
self._add_session_to_request(request)
|
||||
request.user = self.user
|
||||
|
||||
ctx1 = organization_info(request)
|
||||
self.assertIn(SESSION_MAP_BOUNDS_KEY, request.session)
|
||||
self.assertEqual(request.session[SESSION_MAP_BOUNDS_KEY]["user_id"], self.user.pk)
|
||||
|
||||
# Subsequent call with session already containing user_map_bounds does not query or recalculate
|
||||
with self.assertNumQueries(0):
|
||||
ctx2 = organization_info(request)
|
||||
self.assertEqual(ctx1["map_default_center"], ctx2["map_default_center"])
|
||||
self.assertEqual(ctx1["map_default_zoom"], ctx2["map_default_zoom"])
|
||||
self.assertEqual(ctx1["map_default_extent"], ctx2["map_default_extent"])
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ from urllib.parse import urlencode
|
|||
from common.context_processors import (
|
||||
SESSION_ALLOWED_IDS_KEY,
|
||||
SESSION_ORIGIN_ID_KEY,
|
||||
SESSION_MAP_BOUNDS_KEY,
|
||||
compute_initial_allowed_ids,
|
||||
)
|
||||
|
||||
|
|
@ -288,7 +287,6 @@ def switch_account(request):
|
|||
|
||||
session[SESSION_ALLOWED_IDS_KEY] = allowed_ids
|
||||
session[SESSION_ORIGIN_ID_KEY] = origin_user_id
|
||||
session.pop(SESSION_MAP_BOUNDS_KEY, None)
|
||||
|
||||
return redirect(origin_next or reverse("home:index"))
|
||||
|
||||
|
|
|
|||
|
|
@ -126,17 +126,17 @@ class CompanyAdmin(admin.ModelAdmin):
|
|||
|
||||
@admin.register(Contract)
|
||||
class ContractAdmin(admin.ModelAdmin):
|
||||
list_display = ('contract_number', 'description', 'company', 'municipalities_list', 'is_blocked', 'is_active', 'start_date', 'end_date', 'contact_phone', 'send_email_notifications', 'send_daily_summary', 'uses_occupations', 'occupations_required', 'uses_intervention_manager', 'allow_team_members_edit', 'auto_assign_control_team', 'thematics_list')
|
||||
list_display = ('contract_number', 'description', 'company', 'is_blocked', 'is_active', 'start_date', 'end_date', 'contact_phone', 'send_email_notifications', 'send_daily_summary', 'uses_occupations', 'occupations_required', 'uses_intervention_manager', 'allow_team_members_edit', 'auto_assign_control_team', 'thematics_list')
|
||||
search_fields = ('contract_number', 'company__name', 'description', 'contact_phone')
|
||||
list_filter = ('is_blocked', 'is_active', 'municipalities', 'thematics', 'uses_occupations', 'occupations_required', 'uses_intervention_manager', 'allow_team_members_edit', 'auto_assign_control_team', 'send_daily_summary')
|
||||
filter_horizontal = ('thematics', 'municipalities')
|
||||
list_filter = ('is_blocked', 'is_active', 'thematics', 'uses_occupations', 'occupations_required', 'uses_intervention_manager', 'allow_team_members_edit', 'auto_assign_control_team', 'send_daily_summary')
|
||||
filter_horizontal = ('thematics',)
|
||||
|
||||
def get_inlines(self, request, obj=None):
|
||||
return [_get_pause_reason_inline(), _get_symptom_autoclose_inline(), _get_operation_template_posts_inline()] + list(super().get_inlines(request, obj))
|
||||
|
||||
fieldsets = (
|
||||
(None, {
|
||||
'fields': ('company', 'municipalities', 'contract_number', 'description', 'start_date', 'end_date', 'is_active', 'committed_amount', 'internal_link', 'executive_officer', 'contact_phone', 'signatory_name', 'signatory_role', 'thematics'),
|
||||
'fields': ('company', 'contract_number', 'description', 'start_date', 'end_date', 'is_active', 'committed_amount', 'internal_link', 'executive_officer', 'contact_phone', 'signatory_name', 'signatory_role', 'thematics'),
|
||||
}),
|
||||
('Blocage budgétaire / Suspension', {
|
||||
'fields': ('is_blocked', 'blocked_reason', 'blocked_at'),
|
||||
|
|
@ -164,10 +164,6 @@ class ContractAdmin(admin.ModelAdmin):
|
|||
}
|
||||
js = ('contracts/js/contract_admin.js',)
|
||||
|
||||
def municipalities_list(self, obj):
|
||||
return ", ".join([m.name for m in obj.municipalities.all()])
|
||||
municipalities_list.short_description = 'Communes'
|
||||
|
||||
def thematics_list(self, obj):
|
||||
return ", ".join([thematic.name for thematic in obj.thematics.all()])
|
||||
thematics_list.short_description = 'Thématiques'
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
# Generated by Django 6.0.7 on 2026-09-19 13:49
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('common', '0015_remove_obsolete_schools_appview'),
|
||||
('contracts', '0009_contract_auto_assign_control_team'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='contract',
|
||||
name='municipality',
|
||||
field=models.ForeignKey(blank=True, help_text='Commune couverte par ce contrat (permet le cadrage cartographique automatique).', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='contracts', to='common.municipality', verbose_name='Commune associée'),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
# Generated manually for multi-commune contracts
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def copy_municipality_to_municipalities(apps, schema_editor):
|
||||
Contract = apps.get_model('contracts', 'Contract')
|
||||
for contract in Contract.objects.all():
|
||||
if getattr(contract, 'municipality_id', None):
|
||||
contract.municipalities.add(contract.municipality_id)
|
||||
|
||||
|
||||
def reverse_copy(apps, schema_editor):
|
||||
Contract = apps.get_model('contracts', 'Contract')
|
||||
for contract in Contract.objects.all():
|
||||
first_m = contract.municipalities.first()
|
||||
if first_m:
|
||||
contract.municipality_id = first_m.id
|
||||
contract.save(update_fields=['municipality_id'])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('common', '0015_remove_obsolete_schools_appview'),
|
||||
('contracts', '0010_contract_municipality'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='contract',
|
||||
name='municipalities',
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text='Communes couvertes par ce contrat (permet le filtrage et le cadrage cartographique).',
|
||||
related_name='contracts',
|
||||
to='common.municipality',
|
||||
verbose_name='Communes associées',
|
||||
),
|
||||
),
|
||||
migrations.RunPython(copy_municipality_to_municipalities, reverse_code=reverse_copy),
|
||||
migrations.RemoveField(
|
||||
model_name='contract',
|
||||
name='municipality',
|
||||
),
|
||||
]
|
||||
|
|
@ -372,18 +372,6 @@ class Contract(models.Model):
|
|||
verbose_name=_("Thématiques"),
|
||||
help_text=_("Thématiques associées à ce contrat."),
|
||||
)
|
||||
municipalities = models.ManyToManyField(
|
||||
'common.Municipality',
|
||||
blank=True,
|
||||
related_name="contracts",
|
||||
verbose_name=_("Communes associées"),
|
||||
help_text=_("Communes couvertes par ce contrat (permet le filtrage et le cadrage cartographique).")
|
||||
)
|
||||
|
||||
@property
|
||||
def municipality(self):
|
||||
"""Retourne la première commune associée (compatibilité ascendante)."""
|
||||
return self.municipalities.first()
|
||||
is_active = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_("Actif"),
|
||||
|
|
|
|||
Loading…
Reference in a new issue