refactor: harden API error handling in frontend modals and update asset code generation prefix in tests
This commit is contained in:
parent
24bf8a9bb5
commit
a410cc855f
5 changed files with 388 additions and 82 deletions
|
|
@ -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 = '<i class="bi bi-check-circle me-1"></i>' + 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 = '<i class="bi bi-exclamation-triangle me-1"></i>' + (result.error || gettext('Erreur lors de la création'));
|
||||
alertContainer.innerHTML = '<i class="bi bi-exclamation-triangle me-1"></i>' + 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 = '<i class="bi bi-exclamation-triangle me-1"></i>' + gettext('Erreur de connexion');
|
||||
alertContainer.innerHTML = '<i class="bi bi-exclamation-triangle me-1"></i>' + (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 = '<i class="bi bi-check-circle me-1"></i>' + 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 = '<i class="bi bi-exclamation-triangle me-1"></i>' + (data.error || gettext('Erreur lors de la création'));
|
||||
templateAlert.innerHTML = '<i class="bi bi-exclamation-triangle me-1"></i>' + 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 = '<i class="bi bi-exclamation-triangle me-1"></i>' + gettext('Erreur de connexion');
|
||||
templateAlert.innerHTML = '<i class="bi bi-exclamation-triangle me-1"></i>' + (error.message || gettext('Erreur de connexion'));
|
||||
templateAlert.classList.remove('d-none');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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', `<i class="bi bi-check-circle me-1"></i>${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', `<i class="bi bi-exclamation-triangle me-1"></i>${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', `<i class="bi bi-exclamation-triangle me-1"></i>${escapeHtml(errorMsg)}`, 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating asset:', error);
|
||||
setModalAlert('unifiedAddAssetAlert', `<i class="bi bi-exclamation-triangle me-1"></i>${escapeHtml(gettext('Erreur de connexion.'))}`, 'danger');
|
||||
setModalAlert('unifiedAddAssetAlert', `<i class="bi bi-exclamation-triangle me-1"></i>${escapeHtml(error.message || gettext('Erreur de connexion.'))}`, 'danger');
|
||||
} finally {
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = false;
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in a new issue