Skip to content

Commit 6ed221d

Browse files
authored
Merge pull request #452 from ansforge/feat/converter/nomenclatures/add-nomenclature-conversion
[ Converter / Nomenclatures ] Conversion de nomenclatures EDA
2 parents 91a1690 + df6f962 commit 6ed221d

6 files changed

Lines changed: 424 additions & 0 deletions

File tree

converter/converter/cisu/create_case/create_case_cisu_constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ class CreateCaseCISUConstants:
7474
QUALIFICATION_VICTIM_COUNT_PATH = f"{QUALIFICATION_VICTIMS_PATH}.count"
7575
QUALIFICATION_DETAILS_PRIORITY_PATH = f"{QUALIFICATION_PATH}.details.priority"
7676
QUALIFICATION_WHATS_HAPPEN_PATH = f"{QUALIFICATION_PATH}.whatsHappen"
77+
QUALIFICATION_HEALTH_MOTIVE_PATH = f"{QUALIFICATION_PATH}.healthMotive"
78+
QUALIFICATION_RISK_THREAT_PATH = f"{QUALIFICATION_PATH}.riskThreat"
79+
QUALIFICATION_LOCATION_KIND_PATH = f"{QUALIFICATION_PATH}.locationKind"
7780

7881
MEDICAL_NOTE_PATH = "$.medicalNote"
7982

converter/converter/cisu/create_case/create_case_cisu_converter.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,34 @@
2424
set_value,
2525
)
2626
from converter.cisu.base_cisu_converter import BaseCISUConverter
27+
from converter.nomenclatures.utils import (
28+
apply_nomenclature_mapping,
29+
apply_nomenclature_mapping_to_list,
30+
)
31+
from converter.nomenclatures.from_cisu_to_rs.whats_happen import (
32+
MAP as CISU_TO_RS_WHATS_HAPPEN_MAP,
33+
)
34+
from converter.nomenclatures.from_cisu_to_rs.health_motive import (
35+
MAP as CISU_TO_RS_HEALTH_MOTIVE_MAP,
36+
)
37+
from converter.nomenclatures.from_cisu_to_rs.risk_threat import (
38+
MAP as CISU_TO_RS_RISK_THREAT_MAP,
39+
)
40+
from converter.nomenclatures.from_cisu_to_rs.location_kind import (
41+
MAP as CISU_TO_RS_LOCATION_KIND_MAP,
42+
)
43+
from converter.nomenclatures.from_rs_to_cisu.whats_happen import (
44+
MAP as RS_TO_CISU_WHATS_HAPPEN_MAP,
45+
)
46+
from converter.nomenclatures.from_rs_to_cisu.health_motive import (
47+
MAP as RS_TO_CISU_HEALTH_MOTIVE_MAP,
48+
)
49+
from converter.nomenclatures.from_rs_to_cisu.risk_threat import (
50+
MAP as RS_TO_CISU_RISK_THREAT_MAP,
51+
)
52+
from converter.nomenclatures.from_rs_to_cisu.location_kind import (
53+
MAP as RS_TO_CISU_LOCATION_KIND_MAP,
54+
)
2755
import logging
2856

2957
logger = logging.getLogger(__name__)
@@ -283,6 +311,29 @@ def add_call_taker_org_to_custom_map(json_data: Dict[str, Any]):
283311

284312
return json_data
285313

314+
def apply_qualification_nomenclature_mappings(json_data: Dict[str, Any]):
315+
logger.debug("Applying qualification nomenclature mappings")
316+
apply_nomenclature_mapping(
317+
json_data,
318+
CreateCaseCISUConstants.QUALIFICATION_WHATS_HAPPEN_PATH,
319+
CISU_TO_RS_WHATS_HAPPEN_MAP,
320+
)
321+
apply_nomenclature_mapping(
322+
json_data,
323+
CreateCaseCISUConstants.QUALIFICATION_HEALTH_MOTIVE_PATH,
324+
CISU_TO_RS_HEALTH_MOTIVE_MAP,
325+
)
326+
apply_nomenclature_mapping_to_list(
327+
json_data,
328+
CreateCaseCISUConstants.QUALIFICATION_RISK_THREAT_PATH,
329+
CISU_TO_RS_RISK_THREAT_MAP,
330+
)
331+
apply_nomenclature_mapping(
332+
json_data,
333+
CreateCaseCISUConstants.QUALIFICATION_LOCATION_KIND_PATH,
334+
CISU_TO_RS_LOCATION_KIND_MAP,
335+
)
336+
286337
# Create independent envelope copy without usecase for output
287338
output_json = cls.copy_cisu_input_content(input_json)
288339

@@ -300,6 +351,8 @@ def add_call_taker_org_to_custom_map(json_data: Dict[str, Any]):
300351
set_default_location_freetext(output_use_case_json)
301352
add_location_detail(output_use_case_json)
302353

354+
apply_qualification_nomenclature_mappings(output_use_case_json)
355+
303356
add_case_priority(output_use_case_json)
304357
add_to_initial_alert_notes(
305358
output_use_case_json,
@@ -388,6 +441,29 @@ def add_default_external_info_type(json_data: Dict[str, Any]):
388441
CreateCaseCISUConstants.DEFAULT_LOCATION_EXTERNAL_INFO_TYPE,
389442
)
390443

444+
def apply_qualification_nomenclature_mappings(json_data: Dict[str, Any]):
445+
logger.debug("Applying qualification nomenclature mappings")
446+
apply_nomenclature_mapping(
447+
json_data,
448+
CreateCaseCISUConstants.QUALIFICATION_WHATS_HAPPEN_PATH,
449+
RS_TO_CISU_WHATS_HAPPEN_MAP,
450+
)
451+
apply_nomenclature_mapping(
452+
json_data,
453+
CreateCaseCISUConstants.QUALIFICATION_HEALTH_MOTIVE_PATH,
454+
RS_TO_CISU_HEALTH_MOTIVE_MAP,
455+
)
456+
apply_nomenclature_mapping_to_list(
457+
json_data,
458+
CreateCaseCISUConstants.QUALIFICATION_RISK_THREAT_PATH,
459+
RS_TO_CISU_RISK_THREAT_MAP,
460+
)
461+
apply_nomenclature_mapping(
462+
json_data,
463+
CreateCaseCISUConstants.QUALIFICATION_LOCATION_KIND_PATH,
464+
RS_TO_CISU_LOCATION_KIND_MAP,
465+
)
466+
391467
# Create independent envelope copy without usecase for output
392468
output_json = cls.copy_rs_input_content(input_json)
393469

@@ -420,6 +496,8 @@ def add_default_external_info_type(json_data: Dict[str, Any]):
420496
CreateCaseCISUConstants.DEFAULT_LOCATION_COUNTRY,
421497
)
422498

499+
apply_qualification_nomenclature_mappings(output_usecase_json)
500+
423501
if not is_field_completed(
424502
output_usecase_json, CreateCaseCISUConstants.QUALIFICATION_WHATS_HAPPEN_PATH
425503
):
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
from typing import Any, Dict, Optional
2+
3+
from converter.utils import delete_paths, get_field_value, set_value
4+
5+
6+
def apply_nomenclature_mapping(
7+
json_data: Dict[str, Any],
8+
field_path: str,
9+
mapping: Dict[str, Optional[Dict[str, str]]],
10+
) -> None:
11+
"""
12+
Looks up json_data[field_path].code in the given nomenclature mapping and
13+
updates json_data accordingly:
14+
- no entry for the code: json_data is left untouched
15+
- entry maps to a dict: the field at field_path is replaced by that dict
16+
- entry maps to None: the field at field_path is removed
17+
"""
18+
code = get_field_value(json_data, f"{field_path}.code")
19+
if code is None or code not in mapping:
20+
return
21+
22+
new_value = mapping[code]
23+
if new_value is None:
24+
delete_paths(json_data, [field_path])
25+
else:
26+
set_value(json_data, field_path, new_value)
27+
28+
29+
def apply_nomenclature_mapping_to_list(
30+
json_data: Dict[str, Any],
31+
field_path: str,
32+
mapping: Dict[str, Optional[Dict[str, str]]],
33+
) -> None:
34+
"""
35+
Same semantics as apply_nomenclature_mapping, applied item by item to the
36+
array field at field_path:
37+
- an item's code has no entry in mapping: the item is left untouched
38+
- entry maps to a dict: the item is replaced by that dict
39+
- entry maps to None: the item is removed from the array
40+
If the array ends up empty, the field itself is removed.
41+
"""
42+
items = get_field_value(json_data, field_path)
43+
if not isinstance(items, list):
44+
return
45+
46+
updated_items = []
47+
for item in items:
48+
code = item.get("code") if isinstance(item, dict) else None
49+
if code is None or code not in mapping:
50+
updated_items.append(item)
51+
continue
52+
53+
new_value = mapping[code]
54+
if new_value is not None:
55+
updated_items.append(new_value)
56+
57+
if updated_items:
58+
set_value(json_data, field_path, updated_items)
59+
else:
60+
delete_paths(json_data, [field_path])
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
"""
2+
Integration tests exercising CreateCaseCISUConverter.from_cisu_to_rs / from_rs_to_cisu
3+
end-to-end against real nomenclature map entries, for the qualification.whatsHappen,
4+
qualification.healthMotive, qualification.riskThreat and qualification.locationKind
5+
fields. Complements the unit tests in tests/nomenclatures/test_utils.py (which cover
6+
apply_nomenclature_mapping/apply_nomenclature_mapping_to_list in isolation) by proving
7+
the maps are actually wired into the converter and that real fixture-shaped messages
8+
produce the expected replace/remove behaviour.
9+
"""
10+
11+
import copy
12+
13+
from converter.cisu.create_case.create_case_cisu_converter import (
14+
CreateCaseCISUConverter,
15+
)
16+
from tests.cisu.helpers import get_edxl_message
17+
from tests.constants import TestConstants
18+
from tests.test_helpers import TestHelper
19+
20+
RC_EDA_BASE = TestHelper.load_json_file(
21+
"tests/fixtures/RC-EDA/RC-EDA_required_fields.json"
22+
)
23+
RS_EDA_BASE = TestHelper.load_json_file(
24+
"tests/fixtures/RS-EDA/cisu_case/RS-EDA_required_fields.json"
25+
)
26+
27+
28+
def cisu_to_rs_message(qualification_overrides: dict) -> dict:
29+
content = copy.deepcopy(RC_EDA_BASE)
30+
content["createCase"]["qualification"].update(qualification_overrides)
31+
edxl = TestHelper.combine_edxl_envelope_and_message(
32+
TestConstants.EDXL_FIRE_TO_HEALTH_ENVELOPE_PATH, content
33+
)
34+
result = CreateCaseCISUConverter.from_cisu_to_rs(edxl)
35+
return get_edxl_message(result)["createCaseHealth"]["qualification"]
36+
37+
38+
def rs_to_cisu_message(qualification_overrides: dict) -> dict:
39+
content = copy.deepcopy(RS_EDA_BASE)
40+
content["createCaseHealth"]["qualification"].update(qualification_overrides)
41+
edxl = TestHelper.combine_edxl_envelope_and_message(
42+
TestConstants.EDXL_HEALTH_TO_FIRE_ENVELOPE_PATH, content
43+
)
44+
result = CreateCaseCISUConverter.from_rs_to_cisu(edxl)
45+
return get_edxl_message(result)["createCase"]["qualification"]
46+
47+
48+
# --- from_cisu_to_rs: code has no entry in the map -> field untouched ---
49+
50+
51+
def test_from_cisu_to_rs_whats_happen_unmapped_code_is_untouched():
52+
unmapped = {"code": "C99.99.99", "label": "Code inconnu"}
53+
qualification = cisu_to_rs_message({"whatsHappen": unmapped})
54+
assert qualification["whatsHappen"] == unmapped
55+
56+
57+
# --- from_cisu_to_rs: code has a non-None entry -> field replaced ---
58+
59+
60+
def test_from_cisu_to_rs_whats_happen_mapped_code_is_replaced():
61+
qualification = cisu_to_rs_message(
62+
{"whatsHappen": {"code": "C02.05.03", "label": "Menace de suicide"}}
63+
)
64+
assert qualification["whatsHappen"] == {
65+
"code": "C02.18.00",
66+
"label": "Problème psychiatrique, menace de suicide",
67+
}
68+
69+
70+
def test_from_cisu_to_rs_health_motive_mapped_code_is_replaced():
71+
qualification = cisu_to_rs_message(
72+
{"healthMotive": {"code": "M20.00", "label": "Inconscience"}}
73+
)
74+
assert qualification["healthMotive"] == {
75+
"code": "M08.00.00",
76+
"label": "Personne inconsciente ",
77+
}
78+
79+
80+
def test_from_cisu_to_rs_location_kind_mapped_code_is_replaced():
81+
qualification = cisu_to_rs_message(
82+
{
83+
"locationKind": {
84+
"code": "L02.02.04",
85+
"label": "Aire de repos sur voie rapide",
86+
}
87+
}
88+
)
89+
assert qualification["locationKind"] == {
90+
"code": "L02.02.03",
91+
"label": "Aire de repos ou de service sur voie rapide",
92+
}
93+
94+
95+
# --- from_cisu_to_rs: riskThreat is an array -> per-item behaviour ---
96+
97+
98+
def test_from_cisu_to_rs_risk_threat_mapped_to_none_removes_item_and_field():
99+
qualification = cisu_to_rs_message(
100+
{"riskThreat": [{"code": "R36", "label": "Menace"}]}
101+
)
102+
assert "riskThreat" not in qualification
103+
104+
105+
def test_from_cisu_to_rs_risk_threat_removes_only_the_none_mapped_item():
106+
unmapped = {"code": "R99", "label": "Autre menace"}
107+
qualification = cisu_to_rs_message(
108+
{"riskThreat": [{"code": "R36", "label": "Menace"}, unmapped]}
109+
)
110+
assert qualification["riskThreat"] == [unmapped]
111+
112+
113+
# --- from_rs_to_cisu: same three cases, using the reverse maps ---
114+
115+
116+
def test_from_rs_to_cisu_whats_happen_unmapped_code_is_untouched():
117+
unmapped = {"code": "C99.99.99", "label": "Code inconnu"}
118+
qualification = rs_to_cisu_message({"whatsHappen": unmapped})
119+
assert qualification["whatsHappen"] == unmapped
120+
121+
122+
def test_from_rs_to_cisu_whats_happen_mapped_code_is_replaced():
123+
qualification = rs_to_cisu_message(
124+
{"whatsHappen": {"code": "C02.17.00", "label": "Mort suspecte/cadavre"}}
125+
)
126+
assert qualification["whatsHappen"] == {
127+
"code": "C10.02.01",
128+
"label": "Mort suspecte",
129+
}
130+
131+
132+
def test_from_rs_to_cisu_health_motive_mapped_code_is_replaced():
133+
qualification = rs_to_cisu_message(
134+
{"healthMotive": {"code": "M08.00.00", "label": "Personne inconsciente"}}
135+
)
136+
assert qualification["healthMotive"] == {
137+
"code": "M22.00",
138+
"label": "Personne inconsciente",
139+
}
140+
141+
142+
def test_from_rs_to_cisu_location_kind_mapped_code_is_replaced():
143+
qualification = rs_to_cisu_message(
144+
{"locationKind": {"code": "L01.01.05", "label": "Pavillon"}}
145+
)
146+
assert qualification["locationKind"] == {
147+
"code": "L01.01.00",
148+
"label": "Maison particulière, pavillon",
149+
}
150+
151+
152+
def test_from_rs_to_cisu_risk_threat_mapped_code_replaces_matching_item_only():
153+
unmapped = {"code": "R99", "label": "Autre menace"}
154+
qualification = rs_to_cisu_message(
155+
{
156+
"riskThreat": [
157+
{"code": "R17", "label": "Situation instable"},
158+
unmapped,
159+
]
160+
}
161+
)
162+
assert qualification["riskThreat"] == [
163+
{"code": "R37", "label": "Situation instable/indéterminée"},
164+
unmapped,
165+
]

converter/tests/nomenclatures/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)