From a410cc855fd0efeb867a2d34be05e9caa9699668 Mon Sep 17 00:00:00 2001 From: kdeterme Date: Fri, 21 Aug 2026 09:00:14 +0200 Subject: [PATCH] refactor: harden API error handling in frontend modals and update asset code generation prefix in tests --- loko/assets/static/assets/asset_add_modal.js | 54 ++++-- loko/assets/static/assets/unified_index.js | 16 +- loko/assets/tests.py | 128 ++++++++++-- loko/assets/utils/asset_code_generator.py | 78 ++++++-- loko/assets/views/asset_editing.py | 194 +++++++++++++++---- 5 files changed, 388 insertions(+), 82 deletions(-) diff --git a/loko/assets/static/assets/asset_add_modal.js b/loko/assets/static/assets/asset_add_modal.js index 8f80fbb..b6f88c8 100644 --- a/loko/assets/static/assets/asset_add_modal.js +++ b/loko/assets/static/assets/asset_add_modal.js @@ -376,12 +376,18 @@ }, body: JSON.stringify(data) }) - .then(response => response.json()) - .then(result => { + .then(async response => { + let result = null; + try { + result = await response.json(); + } catch (e) { + result = null; + } + submitBtn.disabled = false; submitBtn.innerHTML = originalBtnText; - if (result.success) { + if (response.ok && result && result.success) { alertContainer.className = 'alert alert-success'; alertContainer.innerHTML = '' + result.message; alertContainer.classList.remove('d-none'); @@ -396,18 +402,20 @@ }, 1000); } } else { + const errorMsg = (result && (result.error || result.message)) + || (response.status ? `${gettext('Erreur serveur')} (${response.status})` : gettext('Erreur lors de la création')); alertContainer.className = 'alert alert-danger'; - alertContainer.innerHTML = '' + (result.error || gettext('Erreur lors de la création')); + alertContainer.innerHTML = '' + errorMsg; alertContainer.classList.remove('d-none'); } }) .catch(error => { - console.error('Error:', error); + console.error('Error creating asset:', error); submitBtn.disabled = false; submitBtn.innerHTML = originalBtnText; alertContainer.className = 'alert alert-danger'; - alertContainer.innerHTML = '' + gettext('Erreur de connexion'); + alertContainer.innerHTML = '' + (error.message || gettext('Erreur de connexion')); alertContainer.classList.remove('d-none'); }); } @@ -426,9 +434,15 @@ asset_id: assetId }) }) - .then(response => response.json()) - .then(data => { - if (data.success && data.has_templates && data.available_templates && data.available_templates.length > 0) { + .then(async response => { + let data = null; + try { + data = await response.json(); + } catch (e) { + data = null; + } + + if (response.ok && data && data.success && data.has_templates && data.available_templates && data.available_templates.length > 0) { // Afficher le modal de sélection de template showTemplateSelectionModal( interventionId, @@ -440,7 +454,7 @@ data.message ); } else { - // Pas de templates disponibles, rediriger normalement + // Pas de templates disponibles ou non applicable, rediriger normalement setTimeout(() => { performRedirect(modalId, interventionId, {intervention_linked: true}); }, 1000); @@ -584,9 +598,15 @@ template_ids: selectedTemplateIds }) }) - .then(response => response.json()) - .then(data => { - if (data.success) { + .then(async response => { + let data = null; + try { + data = await response.json(); + } catch (e) { + data = null; + } + + if (response.ok && data && data.success) { templateAlert.className = 'alert alert-success'; templateAlert.innerHTML = '' + data.message; templateAlert.classList.remove('d-none'); @@ -600,17 +620,19 @@ } else { createBtn.disabled = false; createBtn.innerHTML = originalBtnText; + const errorMsg = (data && (data.error || data.message)) + || (response.status ? `${gettext('Erreur serveur')} (${response.status})` : gettext('Erreur lors de la création')); templateAlert.className = 'alert alert-danger'; - templateAlert.innerHTML = '' + (data.error || gettext('Erreur lors de la création')); + templateAlert.innerHTML = '' + errorMsg; templateAlert.classList.remove('d-none'); } }) .catch(error => { - console.error('Error:', error); + console.error('Error creating operations from template:', error); createBtn.disabled = false; createBtn.innerHTML = originalBtnText; templateAlert.className = 'alert alert-danger'; - templateAlert.innerHTML = '' + gettext('Erreur de connexion'); + templateAlert.innerHTML = '' + (error.message || gettext('Erreur de connexion')); templateAlert.classList.remove('d-none'); }); }); diff --git a/loko/assets/static/assets/unified_index.js b/loko/assets/static/assets/unified_index.js index 1686c66..097ff21 100644 --- a/loko/assets/static/assets/unified_index.js +++ b/loko/assets/static/assets/unified_index.js @@ -972,8 +972,14 @@ $(document).ready(function () { }, body: JSON.stringify(payload), }); - const result = await response.json(); - if (result.success) { + let result = null; + try { + result = await response.json(); + } catch (e) { + result = null; + } + + if (response.ok && result && result.success) { setModalAlert('unifiedAddAssetAlert', `${escapeHtml(result.message || gettext('Asset créé.'))}`, 'success'); setTimeout(() => { bootstrap.Modal.getInstance(document.getElementById('unifiedAddAssetModal'))?.hide(); @@ -985,11 +991,13 @@ $(document).ready(function () { else window.location.reload(); }, 800); } else { - setModalAlert('unifiedAddAssetAlert', `${escapeHtml(result.error || gettext('Erreur lors de la création.'))}`, 'danger'); + const errorMsg = (result && (result.error || result.message)) + || (response.status ? `${gettext('Erreur serveur')} (${response.status})` : gettext('Erreur lors de la création.')); + setModalAlert('unifiedAddAssetAlert', `${escapeHtml(errorMsg)}`, 'danger'); } } catch (error) { console.error('Error creating asset:', error); - setModalAlert('unifiedAddAssetAlert', `${escapeHtml(gettext('Erreur de connexion.'))}`, 'danger'); + setModalAlert('unifiedAddAssetAlert', `${escapeHtml(error.message || gettext('Erreur de connexion.'))}`, 'danger'); } finally { if (submitBtn) { submitBtn.disabled = false; diff --git a/loko/assets/tests.py b/loko/assets/tests.py index a152f54..66507eb 100644 --- a/loko/assets/tests.py +++ b/loko/assets/tests.py @@ -432,8 +432,8 @@ class StructuresLocationDetailAddAssetModalTest(TestCase): self.assertContains(response, "Balisage Test") self.assertContains(response, f'name="location_id" value="{self.location.id}"') self.assertIn("suggested_code", response.context) - self.assertEqual(response.context["suggested_code"], "OA_TEST_01-000001") - self.assertContains(response, 'value="OA_TEST_01-000001"') + self.assertEqual(response.context["suggested_code"], "LOC_TEST_01-000001") + self.assertContains(response, 'value="LOC_TEST_01-000001"') def test_incremental_code_generation_with_existing_assets(self): from assets.models.structures import StructureGeoAsset, StructureAssetModel @@ -441,14 +441,14 @@ class StructuresLocationDetailAddAssetModalTest(TestCase): model = StructureAssetModel.objects.create(code="MOD_TEST", name_fr="Modèle Test", category=self.category) StructureGeoAsset.objects.create( - code="WTB30-000023", + code="LOC_TEST_01-000023", name_fr="Capteur 23", structure=self.structure, location=self.location, model=model ) StructureGeoAsset.objects.create( - code="WTB30-000024", + code="LOC_TEST_01-000024", name_fr="Capteur 24", structure=self.structure, location=self.location, @@ -456,10 +456,10 @@ class StructuresLocationDetailAddAssetModalTest(TestCase): ) next_code = get_next_structure_asset_code(location=self.location) - self.assertEqual(next_code, "WTB30-000025") + self.assertEqual(next_code, "LOC_TEST_01-000025") suggested = get_suggested_asset_code("structures", location_id=self.location.id) - self.assertEqual(suggested, "WTB30-000025") + self.assertEqual(suggested, "LOC_TEST_01-000025") # Test API endpoint self.client.force_login(self.user) @@ -470,13 +470,13 @@ class StructuresLocationDetailAddAssetModalTest(TestCase): self.assertEqual(api_res.status_code, 200) data = api_res.json() self.assertTrue(data.get("success")) - self.assertEqual(data.get("suggested_code"), "WTB30-000025") + self.assertEqual(data.get("suggested_code"), "LOC_TEST_01-000025") # Test page rendering has updated suggested code page_res = self.client.get(reverse("assets:structures_locations_detail", args=[self.location.id])) self.assertEqual(page_res.status_code, 200) - self.assertEqual(page_res.context["suggested_code"], "WTB30-000025") - self.assertContains(page_res, 'value="WTB30-000025"') + self.assertEqual(page_res.context["suggested_code"], "LOC_TEST_01-000025") + self.assertContains(page_res, 'value="LOC_TEST_01-000025"') # Test intervention code suggestion (mobile / desktop intervention) from interventions.models import Intervention, InterventionLocation @@ -495,14 +495,120 @@ class StructuresLocationDetailAddAssetModalTest(TestCase): ) itv_suggested = get_suggested_asset_code("structures", intervention_id=itv.id) - self.assertEqual(itv_suggested, "WTB30-000025") + self.assertEqual(itv_suggested, "LOC_TEST_01-000025") itv_api_res = self.client.get(reverse("assets:get_suggested_asset_code_api"), { "thematic_code": "structures", "intervention_id": itv.id }) self.assertEqual(itv_api_res.status_code, 200) - self.assertEqual(itv_api_res.json().get("suggested_code"), "WTB30-000025") + self.assertEqual(itv_api_res.json().get("suggested_code"), "LOC_TEST_01-000025") + + def test_code_suggestion_specific_location_avoids_other_sublocation_dominant_prefix(self): + """ + Verify that when generating a code for location WTB23B, it generates WTB23B-000001 (or next WTB23B) + even if another location WTB23J under the same structure has many existing assets. + """ + from assets.models.structures import Structure, StructureLocation, StructureGeoAsset, StructureAssetModel + from assets.utils.asset_code_generator import get_next_structure_asset_code, get_suggested_asset_code + from interventions.models import Intervention, InterventionLocation + from django.contrib.contenttypes.models import ContentType + + struct_belliard = Structure.objects.create(code="WTB23", name_fr="Tunnel Belliard") + loc_b = StructureLocation.objects.create(code="WTB23B", name_fr="Belliard B", structure=struct_belliard) + loc_j = StructureLocation.objects.create(code="WTB23J", name_fr="Belliard J", structure=struct_belliard) + + model = StructureAssetModel.objects.create(code="MOD_BELL", name_fr="Modèle Belliard", category=self.category) + + # Create 10 assets for WTB23J + for i in range(1, 11): + StructureGeoAsset.objects.create( + code=f"WTB23J-{i:06d}", + name_fr=f"Asset J {i}", + structure=struct_belliard, + location=loc_j, + model=model + ) + + # For location WTB23B with 0 existing assets, it should suggest WTB23B-000001, NOT WTB23J-000011! + suggested_b = get_next_structure_asset_code(location=loc_b) + self.assertEqual(suggested_b, "WTB23B-000001") + + # For an intervention linked to WTB23B + itv = Intervention.objects.create(code="ITV_WTB23B", title="Cyclique Belliard", thematic=self.thematic) + loc_ct = ContentType.objects.get_for_model(loc_b) + InterventionLocation.objects.create(intervention=itv, content_type=loc_ct, object_id=loc_b.id) + + itv_code = get_suggested_asset_code("structures", intervention_id=itv.id) + self.assertEqual(itv_code, "WTB23B-000001") + + # If WTB23B already has an asset WTB23B-000001, it should suggest WTB23B-000002 + StructureGeoAsset.objects.create( + code="WTB23B-000001", + name_fr="Asset B 1", + structure=struct_belliard, + location=loc_b, + model=model + ) + itv_code_2 = get_suggested_asset_code("structures", intervention_id=itv.id) + self.assertEqual(itv_code_2, "WTB23B-000002") + + def test_create_asset_api_success_with_model_and_intervention(self): + """ + Verify create_asset_api creates a StructureGeoAsset with model, resolves location and links to intervention. + """ + from assets.models.structures import Structure, StructureLocation, StructureGeoAsset, StructureAssetModel + from interventions.models import Intervention, InterventionLocation, InterventionAsset + from django.contrib.contenttypes.models import ContentType + + struct = Structure.objects.create(code="WTB23_TEST", name_fr="Tunnel Belliard Test") + loc = StructureLocation.objects.create(code="WTB23B_TEST", name_fr="Belliard B Test", structure=struct) + model = StructureAssetModel.objects.create(code="MOD_API_TEST", name_fr="Modèle API", category=self.category) + + # Allow model for category + ct = ContentType.objects.get_for_model(StructureGeoAsset) + self.category.allowed_models.add(ct) + + itv = Intervention.objects.create( + code="ITV_CREATE_ASSET", + title="Intervention Création", + thematic=self.thematic, + maintain_type="preventive" + ) + loc_ct = ContentType.objects.get_for_model(loc) + InterventionLocation.objects.create(intervention=itv, content_type=loc_ct, object_id=loc.id) + + self.client.force_login(self.user) + payload = { + "thematic_code": "structures", + "category_id": self.category.id, + "model_id": model.id, + "code": "WTB23B_TEST-000001", + "name_fr": "Nouvel Équipement Test", + "intervention_id": itv.id + } + + res = self.client.post( + reverse("assets:create_asset_api"), + data=json.dumps(payload), + content_type="application/json" + ) + self.assertEqual(res.status_code, 200, f"Error: {res.content}") + data = res.json() + self.assertTrue(data.get("success")) + self.assertTrue(data.get("intervention_linked")) + self.assertTrue(data.get("needs_template_selection")) + + # Verify asset created in DB + created_asset = StructureGeoAsset.objects.get(code="WTB23B_TEST-000001") + self.assertEqual(created_asset.model, model) + self.assertEqual(created_asset.category, self.category) + self.assertEqual(created_asset.location, loc) + self.assertEqual(created_asset.structure, struct) + self.assertEqual(created_asset.name_fr, "Nouvel Équipement Test") + + # Verify linked to intervention + self.assertTrue(InterventionAsset.objects.filter(intervention=itv, object_id=created_asset.id).exists()) diff --git a/loko/assets/utils/asset_code_generator.py b/loko/assets/utils/asset_code_generator.py index 83f29f3..082fd78 100644 --- a/loko/assets/utils/asset_code_generator.py +++ b/loko/assets/utils/asset_code_generator.py @@ -19,6 +19,8 @@ def get_next_structure_asset_code( """ from assets.models.structures import Structure, StructureLocation, StructureGeoAsset + from django.db.models import Q + if intervention_id and not intervention: try: from interventions.models import Intervention @@ -30,7 +32,16 @@ def get_next_structure_asset_code( # 1. Look for linked structure locations for loc_link in intervention.locations.all(): loc_obj = loc_link.location - if isinstance(loc_obj, StructureLocation) or hasattr(loc_obj, 'structure'): + if isinstance(loc_obj, StructureLocation): + location = loc_obj + structure = loc_obj.structure + break + elif isinstance(loc_obj, Structure): + structure = loc_obj + if loc_obj.main_location: + location = loc_obj.main_location + break + elif hasattr(loc_obj, 'structure'): location = loc_obj structure = getattr(loc_obj, 'structure', None) break @@ -39,10 +50,13 @@ def get_next_structure_asset_code( if not structure and not location: for a_link in intervention.interventionasset_set.all(): asset_obj = a_link.asset - if asset_obj and hasattr(asset_obj, 'structure') and asset_obj.structure: - structure = asset_obj.structure - location = getattr(asset_obj, 'location', None) - break + if asset_obj: + if hasattr(asset_obj, 'location') and asset_obj.location: + location = asset_obj.location + if hasattr(asset_obj, 'structure') and asset_obj.structure: + structure = asset_obj.structure + if location or structure: + break # 3. Fallback: match by location_code / address if structure exists if not structure and not location and intervention.location_code: @@ -54,6 +68,8 @@ def get_next_structure_asset_code( struct_match = Structure.objects.filter(code__iexact=intervention.location_code).first() if struct_match: structure = struct_match + if struct_match.main_location: + location = struct_match.main_location if location_id and not location: try: @@ -73,22 +89,41 @@ def get_next_structure_asset_code( if not structure and not location: return "" - # Fetch existing asset codes for this structure or location - if structure: - codes = list(StructureGeoAsset.objects.filter(structure=structure).values_list('code', flat=True)) + # Target prefix is the location code if location is present, otherwise structure code + target_prefix_base = location.code if location and location.code else (structure.code if structure and structure.code else "") + if not target_prefix_base: + return "" + + # Look for existing asset codes + # Priority: codes matching this specific location / prefix + if location: + codes = list( + StructureGeoAsset.objects.filter( + Q(location=location) | Q(code__istartswith=location.code) + ).values_list('code', flat=True) + ) + elif structure: + codes = list( + StructureGeoAsset.objects.filter( + Q(structure=structure) | Q(code__istartswith=structure.code) + ).values_list('code', flat=True) + ) else: - codes = list(StructureGeoAsset.objects.filter(location=location).values_list('code', flat=True)) + codes = [] pattern = re.compile(r'^(.*?)(\d+)$') matches = [] for c in codes: if not c: continue - m = pattern.match(c.strip()) + c_str = c.strip() + m = pattern.match(c_str) if m: prefix = m.group(1) num_str = m.group(2) - matches.append((prefix, int(num_str), len(num_str))) + # Only consider codes that start with our target prefix (case-insensitive) + if prefix.upper().startswith(target_prefix_base.upper()): + matches.append((prefix, int(num_str), len(num_str))) if matches: prefix_counts = Counter(m[0] for m in matches) @@ -104,12 +139,27 @@ def get_next_structure_asset_code( suggested_code = f"{dominant_prefix}{str(next_num).zfill(digit_len)}" return suggested_code else: - struct_code = structure.code if structure else (location.code if location else "STR") + # Default code formatting: determine digit length and separator from existing structure assets if any + digit_len = 6 + separator = "-" + if structure: + struct_codes = list(StructureGeoAsset.objects.filter(structure=structure).values_list('code', flat=True)) + struct_matches = [pattern.match(sc.strip()) for sc in struct_codes if sc and pattern.match(sc.strip())] + if struct_matches: + digit_len = max(len(sm.group(2)) for sm in struct_matches) + sample_prefix = struct_matches[0].group(1) + if sample_prefix.endswith('-'): + separator = "-" + elif sample_prefix.endswith('_'): + separator = "_" + elif not sample_prefix[-1].isalnum(): + separator = sample_prefix[-1] + next_num = 1 - suggested_code = f"{struct_code}-{str(next_num).zfill(6)}" + suggested_code = f"{target_prefix_base}{separator}{str(next_num).zfill(digit_len)}" while StructureGeoAsset.objects.filter(code=suggested_code).exists(): next_num += 1 - suggested_code = f"{struct_code}-{str(next_num).zfill(6)}" + suggested_code = f"{target_prefix_base}{separator}{str(next_num).zfill(digit_len)}" return suggested_code diff --git a/loko/assets/views/asset_editing.py b/loko/assets/views/asset_editing.py index 000e3ef..eaf7632 100644 --- a/loko/assets/views/asset_editing.py +++ b/loko/assets/views/asset_editing.py @@ -29,6 +29,15 @@ from interventions.models import Intervention, InterventionAsset from assets.views.locations import ASSETS_LOCATION_REGISTRY +def _has_model_field(model_or_instance, field_name): + """Safely check if a model class or instance has a specific field in its _meta.""" + try: + model_or_instance._meta.get_field(field_name) + return True + except Exception: + return False + + # ----------------------------------------------------------------------- # Asset Document API # ----------------------------------------------------------------------- @@ -640,7 +649,7 @@ def replace_asset(request, asset_model, asset_id): setattr(new_asset, field_name, value) # Handle new model if specified - if new_model_id and hasattr(new_asset, 'model'): + if new_model_id and _has_model_field(new_asset, 'model'): try: model_field = model_class._meta.get_field('model') related_model = model_field.related_model @@ -1408,13 +1417,22 @@ def _get_asset_classes_for_category(thematic_code, category): ct = allowed_cts.first() asset_class = ct.model_class() if asset_class: - import importlib - model_class_name = f"{asset_class.__name__}Model" - try: - module = importlib.import_module(asset_class.__module__) - asset_model_class = getattr(module, model_class_name, None) - except Exception: - asset_model_class = None + asset_model_class = None + if hasattr(asset_class, '_meta'): + try: + model_field = asset_class._meta.get_field('model') + if hasattr(model_field, 'related_model'): + asset_model_class = model_field.related_model + except Exception: + asset_model_class = None + if not asset_model_class: + import importlib + model_class_name = f"{asset_class.__name__}Model" + try: + module = importlib.import_module(asset_class.__module__) + asset_model_class = getattr(module, model_class_name, None) + except Exception: + asset_model_class = None return asset_class, asset_model_class category_code = category.code if category and category.code else None @@ -1451,6 +1469,9 @@ def _import_asset_classes(thematic_code, class_names): 'artworks': 'assets.models.artworks', 'sign': 'assets.models.sign', 'roads': 'assets.models.roads', + 'cleanliness': 'assets.models.cleanliness', + 'buildings': 'assets.models.buildings', + 'publicbuildings': 'assets.models.buildings', } module_name = thematic_module_map.get(thematic_code) @@ -1462,6 +1483,13 @@ def _import_asset_classes(thematic_code, class_names): module = importlib.import_module(module_name) asset_class = getattr(module, asset_class_name, None) model_class = getattr(module, model_class_name, None) if model_class_name else None + if asset_class and not model_class and hasattr(asset_class, '_meta'): + try: + model_field = asset_class._meta.get_field('model') + if hasattr(model_field, 'related_model'): + model_class = model_field.related_model + except Exception: + pass return asset_class, model_class except (ImportError, AttributeError): return None, None @@ -1469,8 +1497,6 @@ def _import_asset_classes(thematic_code, class_names): def _get_asset_model_class_for_thematic(thematic_code): """Get the appropriate asset model class for a thematic.""" - from django.apps import apps - # Direct imports for known thematics if thematic_code == 'structures': from assets.models.structures import StructureGeoAsset, StructureAssetModel @@ -1490,23 +1516,57 @@ def _get_asset_model_class_for_thematic(thematic_code): elif thematic_code == 'its': from assets.models.its import ITSGeoAsset, ITSAssetModel return ITSGeoAsset, ITSAssetModel + elif thematic_code == 'artworks': + from assets.models.artworks import ArtworkFountain + return ArtworkFountain, None + elif thematic_code == 'sign': + from assets.models.sign import SignPole, SignPoleModel + return SignPole, SignPoleModel + elif thematic_code == 'roads': + from assets.models.roads import Roadway, RoadwayModel + return Roadway, RoadwayModel + elif thematic_code in ('publicbuildings', 'buildings'): + from assets.models.buildings import BuildingAsset + return BuildingAsset, None + elif thematic_code == 'cleanliness': + from assets.models.cleanliness import CleanLitterBin, CleanLitterBinModel + return CleanLitterBin, CleanLitterBinModel return None, None def _get_asset_model_instance(thematic_code, model_id, category=None): """Get the asset model instance for a given thematic, model ID, and optionally category.""" + if not model_id: + return None + # Try category-specific lookup first + asset_model_class = None if category: _, asset_model_class = _get_asset_classes_for_category(thematic_code, category) - else: + if not asset_model_class: _, asset_model_class = _get_asset_model_class_for_thematic(thematic_code) if asset_model_class and model_id: try: return asset_model_class.objects.get(pk=model_id) except asset_model_class.DoesNotExist: - return None + pass + + # Universal fallback: search across all AbstractAssetModel subclasses + from django.apps import apps + from assets.models.core import AbstractAssetModel + for model in apps.get_models(): + if model._meta.abstract: + continue + try: + if issubclass(model, AbstractAssetModel): + instance = model.objects.filter(pk=model_id).first() + if instance: + return instance + except (TypeError, Exception): + continue + return None @@ -1590,7 +1650,22 @@ def create_asset_api(request): }, status=404) allowed_actions = get_allowed_actions_for_thematic(request.user, thematic) - if 'create' not in allowed_actions: + can_create = 'create' in allowed_actions + + # If creating from an intervention, check if user has permission to edit assets on this intervention + intervention = None + if intervention_id: + try: + intervention = Intervention.objects.get(pk=intervention_id) + from interventions.permissions import get_allowed_update_fields, can_edit_intervention + if can_edit_intervention(request.user, intervention): + intv_allowed_fields = get_allowed_update_fields(request.user, intervention) + if 'assets' in intv_allowed_fields: + can_create = True + except Intervention.DoesNotExist: + intervention = None + + if not can_create: return JsonResponse({ 'success': False, 'error': _("Vous n'avez pas la permission de créer des équipements") @@ -1639,23 +1714,55 @@ def create_asset_api(request): 'error': _("Modèle requis") }, status=400) - # If creating from an intervention without explicit location, get the first location from intervention - intervention = None - if intervention_id: - try: - intervention = Intervention.objects.get(pk=intervention_id) + # If creating from an intervention without explicit location, resolve location/structure from intervention + if intervention and not location_id and not structure_id: + for loc_link in intervention.locations.all(): + loc_obj = loc_link.location + if loc_obj: + if thematic_code == 'structures': + from assets.models.structures import StructureLocation, Structure + if isinstance(loc_obj, StructureLocation): + location_id = loc_obj.id + structure_id = getattr(loc_obj, 'structure_id', None) + break + elif isinstance(loc_obj, Structure): + structure_id = loc_obj.id + if loc_obj.main_location: + location_id = loc_obj.main_location.id + break + elif hasattr(loc_obj, 'structure'): + location_id = loc_obj.id + structure_id = getattr(loc_obj, 'structure_id', None) + break + elif thematic_code == 'trafficlights': + from assets.models.trafficlights import TrafficLightIntersection + if isinstance(loc_obj, TrafficLightIntersection) or hasattr(loc_obj, 'pk'): + location_id = loc_obj.id + break + else: + if hasattr(loc_obj, 'pk'): + location_id = loc_obj.id + break - # If no location_id specified, try to get first location from intervention - if not location_id and not structure_id: - from interventions.models import InterventionLocation - first_location_link = intervention.locations.first() - - if first_location_link: - # Set location_id or structure_id based on what we find - location_id = first_location_link.object_id - # We'll use the location's content type to determine what to set - except Intervention.DoesNotExist: - intervention = None + # Fallback: match by location_code on intervention + if not location_id and not structure_id and intervention.location_code: + if thematic_code == 'structures': + from assets.models.structures import StructureLocation, Structure + loc_match = StructureLocation.objects.filter(code__iexact=intervention.location_code).first() + if loc_match: + location_id = loc_match.id + structure_id = getattr(loc_match, 'structure_id', None) + else: + struct_match = Structure.objects.filter(code__iexact=intervention.location_code).first() + if struct_match: + structure_id = struct_match.id + if struct_match.main_location: + location_id = struct_match.main_location.id + elif thematic_code == 'trafficlights': + from assets.models.trafficlights import TrafficLightIntersection + inter_match = TrafficLightIntersection.objects.filter(code__iexact=intervention.location_code).first() + if inter_match: + location_id = inter_match.id with transaction.atomic(): # Create the new asset @@ -1663,16 +1770,16 @@ def create_asset_api(request): new_asset.code = code # Only set name fields if the model has them - if hasattr(new_asset, 'name_fr'): + if _has_model_field(new_asset, 'name_fr'): new_asset.name_fr = name_fr if name_fr else None - if hasattr(new_asset, 'name_nl'): + if _has_model_field(new_asset, 'name_nl'): new_asset.name_nl = name_nl if name_nl else None # Only set category if the model has it - if hasattr(new_asset, 'category'): + if _has_model_field(new_asset, 'category'): new_asset.category = category - if hasattr(new_asset, 'model'): + if _has_model_field(new_asset, 'model'): new_asset.model = asset_model_instance new_asset.status = 'active' new_asset.validation_status = 'to_be_validated' @@ -1691,7 +1798,7 @@ def create_asset_api(request): pass # Handle structure (for structures) - if structure_id: + if structure_id and not getattr(new_asset, 'structure', None): from assets.models.structures import Structure try: structure = Structure.objects.get(pk=structure_id) @@ -1700,7 +1807,7 @@ def create_asset_api(request): pass # Handle intersection (for trafficlights) - if thematic_code == 'trafficlights': + elif thematic_code == 'trafficlights': intersection_id = data.get('intersection_id') or location_id if not intersection_id: @@ -1725,20 +1832,33 @@ def create_asset_api(request): from assets.models.trafficlights import TrafficLightPole try: pole = TrafficLightPole.objects.get(pk=pole_id) - if hasattr(new_asset, 'pole'): + if _has_model_field(new_asset, 'pole'): new_asset.pole = pole except TrafficLightPole.DoesNotExist: pass + # Handle generic location for other thematics + elif location_id: + for field in asset_class._meta.get_fields(): + if hasattr(field, 'related_model') and field.related_model and not field.auto_created: + try: + loc_instance = field.related_model.objects.get(pk=location_id) + setattr(new_asset, field.name, loc_instance) + break + except Exception: + continue + # 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'): + if _has_model_field(new_asset, field_name) and field_name not in ('id', 'code'): setattr(new_asset, field_name, value) # Assign position from location / hierarchy location = getattr(new_asset, 'location', None) or getattr(new_asset, 'intersection', None) + if not location and getattr(new_asset, 'structure', None): + location = getattr(new_asset.structure, 'main_location', None) _assign_default_position(new_asset, location, thematic_code) new_asset.save()