tricc-oo 1.6.11__tar.gz → 1.6.13__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tricc_oo-1.6.11/tricc_oo.egg-info → tricc_oo-1.6.13}/PKG-INFO +1 -1
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/pyproject.toml +1 -1
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/converters/codesystem_to_ocl.py +4 -4
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/converters/datadictionnary.py +1 -1
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/converters/drawio_type_map.py +11 -11
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/converters/xml_to_tricc.py +30 -15
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/serializers/xls_form.py +14 -2
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/strategies/output/dhis2_form.py +15 -211
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/strategies/output/fhir_form.py +1 -1
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/strategies/output/xlsform_cht.py +7 -19
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/visitors/tricc.py +29 -18
- {tricc_oo-1.6.11 → tricc_oo-1.6.13/tricc_oo.egg-info}/PKG-INFO +1 -1
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo.egg-info/SOURCES.txt +0 -1
- tricc_oo-1.6.11/tests/test_build.py +0 -260
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/LICENSE +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/README.md +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/setup.cfg +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tests/build.py +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tests/test_cql.py +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tests/to_ocl.py +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/__init__.py +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/converters/__init__.py +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/converters/cql/cqlLexer.py +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/converters/cql/cqlListener.py +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/converters/cql/cqlParser.py +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/converters/cql/cqlVisitor.py +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/converters/cql_to_operation.py +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/converters/tricc_to_xls_form.py +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/converters/utils.py +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/models/__init__.py +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/models/base.py +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/models/calculate.py +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/models/lang.py +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/models/ocl.py +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/models/ordered_set.py +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/models/tricc.py +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/parsers/__init__.py +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/parsers/xml.py +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/serializers/__init__.py +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/serializers/planuml.py +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/strategies/__init__.py +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/strategies/input/__init__.py +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/strategies/input/base_input_strategy.py +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/strategies/input/drawio.py +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/strategies/output/base_output_strategy.py +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/strategies/output/html_form.py +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/strategies/output/openmrs_form.py +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/strategies/output/spice.py +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/strategies/output/xls_form.py +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/strategies/output/xlsform_cdss.py +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/strategies/output/xlsform_cht_hf.py +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/visitors/__init__.py +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/visitors/utils.py +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo/visitors/xform_pd.py +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo.egg-info/dependency_links.txt +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo.egg-info/requires.txt +0 -0
- {tricc_oo-1.6.11 → tricc_oo-1.6.13}/tricc_oo.egg-info/top_level.txt +0 -0
|
@@ -31,16 +31,16 @@ def extract_properties_metadata(fhir_cs: CodeSystem) -> Dict[str, Dict]:
|
|
|
31
31
|
|
|
32
32
|
property_types[prop.code] = {
|
|
33
33
|
"name": prop.code,
|
|
34
|
-
"
|
|
34
|
+
"dataType": ocl_type,
|
|
35
35
|
"description": prop.description if hasattr(prop, "description") else "",
|
|
36
36
|
}
|
|
37
37
|
return property_types
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
def get_fhir_concept_datatype(concept):
|
|
41
|
-
datatype = extract_concept_properties(concept, ["
|
|
41
|
+
datatype = extract_concept_properties(concept, ["dataType"])
|
|
42
42
|
if datatype:
|
|
43
|
-
return datatype["
|
|
43
|
+
return datatype["dataType"]
|
|
44
44
|
else:
|
|
45
45
|
return OclConstants.DATA_TYPE_NONE
|
|
46
46
|
|
|
@@ -84,7 +84,7 @@ def get_attributes_from_concept_properties(concept, property_types: Dict) -> Lis
|
|
|
84
84
|
"type": "Attribute",
|
|
85
85
|
"attribute_type": code,
|
|
86
86
|
"value": value,
|
|
87
|
-
"value_type": property_types[code]["
|
|
87
|
+
"value_type": property_types[code]["dataType"],
|
|
88
88
|
}
|
|
89
89
|
)
|
|
90
90
|
return attributes
|
|
@@ -103,7 +103,7 @@ def check_and_add_concept(code_system: CodeSystem, code: str, display: str, attr
|
|
|
103
103
|
# TODO support other type of Codesystem Concept Property Value
|
|
104
104
|
existing_attributes
|
|
105
105
|
if p.valueString != v:
|
|
106
|
-
logger.warning(f"conflicting value for property {k}
|
|
106
|
+
logger.warning(f"conflicting value for concept `{concept.code}` property ` {k}`: {p.valueString} != {v}")
|
|
107
107
|
if not existing_attributes:
|
|
108
108
|
new_concept.property.append(CodeSystemConceptProperty(code=k, valueString=v))
|
|
109
109
|
|
|
@@ -47,7 +47,7 @@ TYPE_MAP = {
|
|
|
47
47
|
},
|
|
48
48
|
TriccNodeType.note: {
|
|
49
49
|
"objects": ["UserObject", "object"],
|
|
50
|
-
"attributes": ["relevance", "priority", "
|
|
50
|
+
"attributes": ["relevance", "priority", "concept_type"],
|
|
51
51
|
"mandatory_attributes": ["label", "name"],
|
|
52
52
|
"model": TriccNodeNote,
|
|
53
53
|
},
|
|
@@ -76,7 +76,7 @@ TYPE_MAP = {
|
|
|
76
76
|
"priority",
|
|
77
77
|
"trigger",
|
|
78
78
|
"default",
|
|
79
|
-
"
|
|
79
|
+
"concept_type",
|
|
80
80
|
],
|
|
81
81
|
"mandatory_attributes": ["label", "name", "list_name"],
|
|
82
82
|
"model": TriccNodeSelectOne,
|
|
@@ -94,7 +94,7 @@ TYPE_MAP = {
|
|
|
94
94
|
"priority",
|
|
95
95
|
"trigger",
|
|
96
96
|
"default",
|
|
97
|
-
"
|
|
97
|
+
"concept_type",
|
|
98
98
|
],
|
|
99
99
|
"mandatory_attributes": ["label", "name", "list_name"],
|
|
100
100
|
"model": TriccNodeSelectMultiple,
|
|
@@ -112,7 +112,7 @@ TYPE_MAP = {
|
|
|
112
112
|
"priority",
|
|
113
113
|
"trigger",
|
|
114
114
|
"default",
|
|
115
|
-
"
|
|
115
|
+
"concept_type",
|
|
116
116
|
],
|
|
117
117
|
"mandatory_attributes": ["label", "name"],
|
|
118
118
|
"model": TriccNodeDecimal,
|
|
@@ -130,7 +130,7 @@ TYPE_MAP = {
|
|
|
130
130
|
"priority",
|
|
131
131
|
"trigger",
|
|
132
132
|
"default",
|
|
133
|
-
"
|
|
133
|
+
"concept_type",
|
|
134
134
|
],
|
|
135
135
|
"mandatory_attributes": ["label", "name"],
|
|
136
136
|
"model": TriccNodeInteger,
|
|
@@ -145,7 +145,7 @@ TYPE_MAP = {
|
|
|
145
145
|
"default",
|
|
146
146
|
"constraint",
|
|
147
147
|
"constraint_message",
|
|
148
|
-
"
|
|
148
|
+
"concept_type",
|
|
149
149
|
],
|
|
150
150
|
"mandatory_attributes": ["label", "name"],
|
|
151
151
|
"model": TriccNodeText,
|
|
@@ -160,7 +160,7 @@ TYPE_MAP = {
|
|
|
160
160
|
"default",
|
|
161
161
|
"constraint",
|
|
162
162
|
"constraint_message",
|
|
163
|
-
"
|
|
163
|
+
"concept_type",
|
|
164
164
|
],
|
|
165
165
|
"mandatory_attributes": ["label", "name"],
|
|
166
166
|
"model": TriccNodeDate,
|
|
@@ -183,7 +183,7 @@ TYPE_MAP = {
|
|
|
183
183
|
"save",
|
|
184
184
|
"reference",
|
|
185
185
|
"trigger",
|
|
186
|
-
"
|
|
186
|
+
"concept_type",
|
|
187
187
|
"remote_reference",
|
|
188
188
|
],
|
|
189
189
|
"mandatory_attributes": ["name", "label"],
|
|
@@ -217,7 +217,7 @@ TYPE_MAP = {
|
|
|
217
217
|
},
|
|
218
218
|
TriccNodeType.not_available: {
|
|
219
219
|
"objects": ["UserObject", "object"],
|
|
220
|
-
"attributes": ["
|
|
220
|
+
"attributes": ["concept_type"],
|
|
221
221
|
"mandatory_attributes": ["label", "name", "list_name"],
|
|
222
222
|
"model": TriccNodeSelectNotAvailable,
|
|
223
223
|
},
|
|
@@ -233,7 +233,7 @@ TYPE_MAP = {
|
|
|
233
233
|
"priority",
|
|
234
234
|
"trigger",
|
|
235
235
|
"default",
|
|
236
|
-
"
|
|
236
|
+
"concept_type",
|
|
237
237
|
],
|
|
238
238
|
"mandatory_attributes": ["label", "name", "list_name"],
|
|
239
239
|
"model": TriccNodeSelectYesNo,
|
|
@@ -295,7 +295,7 @@ TYPE_MAP = {
|
|
|
295
295
|
},
|
|
296
296
|
TriccNodeType.input: {
|
|
297
297
|
"objects": ["UserObject", "object"],
|
|
298
|
-
"attributes": ["save", "reference", "
|
|
298
|
+
"attributes": ["save", "reference", "data_type", "concept_type"],
|
|
299
299
|
"mandatory_attributes": ["name", "label"],
|
|
300
300
|
"model": TriccNodeInput,
|
|
301
301
|
},
|
|
@@ -128,7 +128,7 @@ def create_activity(diagram, media_path, project):
|
|
|
128
128
|
for n in nodes.values():
|
|
129
129
|
|
|
130
130
|
if (
|
|
131
|
-
issubclass(n.__class__, (TriccNodeDisplayModel, TriccNodeDisplayCalculateBase))
|
|
131
|
+
issubclass(n.__class__, (TriccNodeDisplayModel, TriccNodeDisplayCalculateBase, TriccNodeInput))
|
|
132
132
|
and not isinstance(n, (TriccRhombusMixIn, TriccNodeRhombus, TriccNodeDisplayBridge))
|
|
133
133
|
and not n.name.startswith("label_") # FIXME
|
|
134
134
|
):
|
|
@@ -139,7 +139,7 @@ def create_activity(diagram, media_path, project):
|
|
|
139
139
|
system,
|
|
140
140
|
n.select.name,
|
|
141
141
|
n.label,
|
|
142
|
-
{"
|
|
142
|
+
{"dataType": "Boolean", "conceptType": get_concept_type(n)},
|
|
143
143
|
)
|
|
144
144
|
elif not isinstance(n, TriccNodeSelectNotAvailable):
|
|
145
145
|
add_concept(
|
|
@@ -148,8 +148,20 @@ def create_activity(diagram, media_path, project):
|
|
|
148
148
|
n.name,
|
|
149
149
|
n.label,
|
|
150
150
|
{
|
|
151
|
-
"
|
|
152
|
-
"
|
|
151
|
+
"dataType": get_data_type(n.tricc_type),
|
|
152
|
+
"conceptType": get_concept_type(n),
|
|
153
|
+
},
|
|
154
|
+
)
|
|
155
|
+
elif not issubclass(n.__class__, TriccNodeCalculate):
|
|
156
|
+
system = n.name.split(".")[0] if "." in n.name else "calculate"
|
|
157
|
+
add_concept(
|
|
158
|
+
project.code_systems,
|
|
159
|
+
system,
|
|
160
|
+
n.name,
|
|
161
|
+
n.label,
|
|
162
|
+
{
|
|
163
|
+
"dataType": get_data_type(n.tricc_type),
|
|
164
|
+
"conceptType": get_concept_type(n),
|
|
153
165
|
},
|
|
154
166
|
)
|
|
155
167
|
if getattr(n, "save", None):
|
|
@@ -160,8 +172,8 @@ def create_activity(diagram, media_path, project):
|
|
|
160
172
|
n.save,
|
|
161
173
|
n.label,
|
|
162
174
|
{
|
|
163
|
-
"
|
|
164
|
-
"
|
|
175
|
+
"dataType": get_data_type(n.tricc_type),
|
|
176
|
+
"conceptType": get_concept_type(n),
|
|
165
177
|
},
|
|
166
178
|
)
|
|
167
179
|
|
|
@@ -175,7 +187,7 @@ def create_activity(diagram, media_path, project):
|
|
|
175
187
|
# link back the activity
|
|
176
188
|
activity.root.activity = activity
|
|
177
189
|
manage_dangling_calculate(activity)
|
|
178
|
-
|
|
190
|
+
# assign the process
|
|
179
191
|
if activity is not None:
|
|
180
192
|
if activity.root is not None:
|
|
181
193
|
project.pages[activity.id] = activity
|
|
@@ -197,6 +209,7 @@ def create_activity(diagram, media_path, project):
|
|
|
197
209
|
)
|
|
198
210
|
if images:
|
|
199
211
|
project.images += images
|
|
212
|
+
# Assign parent to NotAvailable
|
|
200
213
|
for node in list(
|
|
201
214
|
filter(
|
|
202
215
|
lambda p_node: isinstance(p_node, TriccNodeSelectNotAvailable),
|
|
@@ -477,10 +490,10 @@ def set_additional_attributes(attribute_names, elm, node):
|
|
|
477
490
|
setattr(node, attributename, attribute)
|
|
478
491
|
|
|
479
492
|
|
|
480
|
-
def
|
|
481
|
-
|
|
482
|
-
if
|
|
483
|
-
return
|
|
493
|
+
def get_concept_type(node):
|
|
494
|
+
concept_type = getattr(node, "concept_type", None)
|
|
495
|
+
if concept_type:
|
|
496
|
+
return concept_type
|
|
484
497
|
if isinstance(node, TriccNodeSelectMultiple):
|
|
485
498
|
return "Question"
|
|
486
499
|
elif isinstance(node, TriccNodeSelectOption):
|
|
@@ -535,7 +548,7 @@ def get_select_options(diagram, select_node, nodes):
|
|
|
535
548
|
activity=select_node.activity,
|
|
536
549
|
group=select_node.group,
|
|
537
550
|
)
|
|
538
|
-
set_additional_attributes(["save", "relevance", "
|
|
551
|
+
set_additional_attributes(["save", "relevance", "concept_type"], elm, option)
|
|
539
552
|
load_expressions(option)
|
|
540
553
|
options[i] = option
|
|
541
554
|
nodes[id] = option
|
|
@@ -802,9 +815,11 @@ def set_mandatory_attribute(elm, mandatory_attributes, diagram=None):
|
|
|
802
815
|
id = elm.attrib.get("id")
|
|
803
816
|
attribute_value = _get_name(name, id, diagram_id)
|
|
804
817
|
elif attributes == "list_name":
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
818
|
+
attribute_value = elm.attrib.get("list_name", None)
|
|
819
|
+
if not attribute_value:
|
|
820
|
+
name = elm.attrib.get("name")
|
|
821
|
+
id = elm.attrib.get("id")
|
|
822
|
+
attribute_value = TRICC_LIST_NAME.format(clean_str(_get_name(name, id, diagram_id), replace_dots=True))
|
|
808
823
|
else:
|
|
809
824
|
attribute_value = elm.attrib.get(attributes)
|
|
810
825
|
if attribute_value is None:
|
|
@@ -32,6 +32,7 @@ from tricc_oo.visitors.tricc import (
|
|
|
32
32
|
get_applicability_expression,
|
|
33
33
|
get_prev_instance_skip_expression,
|
|
34
34
|
get_process_skip_expression,
|
|
35
|
+
process_operation_reference,
|
|
35
36
|
)
|
|
36
37
|
|
|
37
38
|
logger = logging.getLogger("default")
|
|
@@ -73,7 +74,7 @@ def start_group(
|
|
|
73
74
|
group=cur_group.group,
|
|
74
75
|
activity=cur_group.activity,
|
|
75
76
|
name=get_export_group_name(name),
|
|
76
|
-
expression=cur_group.relevance
|
|
77
|
+
expression=cur_group.relevance
|
|
77
78
|
)
|
|
78
79
|
|
|
79
80
|
if calc not in cur_group.activity.calculates:
|
|
@@ -103,7 +104,18 @@ def start_group(
|
|
|
103
104
|
if not relevance:
|
|
104
105
|
relevance_expression_str = ""
|
|
105
106
|
elif isinstance(relevance_expression, (TriccOperation, TriccStatic)):
|
|
106
|
-
|
|
107
|
+
relevance_expression = process_operation_reference(
|
|
108
|
+
relevance_expression,
|
|
109
|
+
cur_group,
|
|
110
|
+
processed_nodes=processed_nodes,
|
|
111
|
+
calculates=kwargs.get('calculates', None),
|
|
112
|
+
used_calculates=kwargs.get('used_calculates', None),
|
|
113
|
+
replace_reference=True,
|
|
114
|
+
warn=False,
|
|
115
|
+
codesystems=kwargs.get('codesystems', None),
|
|
116
|
+
) or relevance_expression
|
|
117
|
+
if relevance_expression:
|
|
118
|
+
relevance_expression_str = strategy.get_tricc_operation_expression(relevance_expression)
|
|
107
119
|
|
|
108
120
|
# group
|
|
109
121
|
values = []
|
|
@@ -16,7 +16,7 @@ import datetime
|
|
|
16
16
|
from tricc_oo.strategies.output.base_output_strategy import BaseOutPutStrategy
|
|
17
17
|
from tricc_oo.models.base import (
|
|
18
18
|
not_clean, TriccOperation,
|
|
19
|
-
TriccStatic, TriccReference
|
|
19
|
+
TriccStatic, TriccReference
|
|
20
20
|
)
|
|
21
21
|
from tricc_oo.models.tricc import (
|
|
22
22
|
TriccNodeSelectOption,
|
|
@@ -27,8 +27,6 @@ from tricc_oo.models.tricc import (
|
|
|
27
27
|
TriccNodeActivity,
|
|
28
28
|
TriccNodeSelect,
|
|
29
29
|
TriccNodeSelectYesNo,
|
|
30
|
-
TriccNodeNote,
|
|
31
|
-
TriccNodeMoreInfo,
|
|
32
30
|
)
|
|
33
31
|
from tricc_oo.models.calculate import TriccNodeDisplayCalculateBase
|
|
34
32
|
from tricc_oo.models.ordered_set import OrderedSet
|
|
@@ -46,7 +44,7 @@ class DHIS2Strategy(BaseOutPutStrategy):
|
|
|
46
44
|
|
|
47
45
|
def __init__(self, project, output_path):
|
|
48
46
|
super().__init__(project, output_path)
|
|
49
|
-
form_id = getattr(self.project.start_pages["main"]
|
|
47
|
+
form_id = getattr(self.project.start_pages["main"], 'form_id', 'dhis2_program')
|
|
50
48
|
self.program_metadata = {
|
|
51
49
|
"id": self.generate_id(form_id),
|
|
52
50
|
"name": form_id,
|
|
@@ -206,17 +204,13 @@ class DHIS2Strategy(BaseOutPutStrategy):
|
|
|
206
204
|
return False
|
|
207
205
|
|
|
208
206
|
if node not in processed_nodes:
|
|
209
|
-
# Skip relevance generation for TriccNodeMoreInfo as they don't create DataElements
|
|
210
|
-
if isinstance(node, TriccNodeMoreInfo):
|
|
211
|
-
return True
|
|
212
|
-
|
|
213
207
|
relevance = None
|
|
214
208
|
if hasattr(node, 'relevance') and node.relevance:
|
|
215
209
|
relevance = node.relevance
|
|
216
210
|
if hasattr(node, 'expression') and node.expression:
|
|
217
211
|
relevance = node.expression
|
|
218
212
|
if relevance:
|
|
219
|
-
relevance_str = self.convert_expression_to_string(relevance)
|
|
213
|
+
relevance_str = self.convert_expression_to_string(not_clean(relevance))
|
|
220
214
|
if relevance_str and relevance_str != 'false':
|
|
221
215
|
# Create program rule action for hiding/showing based on relevance
|
|
222
216
|
rule_id = self.generate_id(f"rule_{node.get_name()}_relevance")
|
|
@@ -231,10 +225,8 @@ class DHIS2Strategy(BaseOutPutStrategy):
|
|
|
231
225
|
"activity_ref": node, # Temporary reference to be replaced with section ID
|
|
232
226
|
"programRule": {"id": rule_id},
|
|
233
227
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
# For regular nodes that get DataElements, use HIDEFIELD action
|
|
237
|
-
# Exclude TriccNodeNote and TriccNodeMoreInfo as they don't get DataElements
|
|
228
|
+
else:
|
|
229
|
+
# For regular nodes, use HIDEFIELD action
|
|
238
230
|
program_rule_action = {
|
|
239
231
|
"id": action_id,
|
|
240
232
|
"programRuleActionType": "HIDEFIELD",
|
|
@@ -243,45 +235,18 @@ class DHIS2Strategy(BaseOutPutStrategy):
|
|
|
243
235
|
},
|
|
244
236
|
"programRule": {"id": rule_id}
|
|
245
237
|
}
|
|
246
|
-
|
|
238
|
+
self.program_rule_actions.append(program_rule_action)
|
|
247
239
|
|
|
248
240
|
# Create program rule referencing the action
|
|
249
|
-
condition = self.simplify_expression(f"({relevance_str})
|
|
241
|
+
condition = self.simplify_expression(f"!({relevance_str})") # Negate for hide when true
|
|
250
242
|
condition = self.simplify_expression(condition)
|
|
251
243
|
self.program_rules.append({
|
|
252
244
|
"id": rule_id,
|
|
253
245
|
"name": f"Hide `{self.get_export_name(node)}` when condition met",
|
|
254
|
-
"description": f"Hide `{self.get_display(node)
|
|
246
|
+
"description": f"Hide `{self.get_display(node)}` based on relevance",
|
|
255
247
|
"condition": condition,
|
|
256
248
|
"programRuleActions": [{"id": action_id}]
|
|
257
249
|
})
|
|
258
|
-
|
|
259
|
-
# Check if field should be mandatory based on 'required' attribute
|
|
260
|
-
if bool(getattr(node, 'required', False)):
|
|
261
|
-
# Create program rule for mandatory field using relevance function
|
|
262
|
-
mandatory_rule_id = self.generate_id(f"rule_{node.get_name()}_mandatory")
|
|
263
|
-
mandatory_action_id = self.generate_id(f"action_{mandatory_rule_id}")
|
|
264
|
-
|
|
265
|
-
mandatory_program_rule_action = {
|
|
266
|
-
"id": mandatory_action_id,
|
|
267
|
-
"programRuleActionType": "SETMANDATORYFIELD",
|
|
268
|
-
"dataElement": {
|
|
269
|
-
"id": self.generate_id(self.get_export_name(node))
|
|
270
|
-
},
|
|
271
|
-
"programRule": {"id": mandatory_rule_id}
|
|
272
|
-
}
|
|
273
|
-
self.program_rule_actions.append(mandatory_program_rule_action)
|
|
274
|
-
|
|
275
|
-
# Create program rule for mandatory field - use relevance condition if available
|
|
276
|
-
mandatory_condition = relevance_str if relevance_str and relevance_str != 'false' else "true"
|
|
277
|
-
self.program_rules.append({
|
|
278
|
-
"id": mandatory_rule_id,
|
|
279
|
-
"name": f"Make `{self.get_export_name(node)}` mandatory",
|
|
280
|
-
"description": f"Set `{self.get_display(node)[:128]}` as mandatory field",
|
|
281
|
-
"condition": mandatory_condition,
|
|
282
|
-
"programRuleActions": [{"id": mandatory_action_id}]
|
|
283
|
-
})
|
|
284
|
-
|
|
285
250
|
return True
|
|
286
251
|
|
|
287
252
|
def generate_data_element(self, node):
|
|
@@ -309,8 +274,8 @@ class DHIS2Strategy(BaseOutPutStrategy):
|
|
|
309
274
|
"id": de_id,
|
|
310
275
|
"name": self.get_export_name(node),
|
|
311
276
|
"shortName": node.name[:50],
|
|
312
|
-
"displayFormName":
|
|
313
|
-
"formName":
|
|
277
|
+
"displayFormName": self.get_display(node),
|
|
278
|
+
"formName": self.get_display(node),
|
|
314
279
|
"valueType": value_type,
|
|
315
280
|
"domainType": "TRACKER",
|
|
316
281
|
"aggregationType": "NONE"
|
|
@@ -547,36 +512,8 @@ class DHIS2Strategy(BaseOutPutStrategy):
|
|
|
547
512
|
return False
|
|
548
513
|
|
|
549
514
|
if node not in processed_nodes:
|
|
550
|
-
logger.debug(f"generate_export processing node: {node.get_name()} type: {type(node)}")
|
|
551
|
-
# Special handling for TriccNodeNote - transform into section description
|
|
552
|
-
if isinstance(node, TriccNodeNote):
|
|
553
|
-
logger.info(f"Found TriccNodeNote: {node.get_name()}")
|
|
554
|
-
self.handle_note_as_section_description(node, processed_nodes, **kwargs)
|
|
555
|
-
elif isinstance(node, TriccNodeMoreInfo):
|
|
556
|
-
logger.info(f"Found TriccNodeMoreInfo: {node.get_name()}")
|
|
557
|
-
# Create boolean checkbox data element for moreinfo
|
|
558
|
-
data_element = self.generate_data_element(node)
|
|
559
|
-
if data_element:
|
|
560
|
-
# Add to program stage
|
|
561
|
-
if self.program_metadata["programStages"]:
|
|
562
|
-
psde_id = self.generate_id(f"psde_{node.name}")
|
|
563
|
-
psde = {
|
|
564
|
-
"id": psde_id,
|
|
565
|
-
"dataElement": {"id": data_element["id"]},
|
|
566
|
-
"compulsory": False
|
|
567
|
-
}
|
|
568
|
-
self.program_metadata["programStages"][-1]["programStageDataElements"].append(psde)
|
|
569
|
-
|
|
570
|
-
# Add data element to current section
|
|
571
|
-
if self.current_section and self.current_section in self.sections:
|
|
572
|
-
self.sections[self.current_section]["dataElements"].append({"id": data_element["id"]})
|
|
573
|
-
|
|
574
|
-
# Handle the note section with checkbox conditioning
|
|
575
|
-
checkbox_reference = TriccReference(node.name)
|
|
576
|
-
checkbox_condition = TriccOperation(TriccOperator.ISTRUE, [checkbox_reference])
|
|
577
|
-
self.handle_note_as_section_description(node, processed_nodes, additional_condition=checkbox_condition, **kwargs)
|
|
578
515
|
# Skip creating data elements for calculate nodes - they should only be program rule variables
|
|
579
|
-
|
|
516
|
+
if not issubclass(node.__class__, TriccNodeCalculateBase):
|
|
580
517
|
data_element = self.generate_data_element(node)
|
|
581
518
|
if data_element:
|
|
582
519
|
# Add to program stage
|
|
@@ -585,7 +522,7 @@ class DHIS2Strategy(BaseOutPutStrategy):
|
|
|
585
522
|
psde = {
|
|
586
523
|
"id": psde_id,
|
|
587
524
|
"dataElement": {"id": data_element["id"]},
|
|
588
|
-
"compulsory": False
|
|
525
|
+
"compulsory": bool(getattr(node, 'required', False))
|
|
589
526
|
}
|
|
590
527
|
self.program_metadata["programStages"][-1]["programStageDataElements"].append(psde)
|
|
591
528
|
|
|
@@ -595,136 +532,6 @@ class DHIS2Strategy(BaseOutPutStrategy):
|
|
|
595
532
|
|
|
596
533
|
return True
|
|
597
534
|
|
|
598
|
-
def handle_note_as_section_description(self, node, processed_nodes, additional_condition=None, **kwargs):
|
|
599
|
-
"""Transform TriccNodeNote into section description and create hide logic with section duplication
|
|
600
|
-
|
|
601
|
-
Args:
|
|
602
|
-
node: The node to process (TriccNodeNote or TriccNodeMoreInfo)
|
|
603
|
-
processed_nodes: Set of already processed nodes
|
|
604
|
-
additional_condition: Optional additional condition to combine with existing relevance
|
|
605
|
-
"""
|
|
606
|
-
logger.info(f"Processing note {node.get_name()} for section description")
|
|
607
|
-
if not self.current_section or self.current_section not in self.sections:
|
|
608
|
-
logger.warning(f"No current section found for note {node.get_name()}")
|
|
609
|
-
return
|
|
610
|
-
|
|
611
|
-
# 1. Duplicate the previous section with incremented ID (like XLS form group numbering)
|
|
612
|
-
original_section = self.sections[self.current_section]
|
|
613
|
-
section_name = f"{original_section['name']}_{node.name}"
|
|
614
|
-
duplicated_section_id = self.generate_id(section_name)
|
|
615
|
-
duplicated_section = {
|
|
616
|
-
"id": duplicated_section_id,
|
|
617
|
-
"name": section_name, # Use incremented naming pattern
|
|
618
|
-
"sortOrder": len(self.sections),
|
|
619
|
-
"programStage": {"id": self.program_metadata["programStages"][-1]["id"]},
|
|
620
|
-
"dataElements": original_section.get("dataElements", []).copy(), # Copy data elements
|
|
621
|
-
"activity_ref": original_section.get("activity_ref")
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
# 2. Create a new section for the note/moreinfo with name and ID from the node
|
|
625
|
-
note_section_id = self.generate_id(self.get_export_name(node))
|
|
626
|
-
note_section_name = self.get_export_name(node)
|
|
627
|
-
|
|
628
|
-
note_section = {
|
|
629
|
-
"id": note_section_id,
|
|
630
|
-
"name": note_section_name,
|
|
631
|
-
"sortOrder": len(self.sections),
|
|
632
|
-
"programStage": {"id": self.program_metadata["programStages"][-1]["id"]},
|
|
633
|
-
"dataElements": [],
|
|
634
|
-
"activity_ref": original_section.get("activity_ref") # Same activity reference
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
# Set section description to the note's label
|
|
638
|
-
if hasattr(node, 'label') and node.label:
|
|
639
|
-
note_section["description"] = node.label.replace('\u00a0', ' ').strip()
|
|
640
|
-
logger.info(f"Set note section {note_section_id} description to: {note_section['description']}")
|
|
641
|
-
elif hasattr(node, 'name') and node.name:
|
|
642
|
-
note_section["description"] = node.name.replace('\u00a0', ' ').strip()
|
|
643
|
-
logger.info(f"Set note section {note_section_id} description to: {note_section['description']}")
|
|
644
|
-
else:
|
|
645
|
-
note_section["description"] = str(node.id)
|
|
646
|
-
logger.info(f"Set note section {note_section_id} description to: {note_section['description']}")
|
|
647
|
-
|
|
648
|
-
self.sections[note_section_id] = note_section
|
|
649
|
-
|
|
650
|
-
# Add note section to program stage
|
|
651
|
-
if self.program_metadata["programStages"]:
|
|
652
|
-
self.program_metadata["programStages"][-1]["programStageSections"].append({"id": note_section_id})
|
|
653
|
-
|
|
654
|
-
logger.info(f"Created note section {note_section_id} with name '{note_section_name}'")
|
|
655
|
-
|
|
656
|
-
# 3. Inject the duplicated section as the new current section
|
|
657
|
-
self.sections[duplicated_section_id] = duplicated_section
|
|
658
|
-
|
|
659
|
-
# Add duplicated section to program stage
|
|
660
|
-
if self.program_metadata["programStages"]:
|
|
661
|
-
self.program_metadata["programStages"][-1]["programStageSections"].append({"id": duplicated_section_id})
|
|
662
|
-
|
|
663
|
-
logger.info(f"Duplicated section {self.current_section} as {duplicated_section_id}")
|
|
664
|
-
|
|
665
|
-
self.current_section = duplicated_section_id
|
|
666
|
-
logger.info(f"Set current section to duplicated section {duplicated_section_id}")
|
|
667
|
-
|
|
668
|
-
# Create hide logic for the note section based on combined relevance
|
|
669
|
-
combined_relevance = None
|
|
670
|
-
|
|
671
|
-
# Get parent activity relevance from the original section's activity_ref
|
|
672
|
-
parent_activity = original_section.get("activity_ref")
|
|
673
|
-
if parent_activity and hasattr(parent_activity, 'relevance') and parent_activity.relevance:
|
|
674
|
-
combined_relevance = parent_activity.relevance
|
|
675
|
-
|
|
676
|
-
# Combine with note relevance if it exists
|
|
677
|
-
if hasattr(node, 'relevance') and node.relevance:
|
|
678
|
-
if combined_relevance:
|
|
679
|
-
# Combine using AND operation
|
|
680
|
-
combined_relevance = TriccOperation(
|
|
681
|
-
TriccOperator.AND,
|
|
682
|
-
[combined_relevance, node.relevance]
|
|
683
|
-
)
|
|
684
|
-
else:
|
|
685
|
-
combined_relevance = node.relevance
|
|
686
|
-
|
|
687
|
-
# Combine with additional condition if provided
|
|
688
|
-
if additional_condition:
|
|
689
|
-
if combined_relevance:
|
|
690
|
-
# Combine using AND operation
|
|
691
|
-
combined_relevance = TriccOperation(
|
|
692
|
-
TriccOperator.AND,
|
|
693
|
-
[combined_relevance, additional_condition]
|
|
694
|
-
)
|
|
695
|
-
else:
|
|
696
|
-
combined_relevance = additional_condition
|
|
697
|
-
|
|
698
|
-
# Create hide logic if there's relevance - applied to the note section
|
|
699
|
-
if combined_relevance:
|
|
700
|
-
relevance_str = self.convert_expression_to_string(not_clean(combined_relevance))
|
|
701
|
-
if relevance_str and relevance_str != 'false':
|
|
702
|
-
# Create program rule action for hiding the note section
|
|
703
|
-
rule_id = self.generate_id(f"rule_{node.get_name()}_note_hide_section")
|
|
704
|
-
action_id = self.generate_id(f"action_{rule_id}")
|
|
705
|
-
|
|
706
|
-
program_rule_action = {
|
|
707
|
-
"id": action_id,
|
|
708
|
-
"programRuleActionType": "HIDESECTION",
|
|
709
|
-
"activity_ref": parent_activity, # Use activity reference like other HIDESECTION actions
|
|
710
|
-
"programRule": {"id": rule_id},
|
|
711
|
-
}
|
|
712
|
-
self.program_rule_actions.append(program_rule_action)
|
|
713
|
-
|
|
714
|
-
# Create program rule referencing the action
|
|
715
|
-
condition = self.simplify_expression(f"({relevance_str})==false") # Negate for hide when true
|
|
716
|
-
condition = self.simplify_expression(condition)
|
|
717
|
-
rule_name = f"Hide note section `{note_section_name}` based on relevance"
|
|
718
|
-
if additional_condition:
|
|
719
|
-
rule_name = f"Hide moreinfo section `{note_section_name}` when checkbox false"
|
|
720
|
-
self.program_rules.append({
|
|
721
|
-
"id": rule_id,
|
|
722
|
-
"name": rule_name,
|
|
723
|
-
"description": f"Hide note section `{self.get_display(node)[:128]}` based on combined relevance",
|
|
724
|
-
"condition": condition,
|
|
725
|
-
"programRuleActions": [{"id": action_id}]
|
|
726
|
-
})
|
|
727
|
-
|
|
728
535
|
def clean_section(self, program_stages_payload):
|
|
729
536
|
"""Clean sections by removing empty ones and merging sections with same activity_ref"""
|
|
730
537
|
sections_to_remove = set()
|
|
@@ -735,11 +542,11 @@ class DHIS2Strategy(BaseOutPutStrategy):
|
|
|
735
542
|
section_id = section["id"]
|
|
736
543
|
activity_ref = section.get("activity_ref")
|
|
737
544
|
# Remove empty sections
|
|
738
|
-
if not section.get("dataElements")
|
|
545
|
+
if not section.get("dataElements"):
|
|
739
546
|
sections_to_remove.add(section_id)
|
|
740
547
|
|
|
741
548
|
# Check for sections with same activity_ref
|
|
742
|
-
elif activity_ref == prev_activity_ref
|
|
549
|
+
elif activity_ref == prev_activity_ref:
|
|
743
550
|
# Merge this section into the existing one
|
|
744
551
|
existing_section = self.sections[prev_section_id]
|
|
745
552
|
|
|
@@ -838,8 +645,6 @@ class DHIS2Strategy(BaseOutPutStrategy):
|
|
|
838
645
|
# Non-activity actions (HIDEFIELD) can be added directly
|
|
839
646
|
program_rule_actions_payload.append(action)
|
|
840
647
|
|
|
841
|
-
# Filter out rules that reference non-existent actions
|
|
842
|
-
valid_action_ids = {action["id"] for action in program_rule_actions_payload}
|
|
843
648
|
if self.program_rules:
|
|
844
649
|
program_rules_payload = [
|
|
845
650
|
{
|
|
@@ -847,7 +652,6 @@ class DHIS2Strategy(BaseOutPutStrategy):
|
|
|
847
652
|
"program": {"id": self.program_metadata["id"]}
|
|
848
653
|
}
|
|
849
654
|
for rule in self.program_rules
|
|
850
|
-
if all(action_ref["id"] in valid_action_ids for action_ref in rule["programRuleActions"])
|
|
851
655
|
]
|
|
852
656
|
|
|
853
657
|
if self.program_rule_variables:
|
|
@@ -906,7 +710,7 @@ class DHIS2Strategy(BaseOutPutStrategy):
|
|
|
906
710
|
return self.get_tricc_operation_expression(r)
|
|
907
711
|
elif isinstance(r, TriccReference):
|
|
908
712
|
# Use variable name from concept_map
|
|
909
|
-
node_id = self.concept_map.get(r.value, self.get_export_name(r.value))
|
|
713
|
+
node_id = self.concept_map.get(r.value.name, self.get_export_name(r.value))
|
|
910
714
|
return f"#{{{node_id}}}"
|
|
911
715
|
elif isinstance(r, TriccStatic):
|
|
912
716
|
if isinstance(r.value, bool):
|
|
@@ -97,7 +97,7 @@ class FHIRStrategy(BaseOutPutStrategy):
|
|
|
97
97
|
"type": self.map_tricc_type_to_fhir(node.tricc_type if hasattr(node, 'tricc_type') else 'text')
|
|
98
98
|
}
|
|
99
99
|
if hasattr(node, 'options') and node.options:
|
|
100
|
-
item["answerOption"] = [{"valueString": opt.name} for opt in node.options
|
|
100
|
+
item["answerOption"] = [{"valueString": opt.name} for opt in node.options]
|
|
101
101
|
self.questionnaires[segment]["item"].append(item)
|
|
102
102
|
return True
|
|
103
103
|
|
|
@@ -821,34 +821,22 @@ class XLSFormCHTStrategy(XLSFormCDSSStrategy):
|
|
|
821
821
|
return jar_path
|
|
822
822
|
|
|
823
823
|
def _ensure_odk_validate_jar(self):
|
|
824
|
-
"""Ensure ODK Validate JAR is available by
|
|
824
|
+
"""Ensure ODK Validate JAR is available by downloading from GitHub releases."""
|
|
825
825
|
jar_path = os.path.join(os.path.dirname(__file__), "ODK_Validate.jar")
|
|
826
826
|
|
|
827
827
|
# Check if JAR already exists
|
|
828
828
|
if os.path.exists(jar_path):
|
|
829
829
|
return jar_path
|
|
830
830
|
|
|
831
|
-
#
|
|
832
|
-
|
|
833
|
-
if not os.path.exists(medic_zip_path):
|
|
834
|
-
logger.error("xls2xform-medic zip not found, cannot extract ODK Validate JAR")
|
|
835
|
-
return None
|
|
836
|
-
|
|
831
|
+
# Download JAR from GitHub releases
|
|
832
|
+
jar_url = "https://github.com/getodk/validate/releases/download/v1.20.0/ODK-Validate-v1.20.0.jar"
|
|
837
833
|
try:
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
zip_ref.extract(jar_in_zip, os.path.dirname(__file__))
|
|
842
|
-
|
|
843
|
-
# Move to final location
|
|
844
|
-
extracted_jar = os.path.join(os.path.dirname(__file__), jar_in_zip)
|
|
845
|
-
shutil.move(extracted_jar, jar_path)
|
|
846
|
-
|
|
847
|
-
logger.info(f"Extracted ODK Validate JAR to {jar_path}")
|
|
834
|
+
import urllib.request
|
|
835
|
+
urllib.request.urlretrieve(jar_url, jar_path)
|
|
836
|
+
logger.info(f"Downloaded ODK Validate JAR to {jar_path}")
|
|
848
837
|
return jar_path
|
|
849
|
-
|
|
850
838
|
except Exception as e:
|
|
851
|
-
logger.error(f"Failed to
|
|
839
|
+
logger.error(f"Failed to download ODK Validate JAR: {str(e)}")
|
|
852
840
|
return None
|
|
853
841
|
|
|
854
842
|
def tricc_operation_zscore(self, ref_expressions):
|
|
@@ -185,9 +185,9 @@ def get_version_inheritance(node, last_version, processed_nodes):
|
|
|
185
185
|
path_len=node.path_len + 1,
|
|
186
186
|
expression_reference=TriccOperation(
|
|
187
187
|
TriccOperator.COALESCE,
|
|
188
|
-
[
|
|
188
|
+
[node, last_version],
|
|
189
189
|
),
|
|
190
|
-
reference=[
|
|
190
|
+
reference=[node, last_version],
|
|
191
191
|
activity=node.activity,
|
|
192
192
|
group=node.group,
|
|
193
193
|
label=f"Save calculation for {node.label}",
|
|
@@ -805,7 +805,7 @@ def process_operation_reference(
|
|
|
805
805
|
operation,
|
|
806
806
|
node,
|
|
807
807
|
processed_nodes,
|
|
808
|
-
calculates,
|
|
808
|
+
calculates=None,
|
|
809
809
|
used_calculates=None,
|
|
810
810
|
replace_reference=False,
|
|
811
811
|
warn=False,
|
|
@@ -1740,16 +1740,17 @@ PARENT_GROUP_PRIORITY = 6000
|
|
|
1740
1740
|
ACTIVE_ACTIVITY_PRIORITY = 5000
|
|
1741
1741
|
NON_START_ACTIVITY_PRIORITY = 4000
|
|
1742
1742
|
ACTIVE_ACTIVITY_LOWER_PRIORITY = 3000
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
DEFAULT_PRIORITY = 2000
|
|
1746
|
-
|
|
1743
|
+
FLOW_CALCULATE_NODE_PRIORITY_TOP_UP = 50
|
|
1744
|
+
RHOMBUS_PRIORITY_TO_UP = 50
|
|
1747
1745
|
|
|
1746
|
+
|
|
1748
1747
|
def reorder_node_list(node_list, group, processed_nodes):
|
|
1749
1748
|
# Cache active activities for O(1) lookup
|
|
1750
1749
|
active_activities = {n.activity for n in processed_nodes}
|
|
1751
|
-
|
|
1750
|
+
MAP_PRIORITIES = {}
|
|
1752
1751
|
def get_priority(node):
|
|
1752
|
+
if node.id in MAP_PRIORITIES:
|
|
1753
|
+
return MAP_PRIORITIES[node.id]
|
|
1753
1754
|
# Cache attributes to avoid repeated getattr calls
|
|
1754
1755
|
priority = int(getattr(node, "priority", 0) or 0)
|
|
1755
1756
|
node_group = getattr(node, "group", None)
|
|
@@ -1757,12 +1758,7 @@ def reorder_node_list(node_list, group, processed_nodes):
|
|
|
1757
1758
|
|
|
1758
1759
|
# Check for same group
|
|
1759
1760
|
if group is not None and node_group and node_group.id == group.id:
|
|
1760
|
-
priority += SAME_GROUP_PRIORITY
|
|
1761
|
-
elif (
|
|
1762
|
-
issubclass(node.__class__, TriccNodeDisplayCalculateBase) or
|
|
1763
|
-
isinstance(node, TriccNodeEnd)
|
|
1764
|
-
) and not isinstance(node, TriccNodeActivityEnd) and hasattr(node, 'prev_nodes') and len(node.prev_nodes) > 0:
|
|
1765
|
-
priority += FLOW_CALCULATE_NODE_PRIORITY
|
|
1761
|
+
priority += SAME_GROUP_PRIORITY
|
|
1766
1762
|
# Check for parent group
|
|
1767
1763
|
elif hasattr(group, "group") and group.group and node_group and node_group.id == group.group.id:
|
|
1768
1764
|
priority += PARENT_GROUP_PRIORITY
|
|
@@ -1776,11 +1772,21 @@ def reorder_node_list(node_list, group, processed_nodes):
|
|
|
1776
1772
|
elif activity and activity in active_activities:
|
|
1777
1773
|
priority += ACTIVE_ACTIVITY_LOWER_PRIORITY
|
|
1778
1774
|
# Check for rhombus nodes
|
|
1775
|
+
|
|
1776
|
+
|
|
1777
|
+
if (
|
|
1778
|
+
issubclass(node.__class__, TriccNodeDisplayCalculateBase) or
|
|
1779
|
+
isinstance(node, TriccNodeEnd)
|
|
1780
|
+
) and not isinstance(node, TriccNodeActivityEnd) and hasattr(node, 'prev_nodes') and len(node.prev_nodes) > 0:
|
|
1781
|
+
priority += FLOW_CALCULATE_NODE_PRIORITY_TOP_UP
|
|
1779
1782
|
elif issubclass(node.__class__, TriccRhombusMixIn):
|
|
1780
|
-
priority +=
|
|
1781
|
-
else:
|
|
1782
|
-
priority += DEFAULT_PRIORITY
|
|
1783
|
+
priority += RHOMBUS_PRIORITY_TO_UP
|
|
1783
1784
|
|
|
1785
|
+
if node.prev_nodes:
|
|
1786
|
+
priority = max(priority, *[get_priority(p) for p in node.prev_nodes])
|
|
1787
|
+
|
|
1788
|
+
MAP_PRIORITIES[node.id] = priority
|
|
1789
|
+
|
|
1784
1790
|
return priority
|
|
1785
1791
|
|
|
1786
1792
|
# Sort in place, highest priority first
|
|
@@ -2258,7 +2264,12 @@ def get_none_option(node):
|
|
|
2258
2264
|
def get_count_terms_details(prev_node, processed_nodes, get_overall_exp, negate=False, process=None):
|
|
2259
2265
|
opt_none = get_none_option(prev_node)
|
|
2260
2266
|
if opt_none:
|
|
2261
|
-
|
|
2267
|
+
if isinstance(opt_none, str):
|
|
2268
|
+
operation_none = TriccOperation(TriccOperator.SELECTED, [prev_node, TriccStatic(opt_none)])
|
|
2269
|
+
elif issubclass(opt_none.__class__, TriccBaseModel):
|
|
2270
|
+
operation_none = TriccOperation(TriccOperator.SELECTED, [prev_node, opt_none])
|
|
2271
|
+
else:
|
|
2272
|
+
logger.critical(f"unexpected none option value {opt_none}")
|
|
2262
2273
|
else:
|
|
2263
2274
|
operation_none = TriccOperation(TriccOperator.SELECTED, [prev_node, TriccStatic("opt_none")])
|
|
2264
2275
|
if isinstance(prev_node, TriccNodeSelectYesNo):
|
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
import unittest
|
|
2
|
-
import subprocess
|
|
3
|
-
import sys
|
|
4
|
-
import os
|
|
5
|
-
import tempfile
|
|
6
|
-
import shutil
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
import pandas as pd
|
|
9
|
-
from pyxform import create_survey_from_xls
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class TestBuildScript(unittest.TestCase):
|
|
13
|
-
"""Test cases for the build.py script with different argument combinations."""
|
|
14
|
-
|
|
15
|
-
def setUp(self):
|
|
16
|
-
"""Set up test fixtures."""
|
|
17
|
-
self.test_data_dir = Path(__file__).parent / "data"
|
|
18
|
-
self.test_output_dir = Path(__file__).parent / "output"
|
|
19
|
-
self.demo_file = self.test_data_dir / "demo.drawio"
|
|
20
|
-
|
|
21
|
-
# Ensure test data exists
|
|
22
|
-
self.assertTrue(self.demo_file.exists(), f"Test data file {self.demo_file} does not exist")
|
|
23
|
-
|
|
24
|
-
def run_build_script(self, args):
|
|
25
|
-
"""Helper method to run build.py with given arguments."""
|
|
26
|
-
cmd = [sys.executable, str(Path(__file__).parent / "build.py")] + args
|
|
27
|
-
result = subprocess.run(cmd, capture_output=True, text=True, cwd=Path(__file__).parent.parent)
|
|
28
|
-
return result
|
|
29
|
-
|
|
30
|
-
def test_basic_build_with_demo_file(self):
|
|
31
|
-
"""Test basic build with demo.drawio file."""
|
|
32
|
-
with tempfile.TemporaryDirectory() as temp_dir:
|
|
33
|
-
args = [
|
|
34
|
-
"-i", str(self.demo_file),
|
|
35
|
-
"-o", temp_dir,
|
|
36
|
-
"-l", "i"
|
|
37
|
-
]
|
|
38
|
-
result = self.run_build_script(args)
|
|
39
|
-
self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
|
|
40
|
-
|
|
41
|
-
def test_build_with_directory_input(self):
|
|
42
|
-
"""Test build with directory containing drawio files."""
|
|
43
|
-
with tempfile.TemporaryDirectory() as temp_dir:
|
|
44
|
-
args = [
|
|
45
|
-
"-i", str(self.test_data_dir),
|
|
46
|
-
"-o", temp_dir,
|
|
47
|
-
"-l", "i"
|
|
48
|
-
]
|
|
49
|
-
result = self.run_build_script(args)
|
|
50
|
-
self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
|
|
51
|
-
|
|
52
|
-
def test_build_with_xlsform_strategy(self):
|
|
53
|
-
"""Test build with XLSFormStrategy (default)."""
|
|
54
|
-
with tempfile.TemporaryDirectory() as temp_dir:
|
|
55
|
-
args = [
|
|
56
|
-
"-i", str(self.demo_file),
|
|
57
|
-
"-o", temp_dir,
|
|
58
|
-
"-O", "XLSFormStrategy",
|
|
59
|
-
"-l", "i"
|
|
60
|
-
]
|
|
61
|
-
result = self.run_build_script(args)
|
|
62
|
-
self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
|
|
63
|
-
|
|
64
|
-
def test_build_with_html_strategy(self):
|
|
65
|
-
"""Test build with HTMLStrategy."""
|
|
66
|
-
with tempfile.TemporaryDirectory() as temp_dir:
|
|
67
|
-
args = [
|
|
68
|
-
"-i", str(self.demo_file),
|
|
69
|
-
"-o", temp_dir,
|
|
70
|
-
"-O", "HTMLStrategy",
|
|
71
|
-
"-l", "i"
|
|
72
|
-
]
|
|
73
|
-
result = self.run_build_script(args)
|
|
74
|
-
self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
|
|
75
|
-
|
|
76
|
-
def test_build_with_fhir_strategy(self):
|
|
77
|
-
"""Test build with FHIRStrategy."""
|
|
78
|
-
with tempfile.TemporaryDirectory() as temp_dir:
|
|
79
|
-
args = [
|
|
80
|
-
"-i", str(self.demo_file),
|
|
81
|
-
"-o", temp_dir,
|
|
82
|
-
"-O", "FHIRStrategy",
|
|
83
|
-
"-l", "i"
|
|
84
|
-
]
|
|
85
|
-
result = self.run_build_script(args)
|
|
86
|
-
self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
|
|
87
|
-
|
|
88
|
-
def test_build_with_dhis2_strategy(self):
|
|
89
|
-
"""Test build with DHIS2Strategy."""
|
|
90
|
-
with tempfile.TemporaryDirectory() as temp_dir:
|
|
91
|
-
args = [
|
|
92
|
-
"-i", str(self.demo_file),
|
|
93
|
-
"-o", temp_dir,
|
|
94
|
-
"-O", "DHIS2Strategy",
|
|
95
|
-
"-l", "i"
|
|
96
|
-
]
|
|
97
|
-
result = self.run_build_script(args)
|
|
98
|
-
self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
|
|
99
|
-
|
|
100
|
-
def test_build_with_openmrs_strategy(self):
|
|
101
|
-
"""Test build with OpenMRSStrategy."""
|
|
102
|
-
with tempfile.TemporaryDirectory() as temp_dir:
|
|
103
|
-
args = [
|
|
104
|
-
"-i", str(self.demo_file),
|
|
105
|
-
"-o", temp_dir,
|
|
106
|
-
"-O", "OpenMRSStrategy",
|
|
107
|
-
"-l", "i"
|
|
108
|
-
]
|
|
109
|
-
result = self.run_build_script(args)
|
|
110
|
-
self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
|
|
111
|
-
|
|
112
|
-
def test_build_with_cht_strategy(self):
|
|
113
|
-
"""Test build with XLSFormCHTStrategy."""
|
|
114
|
-
with tempfile.TemporaryDirectory() as temp_dir:
|
|
115
|
-
args = [
|
|
116
|
-
"-i", str(self.demo_file),
|
|
117
|
-
"-o", temp_dir,
|
|
118
|
-
"-O", "XLSFormCHTStrategy",
|
|
119
|
-
"-l", "i"
|
|
120
|
-
]
|
|
121
|
-
result = self.run_build_script(args)
|
|
122
|
-
self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
|
|
123
|
-
|
|
124
|
-
def test_build_with_cht_hf_strategy(self):
|
|
125
|
-
"""Test build with XLSFormCHTHFStrategy."""
|
|
126
|
-
with tempfile.TemporaryDirectory() as temp_dir:
|
|
127
|
-
args = [
|
|
128
|
-
"-i", str(self.demo_file),
|
|
129
|
-
"-o", temp_dir,
|
|
130
|
-
"-O", "XLSFormCHTHFStrategy",
|
|
131
|
-
"-l", "i"
|
|
132
|
-
]
|
|
133
|
-
result = self.run_build_script(args)
|
|
134
|
-
self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
|
|
135
|
-
|
|
136
|
-
def test_build_with_cdss_strategy(self):
|
|
137
|
-
"""Test build with XLSFormCDSSStrategy."""
|
|
138
|
-
with tempfile.TemporaryDirectory() as temp_dir:
|
|
139
|
-
args = [
|
|
140
|
-
"-i", str(self.demo_file),
|
|
141
|
-
"-o", temp_dir,
|
|
142
|
-
"-O", "XLSFormCDSSStrategy",
|
|
143
|
-
"-l", "i"
|
|
144
|
-
]
|
|
145
|
-
result = self.run_build_script(args)
|
|
146
|
-
self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
|
|
147
|
-
|
|
148
|
-
# def test_build_with_spice_strategy(self):
|
|
149
|
-
# """Test build with SpiceStrategy."""
|
|
150
|
-
# with tempfile.TemporaryDirectory() as temp_dir:
|
|
151
|
-
# args = [
|
|
152
|
-
# "-i", str(self.demo_file),
|
|
153
|
-
# "-o", temp_dir,
|
|
154
|
-
# "-O", "SpiceStrategy",
|
|
155
|
-
# "-l", "i"
|
|
156
|
-
# ]
|
|
157
|
-
# result = self.run_build_script(args)
|
|
158
|
-
# self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
|
|
159
|
-
|
|
160
|
-
def test_build_with_debug_level(self):
|
|
161
|
-
"""Test build with debug logging level."""
|
|
162
|
-
with tempfile.TemporaryDirectory() as temp_dir:
|
|
163
|
-
args = [
|
|
164
|
-
"-i", str(self.demo_file),
|
|
165
|
-
"-o", temp_dir,
|
|
166
|
-
"-l", "d"
|
|
167
|
-
]
|
|
168
|
-
result = self.run_build_script(args)
|
|
169
|
-
self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
|
|
170
|
-
|
|
171
|
-
def test_build_with_trad_option(self):
|
|
172
|
-
"""Test build with translation option."""
|
|
173
|
-
with tempfile.TemporaryDirectory() as temp_dir:
|
|
174
|
-
args = [
|
|
175
|
-
"-i", str(self.demo_file),
|
|
176
|
-
"-o", temp_dir,
|
|
177
|
-
"-t",
|
|
178
|
-
"-l", "i"
|
|
179
|
-
]
|
|
180
|
-
result = self.run_build_script(args)
|
|
181
|
-
self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
|
|
182
|
-
|
|
183
|
-
def test_build_with_form_id(self):
|
|
184
|
-
"""Test build with custom form ID."""
|
|
185
|
-
with tempfile.TemporaryDirectory() as temp_dir:
|
|
186
|
-
args = [
|
|
187
|
-
"-i", str(self.demo_file),
|
|
188
|
-
"-o", temp_dir,
|
|
189
|
-
"-d", "test_form_123",
|
|
190
|
-
"-l", "i"
|
|
191
|
-
]
|
|
192
|
-
result = self.run_build_script(args)
|
|
193
|
-
self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
|
|
194
|
-
|
|
195
|
-
def test_build_missing_input(self):
|
|
196
|
-
"""Test build with missing input file."""
|
|
197
|
-
args = ["-o", "/tmp/test_output"]
|
|
198
|
-
result = self.run_build_script(args)
|
|
199
|
-
self.assertNotEqual(result.returncode, 0, "Build should fail with missing input")
|
|
200
|
-
|
|
201
|
-
def test_build_invalid_input_file(self):
|
|
202
|
-
"""Test build with invalid input file."""
|
|
203
|
-
with tempfile.TemporaryDirectory() as temp_dir:
|
|
204
|
-
args = [
|
|
205
|
-
"-i", "/nonexistent/file.drawio",
|
|
206
|
-
"-o", temp_dir,
|
|
207
|
-
"-l", "i"
|
|
208
|
-
]
|
|
209
|
-
result = self.run_build_script(args)
|
|
210
|
-
self.assertNotEqual(result.returncode, 0, "Build should fail with invalid input file")
|
|
211
|
-
|
|
212
|
-
def validate_xls_form(self, xls_path):
|
|
213
|
-
"""Helper method to validate XLS form using ODK libraries."""
|
|
214
|
-
try:
|
|
215
|
-
# Convert XLS to XML using pyxform
|
|
216
|
-
survey = create_survey_from_xls(xls_path)
|
|
217
|
-
xml_output = survey.to_xml()
|
|
218
|
-
|
|
219
|
-
# Basic validation - check if XML was generated successfully
|
|
220
|
-
# In a real scenario, you might want to use odk_validate command line tool
|
|
221
|
-
if xml_output and len(xml_output.strip()) > 0:
|
|
222
|
-
return True, "Validation successful"
|
|
223
|
-
else:
|
|
224
|
-
return False, "Empty XML output"
|
|
225
|
-
except Exception as e:
|
|
226
|
-
return False, str(e)
|
|
227
|
-
|
|
228
|
-
def test_xlsform_strategy_validation(self):
|
|
229
|
-
"""Test XLSFormStrategy output validation with ODK libraries."""
|
|
230
|
-
with tempfile.TemporaryDirectory() as temp_dir:
|
|
231
|
-
xls_output = Path(temp_dir) / "demo_tricc.xlsx"
|
|
232
|
-
args = [
|
|
233
|
-
"-i", str(self.demo_file),
|
|
234
|
-
"-o", temp_dir,
|
|
235
|
-
"-O", "XLSFormStrategy",
|
|
236
|
-
"-l", "i"
|
|
237
|
-
]
|
|
238
|
-
result = self.run_build_script(args)
|
|
239
|
-
self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
|
|
240
|
-
|
|
241
|
-
# Check if XLS file was created
|
|
242
|
-
self.assertTrue(xls_output.exists(), f"XLS file {xls_output} was not created")
|
|
243
|
-
|
|
244
|
-
def test_xlsform_cdss_strategy_validation(self):
|
|
245
|
-
"""Test XLSFormCDSSStrategy output validation with ODK libraries."""
|
|
246
|
-
with tempfile.TemporaryDirectory() as temp_dir:
|
|
247
|
-
xls_output = Path(temp_dir) / "demo_tricc.xlsx"
|
|
248
|
-
args = [
|
|
249
|
-
"-i", str(self.demo_file),
|
|
250
|
-
"-o", temp_dir,
|
|
251
|
-
"-O", "XLSFormCDSSStrategy",
|
|
252
|
-
"-l", "i"
|
|
253
|
-
]
|
|
254
|
-
result = self.run_build_script(args)
|
|
255
|
-
self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if __name__ == "__main__":
|
|
260
|
-
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|