tricc-oo 1.5.27__py3-none-any.whl → 1.5.29__py3-none-any.whl
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.
- tests/build.py +1 -0
- tricc_oo/converters/tricc_to_xls_form.py +1 -1
- tricc_oo/models/base.py +2 -0
- tricc_oo/strategies/output/dhis2_form.py +859 -0
- tricc_oo/strategies/output/openmrs_form.py +47 -3
- tricc_oo/strategies/output/xls_form.py +3 -3
- tricc_oo/visitors/tricc.py +43 -8
- {tricc_oo-1.5.27.dist-info → tricc_oo-1.5.29.dist-info}/METADATA +1 -1
- {tricc_oo-1.5.27.dist-info → tricc_oo-1.5.29.dist-info}/RECORD +12 -11
- {tricc_oo-1.5.27.dist-info → tricc_oo-1.5.29.dist-info}/WHEEL +0 -0
- {tricc_oo-1.5.27.dist-info → tricc_oo-1.5.29.dist-info}/licenses/LICENSE +0 -0
- {tricc_oo-1.5.27.dist-info → tricc_oo-1.5.29.dist-info}/top_level.txt +0 -0
tests/build.py
CHANGED
|
@@ -6,6 +6,7 @@ from tricc_oo.strategies.output.xls_form import XLSFormStrategy # noqa: F401
|
|
|
6
6
|
from tricc_oo.strategies.output.openmrs_form import OpenMRSStrategy # noqa: F401
|
|
7
7
|
from tricc_oo.strategies.output.fhir_form import FHIRStrategy # noqa: F401
|
|
8
8
|
from tricc_oo.strategies.output.html_form import HTMLStrategy # noqa: F401
|
|
9
|
+
from tricc_oo.strategies.output.dhis2_form import DHIS2Strategy # noqa: F401
|
|
9
10
|
from tricc_oo.strategies.input.drawio import DrawioStrategy # noqa: F401
|
|
10
11
|
import getopt
|
|
11
12
|
import logging
|
|
@@ -56,7 +56,7 @@ def get_export_name(node, replace_dots=True):
|
|
|
56
56
|
elif isinstance(value, str):
|
|
57
57
|
export_name = f"'{clean_str(value, replace_dots=replace_dots)}'"
|
|
58
58
|
else:
|
|
59
|
-
export_name =
|
|
59
|
+
export_name = value
|
|
60
60
|
if hasattr(node, 'export_name'):
|
|
61
61
|
node.export_name = export_name
|
|
62
62
|
return export_name
|
tricc_oo/models/base.py
CHANGED
|
@@ -0,0 +1,859 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import json
|
|
4
|
+
import uuid
|
|
5
|
+
import string
|
|
6
|
+
from tricc_oo.visitors.tricc import (
|
|
7
|
+
is_ready_to_process,
|
|
8
|
+
process_reference,
|
|
9
|
+
generate_base,
|
|
10
|
+
generate_calculate,
|
|
11
|
+
walktrhough_tricc_node_processed_stached,
|
|
12
|
+
check_stashed_loop,
|
|
13
|
+
)
|
|
14
|
+
from tricc_oo.converters.tricc_to_xls_form import get_export_name
|
|
15
|
+
import datetime
|
|
16
|
+
from tricc_oo.strategies.output.base_output_strategy import BaseOutPutStrategy
|
|
17
|
+
from tricc_oo.models.base import (
|
|
18
|
+
not_clean, TriccOperation,
|
|
19
|
+
TriccStatic, TriccReference
|
|
20
|
+
)
|
|
21
|
+
from tricc_oo.models.tricc import (
|
|
22
|
+
TriccNodeSelectOption,
|
|
23
|
+
TriccNodeInputModel,
|
|
24
|
+
TriccNodeBaseModel,
|
|
25
|
+
TriccNodeDisplayModel,
|
|
26
|
+
TriccNodeCalculateBase,
|
|
27
|
+
TriccNodeActivity,
|
|
28
|
+
)
|
|
29
|
+
from tricc_oo.models.calculate import TriccNodeDisplayCalculateBase
|
|
30
|
+
from tricc_oo.models.ordered_set import OrderedSet
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger("default")
|
|
33
|
+
|
|
34
|
+
# Namespace for deterministic UUIDs
|
|
35
|
+
UUID_NAMESPACE = uuid.UUID('87654321-4321-8765-cba9-fed098765432')
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class DHIS2Strategy(BaseOutPutStrategy):
|
|
39
|
+
processes = ["main"]
|
|
40
|
+
project = None
|
|
41
|
+
output_path = None
|
|
42
|
+
|
|
43
|
+
def __init__(self, project, output_path):
|
|
44
|
+
super().__init__(project, output_path)
|
|
45
|
+
form_id = getattr(self.project.start_pages["main"], 'form_id', 'dhis2_program')
|
|
46
|
+
self.program_metadata = {
|
|
47
|
+
"id": self.generate_id(form_id),
|
|
48
|
+
"name": form_id,
|
|
49
|
+
"shortName": form_id[:50], # DHIS2 shortName limit
|
|
50
|
+
"programType": "WITHOUT_REGISTRATION",
|
|
51
|
+
"programStages": [],
|
|
52
|
+
"programRules": []
|
|
53
|
+
}
|
|
54
|
+
self.option_sets = {}
|
|
55
|
+
self.options = {}
|
|
56
|
+
self.data_elements = {}
|
|
57
|
+
self.program_rules = []
|
|
58
|
+
self.program_rule_actions = []
|
|
59
|
+
self.program_rule_variables = []
|
|
60
|
+
self.field_counter = 1
|
|
61
|
+
self.current_section = None
|
|
62
|
+
self.concept_map = {}
|
|
63
|
+
# Track programRuleActions per stage
|
|
64
|
+
self.stage_rule_actions = {}
|
|
65
|
+
self.sections = {}
|
|
66
|
+
|
|
67
|
+
def get_export_name(self, r):
|
|
68
|
+
if isinstance(r, TriccNodeSelectOption):
|
|
69
|
+
return self.get_option_value(r.name)
|
|
70
|
+
elif isinstance(r, str):
|
|
71
|
+
return self.get_option_value(r)
|
|
72
|
+
elif isinstance(r, TriccStatic):
|
|
73
|
+
if isinstance(r.value, str):
|
|
74
|
+
return self.get_option_value(r.value)
|
|
75
|
+
elif isinstance(r.value, bool):
|
|
76
|
+
return str(r.value).lower()
|
|
77
|
+
else:
|
|
78
|
+
return r.value
|
|
79
|
+
else:
|
|
80
|
+
return get_export_name(r)
|
|
81
|
+
|
|
82
|
+
def generate_id(self, name):
|
|
83
|
+
"""Generate DHIS2-compliant UID: 1 letter + 10 alphanumeric characters"""
|
|
84
|
+
# Convert UUID to base62-like string and take first 11 chars, ensuring starts with letter
|
|
85
|
+
# Create DHIS2 UID: start with letter, followed by 10 alphanum chars
|
|
86
|
+
letters = string.ascii_letters
|
|
87
|
+
alphanum = string.ascii_letters + string.digits
|
|
88
|
+
|
|
89
|
+
# Use hash of the name to get deterministic but varied results
|
|
90
|
+
import hashlib
|
|
91
|
+
hash_obj = hashlib.md5(name.encode('utf-8')).digest()
|
|
92
|
+
hash_int = int.from_bytes(hash_obj, byteorder='big')
|
|
93
|
+
|
|
94
|
+
# First character: letter
|
|
95
|
+
first_char = letters[hash_int % len(letters)]
|
|
96
|
+
|
|
97
|
+
# Remaining 10 characters: alphanumeric
|
|
98
|
+
remaining_chars = ''
|
|
99
|
+
for i in range(10):
|
|
100
|
+
remaining_chars += alphanum[(hash_int >> (i * 6)) % len(alphanum)]
|
|
101
|
+
|
|
102
|
+
return first_char + remaining_chars
|
|
103
|
+
|
|
104
|
+
def get_option_value(self, option_name):
|
|
105
|
+
if option_name == 'true':
|
|
106
|
+
return TriccStatic(True)
|
|
107
|
+
elif option_name == 'false':
|
|
108
|
+
return TriccStatic(False)
|
|
109
|
+
return self.concept_map.get(option_name, option_name)
|
|
110
|
+
|
|
111
|
+
def get_tricc_operation_expression(self, operation):
|
|
112
|
+
ref_expressions = []
|
|
113
|
+
if not hasattr(operation, "reference"):
|
|
114
|
+
return self.get_tricc_operation_operand(operation)
|
|
115
|
+
for r in operation.reference:
|
|
116
|
+
if isinstance(r, list):
|
|
117
|
+
r_expr = [
|
|
118
|
+
(
|
|
119
|
+
self.get_tricc_operation_expression(sr)
|
|
120
|
+
if isinstance(sr, TriccOperation)
|
|
121
|
+
else self.get_tricc_operation_operand(sr)
|
|
122
|
+
)
|
|
123
|
+
for sr in r
|
|
124
|
+
]
|
|
125
|
+
elif isinstance(r, TriccOperation):
|
|
126
|
+
r_expr = self.get_tricc_operation_expression(r)
|
|
127
|
+
else:
|
|
128
|
+
r_expr = self.get_tricc_operation_operand(r)
|
|
129
|
+
if isinstance(r_expr, TriccReference):
|
|
130
|
+
r_expr = self.get_tricc_operation_operand(r_expr)
|
|
131
|
+
elif isinstance(r_expr, TriccStatic) and isinstance(r_expr.value, bool):
|
|
132
|
+
r_expr = str(r_expr.value).lower()
|
|
133
|
+
ref_expressions.append(r_expr)
|
|
134
|
+
|
|
135
|
+
if hasattr(self, f"tricc_operation_{operation.operator}"):
|
|
136
|
+
callable = getattr(self, f"tricc_operation_{operation.operator}")
|
|
137
|
+
return callable(ref_expressions)
|
|
138
|
+
else:
|
|
139
|
+
raise NotImplementedError(
|
|
140
|
+
f"This type of operation '{operation.operator}' is not supported in this strategy"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
def execute(self):
|
|
144
|
+
version = datetime.datetime.now().strftime("%Y%m%d%H%M")
|
|
145
|
+
logger.info(f"build version: {version}")
|
|
146
|
+
if "main" in self.project.start_pages:
|
|
147
|
+
self.process_base(self.project.start_pages, pages=self.project.pages, version=version)
|
|
148
|
+
else:
|
|
149
|
+
logger.critical("Main process required")
|
|
150
|
+
|
|
151
|
+
logger.info("generate the relevance based on edges")
|
|
152
|
+
self.process_relevance(self.project.start_pages, pages=self.project.pages)
|
|
153
|
+
|
|
154
|
+
logger.info("generate the calculate based on edges")
|
|
155
|
+
self.process_calculate(self.project.start_pages, pages=self.project.pages)
|
|
156
|
+
|
|
157
|
+
logger.info("generate the export format")
|
|
158
|
+
self.process_export(self.project.start_pages, pages=self.project.pages)
|
|
159
|
+
|
|
160
|
+
logger.info("print the export")
|
|
161
|
+
self.export(self.project.start_pages, version=version)
|
|
162
|
+
|
|
163
|
+
def map_tricc_type_to_dhis2_value_type(self, node):
|
|
164
|
+
mapping = {
|
|
165
|
+
'text': 'TEXT',
|
|
166
|
+
'integer': 'INTEGER',
|
|
167
|
+
'decimal': 'NUMBER',
|
|
168
|
+
'date': 'DATE',
|
|
169
|
+
'datetime': 'DATETIME',
|
|
170
|
+
'select_one': 'TEXT', # DHIS2 handles options via optionSets
|
|
171
|
+
'select_multiple': 'TEXT', # Multiple selections as comma-separated
|
|
172
|
+
'select_yesno': 'BOOLEAN',
|
|
173
|
+
'yesno': 'BOOLEAN',
|
|
174
|
+
'boolean': 'BOOLEAN',
|
|
175
|
+
'not_available': 'BOOLEAN',
|
|
176
|
+
'note': 'LONG_TEXT'
|
|
177
|
+
}
|
|
178
|
+
return mapping.get(node.tricc_type, 'TEXT')
|
|
179
|
+
|
|
180
|
+
def generate_base(self, node, processed_nodes, **kwargs):
|
|
181
|
+
if generate_base(node, processed_nodes, **kwargs):
|
|
182
|
+
if getattr(node, 'name', '') not in ('true', 'false'):
|
|
183
|
+
self.concept_map[node.name] = self.generate_id(self.get_export_name(node))
|
|
184
|
+
return True
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
def generate_relevance(self, node, processed_nodes, **kwargs):
|
|
188
|
+
if not is_ready_to_process(node, processed_nodes, strict=True):
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
if node not in processed_nodes:
|
|
192
|
+
relevance = None
|
|
193
|
+
if hasattr(node, 'relevance') and node.relevance:
|
|
194
|
+
relevance = node.relevance
|
|
195
|
+
if hasattr(node, 'expression') and node.expression:
|
|
196
|
+
relevance = node.expression
|
|
197
|
+
if relevance:
|
|
198
|
+
relevance_str = self.convert_expression_to_string(not_clean(relevance))
|
|
199
|
+
if relevance_str and relevance_str != 'false':
|
|
200
|
+
# Create program rule action for hiding/showing based on relevance
|
|
201
|
+
rule_id = self.generate_id(f"rule_{node.get_name()}_relevance")
|
|
202
|
+
action_id = self.generate_id(f"action_{rule_id}")
|
|
203
|
+
|
|
204
|
+
if isinstance(node, TriccNodeActivity):
|
|
205
|
+
# For activities, use HIDESECTION action instead of HIDEFIELD
|
|
206
|
+
# Store activity reference for later section ID assignment
|
|
207
|
+
program_rule_action = {
|
|
208
|
+
"id": action_id,
|
|
209
|
+
"programRuleActionType": "HIDESECTION",
|
|
210
|
+
"activity_ref": node, # Temporary reference to be replaced with section ID
|
|
211
|
+
"programRule": {"id": rule_id},
|
|
212
|
+
}
|
|
213
|
+
else:
|
|
214
|
+
# For regular nodes, use HIDEFIELD action
|
|
215
|
+
program_rule_action = {
|
|
216
|
+
"id": action_id,
|
|
217
|
+
"programRuleActionType": "HIDEFIELD",
|
|
218
|
+
"dataElement": {
|
|
219
|
+
"id": self.generate_id(self.get_export_name(node))
|
|
220
|
+
},
|
|
221
|
+
"programRule": {"id": rule_id}
|
|
222
|
+
}
|
|
223
|
+
self.program_rule_actions.append(program_rule_action)
|
|
224
|
+
|
|
225
|
+
# Create program rule referencing the action
|
|
226
|
+
self.program_rules.append({
|
|
227
|
+
"id": rule_id,
|
|
228
|
+
"name": f"Hide {node.get_name()} when condition met",
|
|
229
|
+
"description": f"Hide {node.get_name()} based on relevance",
|
|
230
|
+
"condition": f"!({relevance_str})", # Negate for hide when true
|
|
231
|
+
"programRuleActions": [{"id": action_id}]
|
|
232
|
+
})
|
|
233
|
+
return True
|
|
234
|
+
|
|
235
|
+
def generate_data_element(self, node):
|
|
236
|
+
if issubclass(node.__class__, TriccNodeDisplayModel) and not isinstance(node, TriccNodeSelectOption):
|
|
237
|
+
de_id = self.generate_id(self.get_export_name(node))
|
|
238
|
+
|
|
239
|
+
# Check if this is a boolean question (yes/no with boolean options)
|
|
240
|
+
is_boolean_question = False
|
|
241
|
+
if hasattr(node, 'options') and node.options:
|
|
242
|
+
option_names = [
|
|
243
|
+
str(self.get_export_name(opt)).lower()
|
|
244
|
+
for opt in node.options.values()
|
|
245
|
+
if isinstance(opt, TriccNodeSelectOption)]
|
|
246
|
+
# If options are only true/false or yes/no variants, treat as boolean
|
|
247
|
+
boolean_options = {'true', 'false', 'yes', 'no', '1', '0'}
|
|
248
|
+
if all(opt in boolean_options for opt in option_names):
|
|
249
|
+
is_boolean_question = True
|
|
250
|
+
|
|
251
|
+
# Override valueType for boolean questions
|
|
252
|
+
value_type = self.map_tricc_type_to_dhis2_value_type(node)
|
|
253
|
+
if is_boolean_question:
|
|
254
|
+
value_type = "BOOLEAN"
|
|
255
|
+
|
|
256
|
+
data_element = {
|
|
257
|
+
"id": de_id,
|
|
258
|
+
"name": self.get_export_name(node),
|
|
259
|
+
"shortName": node.name[:50],
|
|
260
|
+
"displayFormName": getattr(node, 'label', node.name).replace('\u00a0', ' ').strip(),
|
|
261
|
+
"valueType": value_type,
|
|
262
|
+
"domainType": "TRACKER",
|
|
263
|
+
"aggregationType": "NONE"
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
# Only create optionSet for non-boolean select questions
|
|
267
|
+
if node.tricc_type in ['select_one', 'select_multiple'] and not is_boolean_question:
|
|
268
|
+
# Create optionSet for choices
|
|
269
|
+
if hasattr(node, 'options') and node.options:
|
|
270
|
+
option_set_id = self.generate_id(f"optionset_{node.name}")
|
|
271
|
+
data_element["optionSet"] = {"id": option_set_id}
|
|
272
|
+
|
|
273
|
+
# Create the actual optionSet definition
|
|
274
|
+
option_set = {
|
|
275
|
+
"id": option_set_id,
|
|
276
|
+
"name": f"{node.name} Options",
|
|
277
|
+
"shortName": f"{node.name}_opts"[:50],
|
|
278
|
+
"valueType": "TEXT",
|
|
279
|
+
"options": []
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
# Add options (node.options is a dict, not a list)
|
|
283
|
+
for key, option in node.options.items():
|
|
284
|
+
if isinstance(option, TriccNodeSelectOption):
|
|
285
|
+
option_id = self.generate_id(f"option_{node.name}_{option.name}")
|
|
286
|
+
option_name = self.get_export_name(option)
|
|
287
|
+
if isinstance(option_name, str):
|
|
288
|
+
option_name = option_name.replace('\u00a0', ' ').strip()
|
|
289
|
+
elif isinstance(option_name, TriccStatic):
|
|
290
|
+
option_name = str(option_name.value)
|
|
291
|
+
# Create separate option entity
|
|
292
|
+
option_def = {
|
|
293
|
+
"id": option_id,
|
|
294
|
+
"name": option_name,
|
|
295
|
+
"shortName": option.name[:50],
|
|
296
|
+
"code": str(self.get_export_name(option))
|
|
297
|
+
}
|
|
298
|
+
self.options[option_id] = option_def
|
|
299
|
+
|
|
300
|
+
# Add option reference to optionSet (only ID)
|
|
301
|
+
option_set["options"].append({"id": option_id})
|
|
302
|
+
|
|
303
|
+
self.option_sets[option_set_id] = option_set
|
|
304
|
+
|
|
305
|
+
self.data_elements[node.name] = data_element
|
|
306
|
+
return data_element
|
|
307
|
+
return None
|
|
308
|
+
|
|
309
|
+
def generate_calculate(self, node, processed_nodes, **kwargs):
|
|
310
|
+
if generate_calculate(node, processed_nodes, **kwargs):
|
|
311
|
+
if issubclass(node.__class__, TriccNodeCalculateBase) and node.expression:
|
|
312
|
+
# Create program rule variable for the calculate
|
|
313
|
+
var_id = self.generate_id(self.get_export_name(node))
|
|
314
|
+
expression_str = self.convert_expression_to_string(node.expression)
|
|
315
|
+
|
|
316
|
+
# Determine data type from operation
|
|
317
|
+
data_type = "TEXT" # default
|
|
318
|
+
if hasattr(node.expression, 'get_datatype'):
|
|
319
|
+
operation_datatype = node.expression.get_datatype()
|
|
320
|
+
if operation_datatype:
|
|
321
|
+
# Create a mock node with the datatype to use the mapping function
|
|
322
|
+
class MockNode:
|
|
323
|
+
def __init__(self, tricc_type):
|
|
324
|
+
self.tricc_type = tricc_type
|
|
325
|
+
mock_node = MockNode(operation_datatype)
|
|
326
|
+
data_type = self.map_tricc_type_to_dhis2_value_type(mock_node)
|
|
327
|
+
|
|
328
|
+
program_rule_variable = {
|
|
329
|
+
"id": var_id,
|
|
330
|
+
"name": self.get_export_name(node.name)[:50],
|
|
331
|
+
"programRuleVariableSourceType": "CALCULATED_VALUE",
|
|
332
|
+
"calculatedValueScript": expression_str,
|
|
333
|
+
"dataType": data_type,
|
|
334
|
+
"useCodeForOptionSet": False,
|
|
335
|
+
"program": {"id": self.program_metadata["id"]}
|
|
336
|
+
}
|
|
337
|
+
self.program_rule_variables.append(program_rule_variable)
|
|
338
|
+
# Add to concept map for potential referencing
|
|
339
|
+
self.concept_map[node.name] = var_id
|
|
340
|
+
return True
|
|
341
|
+
return False
|
|
342
|
+
|
|
343
|
+
def process_export(self, start_pages, **kwargs):
|
|
344
|
+
self.activity_export(start_pages["main"], **kwargs)
|
|
345
|
+
|
|
346
|
+
def activity_export(self, activity, processed_nodes=None, **kwargs):
|
|
347
|
+
if processed_nodes is None:
|
|
348
|
+
processed_nodes = OrderedSet()
|
|
349
|
+
stashed_nodes = OrderedSet()
|
|
350
|
+
groups = {}
|
|
351
|
+
groups[activity.id] = 0
|
|
352
|
+
path_len = 0
|
|
353
|
+
process = ["main"]
|
|
354
|
+
|
|
355
|
+
# Create program stage
|
|
356
|
+
stage_id = self.generate_id(self.get_export_name(activity))
|
|
357
|
+
program_stage = {
|
|
358
|
+
"id": stage_id,
|
|
359
|
+
"name": getattr(activity.root, 'label', 'Main Stage').replace('\u00a0', ' ').strip(),
|
|
360
|
+
"programStageDataElements": [],
|
|
361
|
+
"programStageSections": []
|
|
362
|
+
}
|
|
363
|
+
self.program_metadata["programStages"].append(program_stage)
|
|
364
|
+
|
|
365
|
+
# Start with the main section for this activity
|
|
366
|
+
self.start_section(activity, groups, processed_nodes, process, **kwargs)
|
|
367
|
+
|
|
368
|
+
walktrhough_tricc_node_processed_stached(
|
|
369
|
+
activity.root,
|
|
370
|
+
self.generate_export,
|
|
371
|
+
processed_nodes,
|
|
372
|
+
stashed_nodes,
|
|
373
|
+
path_len,
|
|
374
|
+
cur_group=activity.root.group,
|
|
375
|
+
process=process,
|
|
376
|
+
recursive=False,
|
|
377
|
+
**kwargs
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
# End the main section
|
|
381
|
+
self.end_section(activity, groups, **kwargs)
|
|
382
|
+
|
|
383
|
+
# Manage stashed nodes similar to other strategies
|
|
384
|
+
prev_stashed_nodes = stashed_nodes.copy()
|
|
385
|
+
loop_count = 0
|
|
386
|
+
len_prev_processed_nodes = 0
|
|
387
|
+
while len(stashed_nodes) > 0:
|
|
388
|
+
loop_count = check_stashed_loop(
|
|
389
|
+
stashed_nodes,
|
|
390
|
+
prev_stashed_nodes,
|
|
391
|
+
processed_nodes,
|
|
392
|
+
len_prev_processed_nodes,
|
|
393
|
+
loop_count,
|
|
394
|
+
)
|
|
395
|
+
prev_stashed_nodes = stashed_nodes.copy()
|
|
396
|
+
len_prev_processed_nodes = len(processed_nodes)
|
|
397
|
+
if len(stashed_nodes) > 0:
|
|
398
|
+
s_node = stashed_nodes.pop()
|
|
399
|
+
if s_node.group is None:
|
|
400
|
+
logger.critical("ERROR group is none for node {}".format(s_node.get_name()))
|
|
401
|
+
|
|
402
|
+
# Start section for stashed node if it's a different group
|
|
403
|
+
self.start_section(s_node.group, groups, processed_nodes, process, relevance=True, **kwargs)
|
|
404
|
+
|
|
405
|
+
walktrhough_tricc_node_processed_stached(
|
|
406
|
+
s_node,
|
|
407
|
+
self.generate_export,
|
|
408
|
+
processed_nodes,
|
|
409
|
+
stashed_nodes,
|
|
410
|
+
path_len,
|
|
411
|
+
groups=groups,
|
|
412
|
+
cur_group=s_node.group,
|
|
413
|
+
recursive=False,
|
|
414
|
+
process=process,
|
|
415
|
+
**kwargs
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
# End section for stashed node
|
|
419
|
+
self.end_section(s_node.group, groups, **kwargs)
|
|
420
|
+
|
|
421
|
+
return processed_nodes
|
|
422
|
+
|
|
423
|
+
def start_section(self, cur_group, groups, processed_nodes, process, relevance=False, **kwargs):
|
|
424
|
+
name = get_export_name(cur_group)
|
|
425
|
+
|
|
426
|
+
if name in groups:
|
|
427
|
+
groups[name] += 1
|
|
428
|
+
name = name + "_" + str(groups[name])
|
|
429
|
+
else:
|
|
430
|
+
groups[name] = 0
|
|
431
|
+
|
|
432
|
+
relevance_expression = (
|
|
433
|
+
cur_group.relevance if (
|
|
434
|
+
relevance and
|
|
435
|
+
cur_group.relevance is not None and
|
|
436
|
+
cur_group.relevance != ""
|
|
437
|
+
) else ""
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
if not relevance:
|
|
441
|
+
relevance_expression = ""
|
|
442
|
+
elif isinstance(relevance_expression, (TriccOperation, TriccStatic)):
|
|
443
|
+
relevance_expression = self.get_tricc_operation_expression(relevance_expression)
|
|
444
|
+
|
|
445
|
+
# Create section
|
|
446
|
+
section_id = self.generate_id(f"section_{name}")
|
|
447
|
+
section_name = name
|
|
448
|
+
if cur_group and hasattr(cur_group, 'label') and cur_group.label:
|
|
449
|
+
section_name = cur_group.label.replace('\u00a0', ' ').strip()
|
|
450
|
+
section = {
|
|
451
|
+
"id": section_id,
|
|
452
|
+
"name": section_name,
|
|
453
|
+
"sortOrder": len(self.sections),
|
|
454
|
+
"programStage": {"id": self.program_metadata["programStages"][-1]["id"]},
|
|
455
|
+
"dataElements": [],
|
|
456
|
+
"activity_ref": cur_group
|
|
457
|
+
}
|
|
458
|
+
# Add section to program stage
|
|
459
|
+
if self.program_metadata["programStages"]:
|
|
460
|
+
self.program_metadata["programStages"][-1]["programStageSections"].append({"id": section_id})
|
|
461
|
+
|
|
462
|
+
self.sections[section_id] = section
|
|
463
|
+
self.current_section = section_id
|
|
464
|
+
|
|
465
|
+
def end_section(self, cur_group, groups, **kwargs):
|
|
466
|
+
# In DHIS2, sections don't have explicit end markers like XLSForm groups
|
|
467
|
+
# The section is already created and added to the program stage
|
|
468
|
+
pass
|
|
469
|
+
|
|
470
|
+
def generate_export(self, node, processed_nodes, **kwargs):
|
|
471
|
+
if not is_ready_to_process(node, processed_nodes, strict=True):
|
|
472
|
+
return False
|
|
473
|
+
|
|
474
|
+
if not process_reference(
|
|
475
|
+
node, processed_nodes, {}, replace_reference=False, codesystems=kwargs.get("codesystems", None)
|
|
476
|
+
):
|
|
477
|
+
return False
|
|
478
|
+
|
|
479
|
+
if node not in processed_nodes:
|
|
480
|
+
# Skip creating data elements for calculate nodes - they should only be program rule variables
|
|
481
|
+
if not issubclass(node.__class__, TriccNodeCalculateBase):
|
|
482
|
+
data_element = self.generate_data_element(node)
|
|
483
|
+
if data_element:
|
|
484
|
+
# Add to program stage
|
|
485
|
+
if self.program_metadata["programStages"]:
|
|
486
|
+
psde_id = self.generate_id(f"psde_{node.name}")
|
|
487
|
+
psde = {
|
|
488
|
+
"id": psde_id,
|
|
489
|
+
"dataElement": {"id": data_element["id"]},
|
|
490
|
+
"compulsory": bool(getattr(node, 'required', False))
|
|
491
|
+
}
|
|
492
|
+
self.program_metadata["programStages"][-1]["programStageDataElements"].append(psde)
|
|
493
|
+
|
|
494
|
+
# Add data element to current section
|
|
495
|
+
if self.current_section and self.current_section in self.sections:
|
|
496
|
+
self.sections[self.current_section]["dataElements"].append({"id": data_element["id"]})
|
|
497
|
+
|
|
498
|
+
return True
|
|
499
|
+
|
|
500
|
+
def clean_section(self, program_stages_payload):
|
|
501
|
+
"""Clean sections by removing empty ones and merging sections with same activity_ref"""
|
|
502
|
+
sections_to_remove = set()
|
|
503
|
+
prev_activity_ref = None
|
|
504
|
+
prev_section_id = None
|
|
505
|
+
|
|
506
|
+
for section in sorted(self.sections.values(), key=lambda x: x["sortOrder"]):
|
|
507
|
+
section_id = section["id"]
|
|
508
|
+
activity_ref = section.get("activity_ref")
|
|
509
|
+
# Remove empty sections
|
|
510
|
+
if not section.get("dataElements"):
|
|
511
|
+
sections_to_remove.add(section_id)
|
|
512
|
+
|
|
513
|
+
# Check for sections with same activity_ref
|
|
514
|
+
elif activity_ref == prev_activity_ref:
|
|
515
|
+
# Merge this section into the existing one
|
|
516
|
+
existing_section = self.sections[prev_section_id]
|
|
517
|
+
|
|
518
|
+
# Move data elements to existing section
|
|
519
|
+
existing_section["dataElements"].extend(section["dataElements"])
|
|
520
|
+
|
|
521
|
+
# Mark this section for removal
|
|
522
|
+
sections_to_remove.add(section_id)
|
|
523
|
+
else:
|
|
524
|
+
prev_activity_ref = activity_ref
|
|
525
|
+
prev_section_id = section_id
|
|
526
|
+
|
|
527
|
+
# Remove sections that should be removed
|
|
528
|
+
for section_id in sections_to_remove:
|
|
529
|
+
if section_id in self.sections:
|
|
530
|
+
del self.sections[section_id]
|
|
531
|
+
|
|
532
|
+
# Update stage sections to remove deleted sections
|
|
533
|
+
for stage in program_stages_payload:
|
|
534
|
+
stage["programStageSections"][:] = [
|
|
535
|
+
s for s in stage["programStageSections"]
|
|
536
|
+
if s["id"] not in sections_to_remove
|
|
537
|
+
]
|
|
538
|
+
|
|
539
|
+
def export(self, start_pages, version):
|
|
540
|
+
form_id = start_pages["main"].root.form_id or "dhis2_program"
|
|
541
|
+
base_path = os.path.join(self.output_path, form_id)
|
|
542
|
+
if not os.path.exists(base_path):
|
|
543
|
+
os.makedirs(base_path)
|
|
544
|
+
|
|
545
|
+
# Prepare collections for all entities
|
|
546
|
+
program_rules_payload = []
|
|
547
|
+
program_rule_actions_payload = []
|
|
548
|
+
program_stages_payload = []
|
|
549
|
+
program_rule_variables_payload = []
|
|
550
|
+
|
|
551
|
+
if self.program_metadata["programStages"]:
|
|
552
|
+
# Extract full stage definitions
|
|
553
|
+
program_stages_payload = [
|
|
554
|
+
{
|
|
555
|
+
**stage,
|
|
556
|
+
"program": {"id": self.program_metadata["id"]}
|
|
557
|
+
}
|
|
558
|
+
for stage in self.program_metadata["programStages"]
|
|
559
|
+
]
|
|
560
|
+
# Clean sections before processing actions to ensure only valid sections are used
|
|
561
|
+
self.clean_section(program_stages_payload)
|
|
562
|
+
# In program, only keep stage ID references
|
|
563
|
+
self.program_metadata["programStages"] = [
|
|
564
|
+
{"id": stage["id"]}
|
|
565
|
+
for stage in program_stages_payload
|
|
566
|
+
]
|
|
567
|
+
else:
|
|
568
|
+
program_stages_payload = []
|
|
569
|
+
|
|
570
|
+
if self.program_rule_actions:
|
|
571
|
+
# Resolve activity references to section IDs for HIDESECTION actions
|
|
572
|
+
program_rule_actions_payload = []
|
|
573
|
+
for action in self.program_rule_actions:
|
|
574
|
+
if action.get("activity_ref"):
|
|
575
|
+
# Find all sections for this activity (after cleaning)
|
|
576
|
+
activity = action["activity_ref"]
|
|
577
|
+
matching_sections = [
|
|
578
|
+
sec_id for sec_id, section in self.sections.items()
|
|
579
|
+
if section.get("activity_ref") == activity
|
|
580
|
+
]
|
|
581
|
+
|
|
582
|
+
# Create one action per matching section
|
|
583
|
+
for i, section_id in enumerate(matching_sections):
|
|
584
|
+
action_copy = dict(action)
|
|
585
|
+
action_copy["programStageSection"] = {"id": section_id}
|
|
586
|
+
del action_copy["activity_ref"]
|
|
587
|
+
|
|
588
|
+
if i > 0:
|
|
589
|
+
# For additional sections, create new IDs for action and corresponding rule
|
|
590
|
+
original_rule_id = action["programRule"]["id"]
|
|
591
|
+
new_rule_id = self.generate_id(f"{original_rule_id}_section_{i}")
|
|
592
|
+
new_action_id = self.generate_id(f"{action['id']}_section_{i}")
|
|
593
|
+
|
|
594
|
+
action_copy["id"] = new_action_id
|
|
595
|
+
action_copy["programRule"] = {"id": new_rule_id}
|
|
596
|
+
|
|
597
|
+
# Create duplicate rule with new ID
|
|
598
|
+
original_rule = next(
|
|
599
|
+
(r for r in self.program_rules if r["id"] == original_rule_id), None
|
|
600
|
+
)
|
|
601
|
+
if original_rule:
|
|
602
|
+
new_rule = dict(original_rule)
|
|
603
|
+
new_rule["id"] = new_rule_id
|
|
604
|
+
new_rule["name"] = f"{original_rule['name']} (Section {i})"
|
|
605
|
+
new_rule["programRuleActions"] = [{"id": new_action_id}]
|
|
606
|
+
self.program_rules.append(new_rule)
|
|
607
|
+
|
|
608
|
+
program_rule_actions_payload.append(action_copy)
|
|
609
|
+
else:
|
|
610
|
+
# Non-activity actions (HIDEFIELD) can be added directly
|
|
611
|
+
program_rule_actions_payload.append(action)
|
|
612
|
+
|
|
613
|
+
if self.program_rules:
|
|
614
|
+
program_rules_payload = [
|
|
615
|
+
{
|
|
616
|
+
**rule,
|
|
617
|
+
"program": {"id": self.program_metadata["id"]}
|
|
618
|
+
}
|
|
619
|
+
for rule in self.program_rules
|
|
620
|
+
]
|
|
621
|
+
|
|
622
|
+
if self.program_rule_variables:
|
|
623
|
+
program_rule_variables_payload = self.program_rule_variables
|
|
624
|
+
|
|
625
|
+
# Build the program with references to other entities
|
|
626
|
+
program_payload = dict(self.program_metadata)
|
|
627
|
+
if program_rule_variables_payload:
|
|
628
|
+
program_payload["programRuleVariables"] = [
|
|
629
|
+
{"id": var["id"]}
|
|
630
|
+
for var in program_rule_variables_payload
|
|
631
|
+
]
|
|
632
|
+
if program_rules_payload:
|
|
633
|
+
program_payload["programRules"] = [
|
|
634
|
+
{"id": rule["id"]}
|
|
635
|
+
for rule in program_rules_payload
|
|
636
|
+
]
|
|
637
|
+
|
|
638
|
+
# Create single comprehensive payload with all entities at root level
|
|
639
|
+
full_payload = {
|
|
640
|
+
"programs": [program_payload]
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if program_stages_payload:
|
|
644
|
+
full_payload["programStages"] = program_stages_payload
|
|
645
|
+
if program_rules_payload:
|
|
646
|
+
full_payload["programRules"] = program_rules_payload
|
|
647
|
+
if program_rule_actions_payload:
|
|
648
|
+
full_payload["programRuleActions"] = program_rule_actions_payload
|
|
649
|
+
if program_rule_variables_payload:
|
|
650
|
+
full_payload["programRuleVariables"] = program_rule_variables_payload
|
|
651
|
+
if self.data_elements:
|
|
652
|
+
full_payload["dataElements"] = list(self.data_elements.values())
|
|
653
|
+
if self.options:
|
|
654
|
+
full_payload["options"] = list(self.options.values())
|
|
655
|
+
if self.option_sets:
|
|
656
|
+
full_payload["optionSets"] = list(self.option_sets.values())
|
|
657
|
+
if self.sections:
|
|
658
|
+
# Remove activity_ref from sections before serialization
|
|
659
|
+
sections_payload = []
|
|
660
|
+
for section in self.sections.values():
|
|
661
|
+
section_copy = dict(section)
|
|
662
|
+
if "activity_ref" in section_copy:
|
|
663
|
+
del section_copy["activity_ref"]
|
|
664
|
+
sections_payload.append(section_copy)
|
|
665
|
+
full_payload["programStageSections"] = sections_payload
|
|
666
|
+
|
|
667
|
+
# Export everything to a single file
|
|
668
|
+
metadata_file = os.path.join(base_path, f"{form_id}_metadata.json")
|
|
669
|
+
with open(metadata_file, 'w') as f:
|
|
670
|
+
json.dump(full_payload, f, indent=2)
|
|
671
|
+
logger.info(f"Exported complete DHIS2 metadata to {metadata_file}")
|
|
672
|
+
|
|
673
|
+
def get_tricc_operation_operand(self, r):
|
|
674
|
+
if isinstance(r, TriccOperation):
|
|
675
|
+
return self.get_tricc_operation_expression(r)
|
|
676
|
+
elif isinstance(r, TriccReference):
|
|
677
|
+
# Use DHIS2 ID from concept_map instead of name
|
|
678
|
+
node_id = self.concept_map.get(r.value.name, self.get_export_name(r.value))
|
|
679
|
+
return f"#{{{node_id}}}"
|
|
680
|
+
elif isinstance(r, TriccStatic):
|
|
681
|
+
if isinstance(r.value, bool):
|
|
682
|
+
return str(r.value).lower()
|
|
683
|
+
if isinstance(r.value, str):
|
|
684
|
+
return f"'{r.value}'"
|
|
685
|
+
else:
|
|
686
|
+
return str(r.value)
|
|
687
|
+
elif isinstance(r, bool):
|
|
688
|
+
return str(r).lower()
|
|
689
|
+
elif isinstance(r, str):
|
|
690
|
+
return f"{r}"
|
|
691
|
+
elif isinstance(r, (int, float)):
|
|
692
|
+
return str(r)
|
|
693
|
+
elif isinstance(r, TriccNodeSelectOption):
|
|
694
|
+
option = self.get_option_value(r.name)
|
|
695
|
+
if r.name in ('true', 'false'):
|
|
696
|
+
return option
|
|
697
|
+
return f"'{option}'"
|
|
698
|
+
elif issubclass(r.__class__, TriccNodeDisplayCalculateBase):
|
|
699
|
+
# Use DHIS2 ID from concept_map instead of name
|
|
700
|
+
node_id = self.concept_map.get(r.name, self.get_export_name(r))
|
|
701
|
+
return f"#{node_id}"
|
|
702
|
+
elif issubclass(r.__class__, TriccNodeInputModel):
|
|
703
|
+
# Use DHIS2 ID from concept_map instead of name
|
|
704
|
+
node_id = self.concept_map.get(r.name, self.get_export_name(r))
|
|
705
|
+
return f"#{{{node_id}}}"
|
|
706
|
+
elif issubclass(r.__class__, TriccNodeBaseModel):
|
|
707
|
+
# Use DHIS2 ID from concept_map instead of name
|
|
708
|
+
node_id = self.concept_map.get(r.name, self.get_export_name(r))
|
|
709
|
+
return f"#{{{node_id}}}"
|
|
710
|
+
else:
|
|
711
|
+
raise NotImplementedError(f"This type of node {r.__class__.__name__} is not supported within an operation")
|
|
712
|
+
|
|
713
|
+
def convert_expression_to_string(self, expression):
|
|
714
|
+
if isinstance(expression, TriccOperation):
|
|
715
|
+
return self.get_tricc_operation_expression(expression)
|
|
716
|
+
else:
|
|
717
|
+
return self.get_tricc_operation_operand(expression)
|
|
718
|
+
|
|
719
|
+
# Operation methods for DHIS2 expressions
|
|
720
|
+
def tricc_operation_equal(self, ref_expressions):
|
|
721
|
+
return f"{ref_expressions[0]} == {ref_expressions[1]}"
|
|
722
|
+
|
|
723
|
+
def tricc_operation_not_equal(self, ref_expressions):
|
|
724
|
+
return f"{ref_expressions[0]} != {ref_expressions[1]}"
|
|
725
|
+
|
|
726
|
+
def tricc_operation_and(self, ref_expressions):
|
|
727
|
+
if len(ref_expressions) == 1:
|
|
728
|
+
return ref_expressions[0]
|
|
729
|
+
if len(ref_expressions) > 1:
|
|
730
|
+
return " && ".join(ref_expressions)
|
|
731
|
+
else:
|
|
732
|
+
return "true"
|
|
733
|
+
|
|
734
|
+
def tricc_operation_or(self, ref_expressions):
|
|
735
|
+
if len(ref_expressions) == 1:
|
|
736
|
+
return ref_expressions[0]
|
|
737
|
+
if len(ref_expressions) > 1:
|
|
738
|
+
return "(" + " || ".join(ref_expressions) + ")"
|
|
739
|
+
else:
|
|
740
|
+
return "true"
|
|
741
|
+
|
|
742
|
+
def tricc_operation_not(self, ref_expressions):
|
|
743
|
+
return f"!({ref_expressions[0]})"
|
|
744
|
+
|
|
745
|
+
def tricc_operation_plus(self, ref_expressions):
|
|
746
|
+
return " + ".join(ref_expressions)
|
|
747
|
+
|
|
748
|
+
def tricc_operation_minus(self, ref_expressions):
|
|
749
|
+
if len(ref_expressions) > 1:
|
|
750
|
+
return " - ".join(map(str, ref_expressions))
|
|
751
|
+
elif len(ref_expressions) == 1:
|
|
752
|
+
return f"-{ref_expressions[0]}"
|
|
753
|
+
|
|
754
|
+
def tricc_operation_more(self, ref_expressions):
|
|
755
|
+
return f"{ref_expressions[0]} > {ref_expressions[1]}"
|
|
756
|
+
|
|
757
|
+
def tricc_operation_less(self, ref_expressions):
|
|
758
|
+
return f"{ref_expressions[0]} < {ref_expressions[1]}"
|
|
759
|
+
|
|
760
|
+
def tricc_operation_more_or_equal(self, ref_expressions):
|
|
761
|
+
return f"{ref_expressions[0]} >= {ref_expressions[1]}"
|
|
762
|
+
|
|
763
|
+
def tricc_operation_less_or_equal(self, ref_expressions):
|
|
764
|
+
return f"{ref_expressions[0]} <= {ref_expressions[1]}"
|
|
765
|
+
|
|
766
|
+
def tricc_operation_selected(self, ref_expressions):
|
|
767
|
+
# For DHIS2, check if value is selected in multi-select
|
|
768
|
+
return f"d2:hasValue({ref_expressions[0]}) && d2:contains({ref_expressions[0]}, {ref_expressions[1]})"
|
|
769
|
+
|
|
770
|
+
def tricc_operation_count(self, ref_expressions):
|
|
771
|
+
return f"d2:count({ref_expressions[0]})"
|
|
772
|
+
|
|
773
|
+
def tricc_operation_multiplied(self, ref_expressions):
|
|
774
|
+
return "*".join(ref_expressions)
|
|
775
|
+
|
|
776
|
+
def tricc_operation_divided(self, ref_expressions):
|
|
777
|
+
return f"{ref_expressions[0]} / {ref_expressions[1]}"
|
|
778
|
+
|
|
779
|
+
def tricc_operation_modulo(self, ref_expressions):
|
|
780
|
+
return f"{ref_expressions[0]} % {ref_expressions[1]}"
|
|
781
|
+
|
|
782
|
+
def tricc_operation_coalesce(self, ref_expressions):
|
|
783
|
+
return f"d2:coalesce({','.join(ref_expressions)})"
|
|
784
|
+
|
|
785
|
+
def tricc_operation_native(self, ref_expressions):
|
|
786
|
+
if len(ref_expressions) > 0:
|
|
787
|
+
return f"{ref_expressions[0]}({','.join(ref_expressions[1:])})"
|
|
788
|
+
|
|
789
|
+
def tricc_operation_istrue(self, ref_expressions):
|
|
790
|
+
return f"{ref_expressions[0]} == true"
|
|
791
|
+
|
|
792
|
+
def tricc_operation_isfalse(self, ref_expressions):
|
|
793
|
+
return f"{ref_expressions[0]} == false"
|
|
794
|
+
|
|
795
|
+
def tricc_operation_parenthesis(self, ref_expressions):
|
|
796
|
+
return f"({ref_expressions[0]})"
|
|
797
|
+
|
|
798
|
+
def tricc_operation_between(self, ref_expressions):
|
|
799
|
+
return f"{ref_expressions[0]} >= {ref_expressions[1]} && {ref_expressions[0]} < {ref_expressions[2]}"
|
|
800
|
+
|
|
801
|
+
def tricc_operation_isnull(self, ref_expressions):
|
|
802
|
+
return f"!d2:hasValue({ref_expressions[0]})"
|
|
803
|
+
|
|
804
|
+
def tricc_operation_isnotnull(self, ref_expressions):
|
|
805
|
+
return f"d2:hasValue({ref_expressions[0]})"
|
|
806
|
+
|
|
807
|
+
def tricc_operation_isnottrue(self, ref_expressions):
|
|
808
|
+
return f"{ref_expressions[0]} != true"
|
|
809
|
+
|
|
810
|
+
def tricc_operation_isnotfalse(self, ref_expressions):
|
|
811
|
+
return f"{ref_expressions[0]} != false"
|
|
812
|
+
|
|
813
|
+
def tricc_operation_notexist(self, ref_expressions):
|
|
814
|
+
return f"!d2:hasValue({ref_expressions[0]})"
|
|
815
|
+
|
|
816
|
+
def tricc_operation_case(self, ref_expressions):
|
|
817
|
+
# Simplified case handling
|
|
818
|
+
parts = []
|
|
819
|
+
for i in range(0, len(ref_expressions), 2):
|
|
820
|
+
if i + 1 < len(ref_expressions):
|
|
821
|
+
parts.append(f"if({ref_expressions[i]}, {ref_expressions[i+1]})")
|
|
822
|
+
return " || ".join(parts)
|
|
823
|
+
|
|
824
|
+
def tricc_operation_ifs(self, ref_expressions):
|
|
825
|
+
return self.tricc_operation_case(ref_expressions[1:])
|
|
826
|
+
|
|
827
|
+
def tricc_operation_if(self, ref_expressions):
|
|
828
|
+
return f"if({ref_expressions[0]}, {ref_expressions[1]}, {ref_expressions[2]})"
|
|
829
|
+
|
|
830
|
+
def tricc_operation_contains(self, ref_expressions):
|
|
831
|
+
return f"d2:contains({ref_expressions[0]}, {ref_expressions[1]})"
|
|
832
|
+
|
|
833
|
+
def tricc_operation_exists(self, ref_expressions):
|
|
834
|
+
parts = []
|
|
835
|
+
for ref in ref_expressions:
|
|
836
|
+
parts.append(f"d2:hasValue({ref})")
|
|
837
|
+
return " && ".join(parts)
|
|
838
|
+
|
|
839
|
+
def tricc_operation_cast_number(self, ref_expressions):
|
|
840
|
+
return f"d2:toNumber({ref_expressions[0]})"
|
|
841
|
+
|
|
842
|
+
def tricc_operation_cast_integer(self, ref_expressions):
|
|
843
|
+
return f"d2:toNumber({ref_expressions[0]})"
|
|
844
|
+
|
|
845
|
+
def tricc_operation_zscore(self, ref_expressions):
|
|
846
|
+
# Placeholder - would need specific implementation
|
|
847
|
+
return f"zscore({','.join(ref_expressions)})"
|
|
848
|
+
|
|
849
|
+
def tricc_operation_datetime_to_decimal(self, ref_expressions):
|
|
850
|
+
return f"d2:daysBetween({ref_expressions[0]}, '1970-01-01')"
|
|
851
|
+
|
|
852
|
+
def tricc_operation_round(self, ref_expressions):
|
|
853
|
+
return f"d2:round({ref_expressions[0]})"
|
|
854
|
+
|
|
855
|
+
def tricc_operation_izscore(self, ref_expressions):
|
|
856
|
+
return f"izscore({','.join(ref_expressions)})"
|
|
857
|
+
|
|
858
|
+
def tricc_operation_concatenate(self, ref_expressions):
|
|
859
|
+
return f"d2:concatenate({','.join(ref_expressions)})"
|
|
@@ -69,6 +69,9 @@ class OpenMRSStrategy(BaseOutPutStrategy):
|
|
|
69
69
|
self.current_segment = None
|
|
70
70
|
self.current_activity = None
|
|
71
71
|
self.concept_map = {}
|
|
72
|
+
self.calculated_fields = [] # Store calculated fields to add to first section of each page
|
|
73
|
+
self.calculated_fields_added = set() # Track which pages have had calculated fields added
|
|
74
|
+
self.inject_version()
|
|
72
75
|
|
|
73
76
|
def get_export_name(self, r):
|
|
74
77
|
if isinstance(r, TriccNodeSelectOption):
|
|
@@ -146,6 +149,9 @@ class OpenMRSStrategy(BaseOutPutStrategy):
|
|
|
146
149
|
logger.info("generate the export format")
|
|
147
150
|
self.process_export(self.project.start_pages, pages=self.project.pages)
|
|
148
151
|
|
|
152
|
+
logger.info("create calculation page")
|
|
153
|
+
self.create_calculation_page()
|
|
154
|
+
|
|
149
155
|
logger.info("print the export")
|
|
150
156
|
self.export(self.project.start_pages, version=version)
|
|
151
157
|
|
|
@@ -250,7 +256,9 @@ class OpenMRSStrategy(BaseOutPutStrategy):
|
|
|
250
256
|
}
|
|
251
257
|
}
|
|
252
258
|
}
|
|
253
|
-
|
|
259
|
+
# Collect calculated fields to add to first section of each page
|
|
260
|
+
self.calculated_fields.append(question)
|
|
261
|
+
return None # Don't return the question, it will be added to first section
|
|
254
262
|
return None
|
|
255
263
|
|
|
256
264
|
def generate_calculate(self, node, processed_nodes, **kwargs):
|
|
@@ -439,7 +447,7 @@ class OpenMRSStrategy(BaseOutPutStrategy):
|
|
|
439
447
|
if len(ref_expressions) == 1:
|
|
440
448
|
return ref_expressions[0]
|
|
441
449
|
if len(ref_expressions) > 1:
|
|
442
|
-
return " || ".join(ref_expressions)
|
|
450
|
+
return "(" + " || ".join(ref_expressions) + ")"
|
|
443
451
|
else:
|
|
444
452
|
return "true"
|
|
445
453
|
|
|
@@ -472,7 +480,7 @@ class OpenMRSStrategy(BaseOutPutStrategy):
|
|
|
472
480
|
return f"({ref_expressions[0]}.includes({ref_expressions[1]}))"
|
|
473
481
|
|
|
474
482
|
def tricc_operation_count(self, ref_expressions):
|
|
475
|
-
return f"({ref_expressions[0]}.length)"
|
|
483
|
+
return f"({ref_expressions[0]}.length || 0)"
|
|
476
484
|
|
|
477
485
|
def tricc_operation_multiplied(self, ref_expressions):
|
|
478
486
|
return "*".join(ref_expressions)
|
|
@@ -638,6 +646,7 @@ class OpenMRSStrategy(BaseOutPutStrategy):
|
|
|
638
646
|
"label": section_label,
|
|
639
647
|
"questions": []
|
|
640
648
|
})
|
|
649
|
+
|
|
641
650
|
if group_node.relevance:
|
|
642
651
|
relevance_str = self.convert_expression_to_string(not_clean(group_node.relevance))
|
|
643
652
|
if relevance_str and relevance_str != 'false':
|
|
@@ -645,3 +654,38 @@ class OpenMRSStrategy(BaseOutPutStrategy):
|
|
|
645
654
|
"hideWhenExpression": f"{relevance_str}"
|
|
646
655
|
}
|
|
647
656
|
logger.debug(f"Started section: {section_label}")
|
|
657
|
+
|
|
658
|
+
def create_calculation_page(self):
|
|
659
|
+
"""Create a dedicated page for all calculated fields"""
|
|
660
|
+
if self.calculated_fields:
|
|
661
|
+
self.clean_sections()
|
|
662
|
+
self.clean_pages()
|
|
663
|
+
page = {
|
|
664
|
+
"label": "Calculations",
|
|
665
|
+
"sections": [
|
|
666
|
+
{
|
|
667
|
+
"label": "Calculations",
|
|
668
|
+
"questions": self.calculated_fields
|
|
669
|
+
}
|
|
670
|
+
]
|
|
671
|
+
}
|
|
672
|
+
self.form_data["pages"].append(page)
|
|
673
|
+
logger.debug("Created calculation page")
|
|
674
|
+
|
|
675
|
+
def inject_version(self):
|
|
676
|
+
# Add hidden version field using version() function
|
|
677
|
+
question = {
|
|
678
|
+
"id": "version",
|
|
679
|
+
"type": "control",
|
|
680
|
+
"label": "",
|
|
681
|
+
"hide": {
|
|
682
|
+
"hideWhenExpression": "true"
|
|
683
|
+
},
|
|
684
|
+
"questionOptions": {
|
|
685
|
+
"calculate": {
|
|
686
|
+
"calculateExpression": "version()"
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
# Collect calculated fields to add to first section of each page
|
|
691
|
+
self.calculated_fields.append(question)
|
|
@@ -399,7 +399,7 @@ class XLSFormStrategy(BaseOutPutStrategy):
|
|
|
399
399
|
# build lower level
|
|
400
400
|
if hasattr(self, f"tricc_operation_{operation.operator}"):
|
|
401
401
|
callable = getattr(self, f"tricc_operation_{operation.operator}")
|
|
402
|
-
return callable(ref_expressions)
|
|
402
|
+
return callable(list(map(str, ref_expressions)))
|
|
403
403
|
else:
|
|
404
404
|
raise NotImplementedError(
|
|
405
405
|
f"This type of opreation '{operation.operator}' is not supported in this strategy"
|
|
@@ -460,9 +460,9 @@ class XLSFormStrategy(BaseOutPutStrategy):
|
|
|
460
460
|
return "1"
|
|
461
461
|
|
|
462
462
|
def tricc_operation_native(self, ref_expressions):
|
|
463
|
-
|
|
463
|
+
|
|
464
464
|
if len(ref_expressions) > 0:
|
|
465
|
-
if ref_expressions[0].startswith(("'","`",)):
|
|
465
|
+
if ref_expressions[0].startswith(("'", "`",)):
|
|
466
466
|
ref_expressions[0] = ref_expressions[0][1:-1]
|
|
467
467
|
if ref_expressions[0] == "GetChoiceName":
|
|
468
468
|
return f"""jr:choice-name({
|
tricc_oo/visitors/tricc.py
CHANGED
|
@@ -1564,7 +1564,12 @@ def set_prev_next_node(source_node, target_node, replaced_node=None, edge_only=F
|
|
|
1564
1564
|
set_next_node(source_node, target_node, replaced_node, edge_only)
|
|
1565
1565
|
|
|
1566
1566
|
if activity and not any([(e.source == source_id) and (e.target == target_id) for e in activity.edges]):
|
|
1567
|
-
|
|
1567
|
+
if issubclass(source_node.__class__, TriccNodeSelect):
|
|
1568
|
+
label = "continue"
|
|
1569
|
+
elif isinstance(source_node, TriccNodeRhombus):
|
|
1570
|
+
label = "yes"
|
|
1571
|
+
else:
|
|
1572
|
+
label = None
|
|
1568
1573
|
activity.edges.append(TriccEdge(id=generate_id(), source=source_id, target=target_id, value=label))
|
|
1569
1574
|
|
|
1570
1575
|
|
|
@@ -1848,6 +1853,16 @@ def get_node_expression(in_node, processed_nodes, get_overall_exp=False, is_prev
|
|
|
1848
1853
|
logger.critical(f"Rhombus without expression {node.get_name()}")
|
|
1849
1854
|
elif is_prev and issubclass(node.__class__, TriccNodeDisplayCalculateBase):
|
|
1850
1855
|
expression = TriccOperation(TriccOperator.ISTRUE, [node])
|
|
1856
|
+
prev_exp_overall = get_node_expression(
|
|
1857
|
+
node,
|
|
1858
|
+
processed_nodes=processed_nodes,
|
|
1859
|
+
get_overall_exp=False,
|
|
1860
|
+
is_prev=False,
|
|
1861
|
+
process=process,
|
|
1862
|
+
negate=negate
|
|
1863
|
+
)
|
|
1864
|
+
if prev_exp_overall in [TriccStatic(True), TriccStatic(False)]:
|
|
1865
|
+
expression = prev_exp_overall
|
|
1851
1866
|
elif hasattr(node, "expression_reference") and isinstance(node.expression_reference, TriccOperation):
|
|
1852
1867
|
# if issubclass(node.__class__, TriccNodeDisplayCalculateBase):
|
|
1853
1868
|
# expression = TriccOperation(
|
|
@@ -2241,7 +2256,10 @@ def get_count_terms_details(prev_node, processed_nodes, get_overall_exp, negate=
|
|
|
2241
2256
|
get_node_expression(
|
|
2242
2257
|
prev_node,
|
|
2243
2258
|
processed_nodes=processed_nodes,
|
|
2244
|
-
get_overall_exp=
|
|
2259
|
+
get_overall_exp=not issubclass(
|
|
2260
|
+
prev_node.__class__,
|
|
2261
|
+
(TriccNodeBridge, TriccNodeDisplayBridge)
|
|
2262
|
+
),
|
|
2245
2263
|
is_prev=True,
|
|
2246
2264
|
process=process,
|
|
2247
2265
|
)
|
|
@@ -2279,7 +2297,10 @@ def get_add_terms(node, processed_nodes, get_overall_exp=False, negate=False, pr
|
|
|
2279
2297
|
get_node_expression(
|
|
2280
2298
|
prev_node,
|
|
2281
2299
|
processed_nodes=processed_nodes,
|
|
2282
|
-
get_overall_exp=
|
|
2300
|
+
get_overall_exp=not issubclass(
|
|
2301
|
+
prev_node.__class__,
|
|
2302
|
+
(TriccNodeBridge, TriccNodeDisplayBridge)
|
|
2303
|
+
),
|
|
2283
2304
|
is_prev=True,
|
|
2284
2305
|
process=process,
|
|
2285
2306
|
)
|
|
@@ -2352,7 +2373,11 @@ def get_rhombus_terms(node, processed_nodes, get_overall_exp=False, negate=False
|
|
|
2352
2373
|
TriccOperator.CAST_NUMBER,
|
|
2353
2374
|
[
|
|
2354
2375
|
get_node_expression(
|
|
2355
|
-
expression,
|
|
2376
|
+
expression,
|
|
2377
|
+
processed_nodes=processed_nodes,
|
|
2378
|
+
get_overall_exp=not issubclass(expression.__class__, (TriccNodeBridge, TriccNodeDisplayBridge)),
|
|
2379
|
+
is_prev=True,
|
|
2380
|
+
process=process
|
|
2356
2381
|
)
|
|
2357
2382
|
],
|
|
2358
2383
|
)
|
|
@@ -2420,7 +2445,7 @@ def get_calculation_terms(node, processed_nodes, get_overall_exp=False, negate=F
|
|
|
2420
2445
|
return get_node_expression(
|
|
2421
2446
|
node_to_negate,
|
|
2422
2447
|
processed_nodes=processed_nodes,
|
|
2423
|
-
get_overall_exp=
|
|
2448
|
+
get_overall_exp=not issubclass(node_to_negate.__class__, (TriccNodeBridge, TriccNodeDisplayBridge)),
|
|
2424
2449
|
is_prev=True,
|
|
2425
2450
|
negate=True,
|
|
2426
2451
|
process=process,
|
|
@@ -2429,7 +2454,7 @@ def get_calculation_terms(node, processed_nodes, get_overall_exp=False, negate=F
|
|
|
2429
2454
|
return get_node_expression(
|
|
2430
2455
|
node_to_negate,
|
|
2431
2456
|
processed_nodes=processed_nodes,
|
|
2432
|
-
get_overall_exp=
|
|
2457
|
+
get_overall_exp=not issubclass(node_to_negate.__class__, (TriccNodeBridge, TriccNodeDisplayBridge)),
|
|
2433
2458
|
is_prev=True,
|
|
2434
2459
|
negate=True,
|
|
2435
2460
|
process=process,
|
|
@@ -2613,10 +2638,20 @@ def generate_base(node, processed_nodes, **kwargs):
|
|
|
2613
2638
|
constraints_min = ""
|
|
2614
2639
|
constraints_max = ""
|
|
2615
2640
|
if node.min is not None and node.min != "":
|
|
2616
|
-
|
|
2641
|
+
node.min = float(node.min)
|
|
2642
|
+
if int(node.min) == node.min:
|
|
2643
|
+
node.min = int(node.min)
|
|
2644
|
+
constraints.append(
|
|
2645
|
+
TriccOperation(TriccOperator.MORE_OR_EQUAL, ["$this", TriccStatic(node.min)])
|
|
2646
|
+
)
|
|
2617
2647
|
constraints_min = "The minimun value is {0}.".format(node.min)
|
|
2618
2648
|
if node.max is not None and node.max != "":
|
|
2619
|
-
|
|
2649
|
+
node.max = float(node.max)
|
|
2650
|
+
if int(node.max) == node.max:
|
|
2651
|
+
node.max = int(node.max)
|
|
2652
|
+
constraints.append(
|
|
2653
|
+
TriccOperation(TriccOperator.LESS_OR_EQUAL, ["$this", TriccStatic(node.max)])
|
|
2654
|
+
)
|
|
2620
2655
|
constraints_max = "The maximum value is {0}.".format(node.max)
|
|
2621
2656
|
if len(constraints) > 1:
|
|
2622
2657
|
node.constraint = TriccOperation(TriccOperator.AND, constraints)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
tests/build.py,sha256=
|
|
1
|
+
tests/build.py,sha256=Qbxvjkj_Wk2nQ-WjaMGiE1FIe3SRmJMRIgeoMoxqlfQ,6748
|
|
2
2
|
tests/test_cql.py,sha256=dAsLMqVaS6qxnq62fg5KqTFu6UG6pHO6Ab3NZ1c9T3Y,5248
|
|
3
3
|
tests/to_ocl.py,sha256=4e-i65K3UM6wHgdVcrZcM9AyL1bahIsXJiZTXhhHgQk,2048
|
|
4
4
|
tricc_oo/__init__.py,sha256=oWCE1ubmC_6iqaWOMgTei4eXVQgV202Ia-tXS1NnW_4,139
|
|
@@ -7,7 +7,7 @@ tricc_oo/converters/codesystem_to_ocl.py,sha256=Fh7Vk73OsxljZKu1k6H9uzYwz334tpQT
|
|
|
7
7
|
tricc_oo/converters/cql_to_operation.py,sha256=PUyV_YpUY98Ox0H_F_CN3UUf_I-BhFZVOcWWKTtwecM,14492
|
|
8
8
|
tricc_oo/converters/datadictionnary.py,sha256=T2HLCBo4Am1p0kFqSH1r0PqbD8AC2IGuWkbvMvSCru0,3658
|
|
9
9
|
tricc_oo/converters/drawio_type_map.py,sha256=UCPiGs7Lw0bigKScmZUnmOhACBz-FiDq92jHkI7RTSQ,9113
|
|
10
|
-
tricc_oo/converters/tricc_to_xls_form.py,sha256=
|
|
10
|
+
tricc_oo/converters/tricc_to_xls_form.py,sha256=C7oDi1_IA0FLjoK700G9bABQZrGEqJltvBD2rGVjkIA,3494
|
|
11
11
|
tricc_oo/converters/utils.py,sha256=JZrtrvvOfXwdkw49pKauzinOcauWwsy-CVcw36TjyLo,1684
|
|
12
12
|
tricc_oo/converters/xml_to_tricc.py,sha256=cDoLTwIMHIqyyNqZGwQte9YdX4y5j1Ac6r7M-zuKWZc,39403
|
|
13
13
|
tricc_oo/converters/cql/cqlLexer.py,sha256=8HArbRphcrpnAG4uogJ2rHv4tc1WLzjN0B1uFeYILAc,49141
|
|
@@ -15,7 +15,7 @@ tricc_oo/converters/cql/cqlListener.py,sha256=fA7-8DcS2Q69ckwjdg57-OfFHBxjTZFdoS
|
|
|
15
15
|
tricc_oo/converters/cql/cqlParser.py,sha256=x3KdrwX9nwENSEJ5Ex7_l5NMnu3kWBO0uLdYu4moTq0,414745
|
|
16
16
|
tricc_oo/converters/cql/cqlVisitor.py,sha256=iHuup2S7OGSVWLEcI4H3oecRqgXztC1sKnew_1P2iGY,33880
|
|
17
17
|
tricc_oo/models/__init__.py,sha256=CgS52LLqdDIaXHvZy08hhu_VaYw80OEdfL_llM9ICBA,108
|
|
18
|
-
tricc_oo/models/base.py,sha256=
|
|
18
|
+
tricc_oo/models/base.py,sha256=AaeB69vWg3ulBttoezEniUiU8HWh_pvrw7tCHpL27g4,25926
|
|
19
19
|
tricc_oo/models/calculate.py,sha256=uNP0IDUqPQcJq9Co05H8eX5wbR_DikSxuOHxfVE5Dxg,8018
|
|
20
20
|
tricc_oo/models/lang.py,sha256=ZMRwdoPWe01wEDhOM0uRk-6rt3BkoAAZM8mZ61--s3A,2265
|
|
21
21
|
tricc_oo/models/ocl.py,sha256=MybSeB6fgCOUVJ4aektff0vrrTZsyfwZ2Gt_pPBu_FY,8728
|
|
@@ -31,20 +31,21 @@ tricc_oo/strategies/input/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJW
|
|
|
31
31
|
tricc_oo/strategies/input/base_input_strategy.py,sha256=BEODXS74na1QRRcJVQ4cxiD8F7uRqaLyhE3QzKpGVvk,3891
|
|
32
32
|
tricc_oo/strategies/input/drawio.py,sha256=uXAUPhXOeg0Uk_BNqlCqFBW4cWNox4VfH559bj1fhC0,12767
|
|
33
33
|
tricc_oo/strategies/output/base_output_strategy.py,sha256=M9UFR67-_CFoW681bPAeBS1OUGuFtmUbM_rltACI0hk,8798
|
|
34
|
+
tricc_oo/strategies/output/dhis2_form.py,sha256=m23EeZB7uXkNCFJr9hNGevgsA1dQqBrtX5uoy_22QRI,36500
|
|
34
35
|
tricc_oo/strategies/output/fhir_form.py,sha256=hbL921pe1Doun4IQrJuZ_Sq2fCh98G3grYie5olC4uc,15740
|
|
35
36
|
tricc_oo/strategies/output/html_form.py,sha256=qSleEZOMV_-Z04y-i-ucyd5rgAYWAyjPwMrw0IHtCRM,8604
|
|
36
|
-
tricc_oo/strategies/output/openmrs_form.py,sha256=
|
|
37
|
+
tricc_oo/strategies/output/openmrs_form.py,sha256=YT5nSEHK-XYOGobCAkoz3mJHcU5N4yjK2UC5fafrZgc,28850
|
|
37
38
|
tricc_oo/strategies/output/spice.py,sha256=QMeoismVC3PdbvwTK0PtUjWX9jl9780fbQIXn76fMXw,10761
|
|
38
|
-
tricc_oo/strategies/output/xls_form.py,sha256=
|
|
39
|
+
tricc_oo/strategies/output/xls_form.py,sha256=26pEea0I_owpsz9S8hoHJNzChA5b2Th8KPRIeTEMfqo,29323
|
|
39
40
|
tricc_oo/strategies/output/xlsform_cdss.py,sha256=X00Lt5MzV8TX14dR4dFI1MqllI5S1e13bKbeysWM9uA,17435
|
|
40
41
|
tricc_oo/strategies/output/xlsform_cht.py,sha256=RY_mre9j6w2vVnRFSGn5R3CuTWFjIbQyl1uWwz9Ay5E,22965
|
|
41
42
|
tricc_oo/strategies/output/xlsform_cht_hf.py,sha256=xm6SKirV3nMZvM2w54_zJcXAeAgAkq-EEqGEjnOWv6c,988
|
|
42
43
|
tricc_oo/visitors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
43
|
-
tricc_oo/visitors/tricc.py,sha256=
|
|
44
|
+
tricc_oo/visitors/tricc.py,sha256=xpV5eeSwC6DJdz0GgZTg1lLoWu-iAl8SVVwIwrSNSYc,108722
|
|
44
45
|
tricc_oo/visitors/utils.py,sha256=j83aAq5s5atXi3OC0jc_uJd54a8XrHHmizeeEbWZQJg,421
|
|
45
46
|
tricc_oo/visitors/xform_pd.py,sha256=ryAnI3V9x3eTmJ2LNsUZfvl0_yfCqo6oBgeSu-WPqaE,9613
|
|
46
|
-
tricc_oo-1.5.
|
|
47
|
-
tricc_oo-1.5.
|
|
48
|
-
tricc_oo-1.5.
|
|
49
|
-
tricc_oo-1.5.
|
|
50
|
-
tricc_oo-1.5.
|
|
47
|
+
tricc_oo-1.5.29.dist-info/licenses/LICENSE,sha256=Pz2eACSxkhsGfW9_iN60pgy-enjnbGTj8df8O3ebnQQ,16726
|
|
48
|
+
tricc_oo-1.5.29.dist-info/METADATA,sha256=cMirGPvaXWJ0SWON_2DkgUHqqJQiC-poK8r8M_fNMlU,8577
|
|
49
|
+
tricc_oo-1.5.29.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
50
|
+
tricc_oo-1.5.29.dist-info/top_level.txt,sha256=NvbfMNAiy9m4b1unBsqpeOQWh4IgA1Xa33BtKA4abxk,15
|
|
51
|
+
tricc_oo-1.5.29.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|