tricc-oo 1.5.13__py3-none-any.whl → 1.6.8__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 +20 -28
- tests/test_build.py +260 -0
- tests/test_cql.py +48 -109
- tests/to_ocl.py +15 -17
- tricc_oo/__init__.py +0 -6
- tricc_oo/converters/codesystem_to_ocl.py +51 -40
- tricc_oo/converters/cql/cqlLexer.py +1 -0
- tricc_oo/converters/cql/cqlListener.py +1 -0
- tricc_oo/converters/cql/cqlParser.py +1 -0
- tricc_oo/converters/cql/cqlVisitor.py +1 -0
- tricc_oo/converters/cql_to_operation.py +129 -123
- tricc_oo/converters/datadictionnary.py +45 -54
- tricc_oo/converters/drawio_type_map.py +146 -65
- tricc_oo/converters/tricc_to_xls_form.py +58 -28
- tricc_oo/converters/utils.py +4 -4
- tricc_oo/converters/xml_to_tricc.py +296 -235
- tricc_oo/models/__init__.py +2 -1
- tricc_oo/models/base.py +333 -305
- tricc_oo/models/calculate.py +66 -51
- tricc_oo/models/lang.py +26 -27
- tricc_oo/models/ocl.py +146 -161
- tricc_oo/models/ordered_set.py +15 -19
- tricc_oo/models/tricc.py +149 -89
- tricc_oo/parsers/xml.py +15 -30
- tricc_oo/serializers/planuml.py +4 -6
- tricc_oo/serializers/xls_form.py +110 -153
- tricc_oo/strategies/input/base_input_strategy.py +28 -32
- tricc_oo/strategies/input/drawio.py +59 -71
- tricc_oo/strategies/output/base_output_strategy.py +151 -65
- tricc_oo/strategies/output/dhis2_form.py +908 -0
- tricc_oo/strategies/output/fhir_form.py +377 -0
- tricc_oo/strategies/output/html_form.py +224 -0
- tricc_oo/strategies/output/openmrs_form.py +694 -0
- tricc_oo/strategies/output/spice.py +106 -127
- tricc_oo/strategies/output/xls_form.py +322 -244
- tricc_oo/strategies/output/xlsform_cdss.py +627 -142
- tricc_oo/strategies/output/xlsform_cht.py +252 -125
- tricc_oo/strategies/output/xlsform_cht_hf.py +13 -24
- tricc_oo/visitors/tricc.py +1424 -1033
- tricc_oo/visitors/utils.py +16 -16
- tricc_oo/visitors/xform_pd.py +91 -89
- {tricc_oo-1.5.13.dist-info → tricc_oo-1.6.8.dist-info}/METADATA +128 -84
- tricc_oo-1.6.8.dist-info/RECORD +52 -0
- tricc_oo-1.6.8.dist-info/licenses/LICENSE +373 -0
- {tricc_oo-1.5.13.dist-info → tricc_oo-1.6.8.dist-info}/top_level.txt +0 -0
- tricc_oo-1.5.13.dist-info/RECORD +0 -46
- {tricc_oo-1.5.13.dist-info → tricc_oo-1.6.8.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import json
|
|
4
|
+
import datetime
|
|
5
|
+
from tricc_oo.strategies.output.base_output_strategy import BaseOutPutStrategy
|
|
6
|
+
from tricc_oo.models.base import (
|
|
7
|
+
TriccOperation,
|
|
8
|
+
TriccStatic, TriccReference
|
|
9
|
+
)
|
|
10
|
+
from tricc_oo.models.tricc import (
|
|
11
|
+
TriccNodeSelectOption,
|
|
12
|
+
TriccNodeInputModel,
|
|
13
|
+
TriccNodeBaseModel
|
|
14
|
+
)
|
|
15
|
+
from tricc_oo.converters.tricc_to_xls_form import get_export_name
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger("default")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FHIRStrategy(BaseOutPutStrategy):
|
|
21
|
+
processes = ["main"]
|
|
22
|
+
project = None
|
|
23
|
+
output_path = None
|
|
24
|
+
|
|
25
|
+
def __init__(self, project, output_path):
|
|
26
|
+
super().__init__(project, output_path)
|
|
27
|
+
self.questionnaires = {} # segment -> questionnaire
|
|
28
|
+
self.cql_libraries = {}
|
|
29
|
+
self.fml_mappings = {}
|
|
30
|
+
|
|
31
|
+
def get_tricc_operation_expression(self, operation):
|
|
32
|
+
# For CQL
|
|
33
|
+
ref_expressions = []
|
|
34
|
+
if not hasattr(operation, "reference"):
|
|
35
|
+
return self.get_tricc_operation_operand(operation)
|
|
36
|
+
for r in operation.reference:
|
|
37
|
+
if isinstance(r, list):
|
|
38
|
+
r_expr = [
|
|
39
|
+
(
|
|
40
|
+
self.get_tricc_operation_expression(sr)
|
|
41
|
+
if isinstance(sr, TriccOperation)
|
|
42
|
+
else self.get_tricc_operation_operand(sr)
|
|
43
|
+
)
|
|
44
|
+
for sr in r
|
|
45
|
+
]
|
|
46
|
+
elif isinstance(r, TriccOperation):
|
|
47
|
+
r_expr = self.get_tricc_operation_expression(r)
|
|
48
|
+
else:
|
|
49
|
+
r_expr = self.get_tricc_operation_operand(r)
|
|
50
|
+
if isinstance(r_expr, TriccReference):
|
|
51
|
+
r_expr = self.get_tricc_operation_operand(r_expr)
|
|
52
|
+
ref_expressions.append(r_expr)
|
|
53
|
+
|
|
54
|
+
# build lower level
|
|
55
|
+
if hasattr(self, f"tricc_operation_{operation.operator}"):
|
|
56
|
+
callable = getattr(self, f"tricc_operation_{operation.operator}")
|
|
57
|
+
return callable(ref_expressions)
|
|
58
|
+
else:
|
|
59
|
+
raise NotImplementedError(
|
|
60
|
+
f"This type of operation '{operation.operator}' is not supported in this strategy"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def execute(self):
|
|
64
|
+
version = datetime.datetime.now().strftime("%Y%m%d%H%M")
|
|
65
|
+
logger.info(f"build version: {version}")
|
|
66
|
+
if "main" in self.project.start_pages:
|
|
67
|
+
self.process_base(self.project.start_pages, pages=self.project.pages, version=version)
|
|
68
|
+
else:
|
|
69
|
+
logger.critical("Main process required")
|
|
70
|
+
|
|
71
|
+
logger.info("generate the relevance based on edges")
|
|
72
|
+
self.process_relevance(self.project.start_pages, pages=self.project.pages)
|
|
73
|
+
|
|
74
|
+
logger.info("generate the calculate based on edges")
|
|
75
|
+
self.process_calculate(self.project.start_pages, pages=self.project.pages)
|
|
76
|
+
|
|
77
|
+
logger.info("generate the export format")
|
|
78
|
+
self.process_export(self.project.start_pages, pages=self.project.pages)
|
|
79
|
+
|
|
80
|
+
logger.info("print the export")
|
|
81
|
+
self.export(self.project.start_pages, version=version)
|
|
82
|
+
|
|
83
|
+
def generate_base(self, node, **kwargs):
|
|
84
|
+
# Generate Questionnaire items per segment
|
|
85
|
+
segment = getattr(node, 'segment', 'main')
|
|
86
|
+
if segment not in self.questionnaires:
|
|
87
|
+
self.questionnaires[segment] = {
|
|
88
|
+
"resourceType": "Questionnaire",
|
|
89
|
+
"id": f"questionnaire-{segment}",
|
|
90
|
+
"url": f"http://example.com/Questionnaire/{segment}",
|
|
91
|
+
"status": "draft",
|
|
92
|
+
"item": []
|
|
93
|
+
}
|
|
94
|
+
item = {
|
|
95
|
+
"linkId": get_export_name(node),
|
|
96
|
+
"text": getattr(node, 'label', ''),
|
|
97
|
+
"type": self.map_tricc_type_to_fhir(node.tricc_type if hasattr(node, 'tricc_type') else 'text')
|
|
98
|
+
}
|
|
99
|
+
if hasattr(node, 'options') and node.options:
|
|
100
|
+
item["answerOption"] = [{"valueString": opt.name} for opt in node.options]
|
|
101
|
+
self.questionnaires[segment]["item"].append(item)
|
|
102
|
+
return True
|
|
103
|
+
|
|
104
|
+
def generate_relevance(self, node, **kwargs):
|
|
105
|
+
# Add enableWhen to Questionnaire item with FHIRPath
|
|
106
|
+
if hasattr(node, 'expression') and node.expression:
|
|
107
|
+
segment = getattr(node, 'segment', 'main')
|
|
108
|
+
if segment in self.questionnaires:
|
|
109
|
+
for item in self.questionnaires[segment]["item"]:
|
|
110
|
+
if item["linkId"] == get_export_name(node):
|
|
111
|
+
# Use FHIRPath expression
|
|
112
|
+
fhirpath_expr = self.convert_expression_to_fhirpath(node.expression)
|
|
113
|
+
item["enableWhen"] = [{
|
|
114
|
+
"question": self.get_question_link(node.expression),
|
|
115
|
+
"operator": "=",
|
|
116
|
+
"answerString": self.get_answer_value(node.expression)
|
|
117
|
+
}]
|
|
118
|
+
# Alternatively, use expression for complex logic
|
|
119
|
+
item["enableWhenExpression"] = {
|
|
120
|
+
"language": "text/fhirpath",
|
|
121
|
+
"expression": fhirpath_expr
|
|
122
|
+
}
|
|
123
|
+
break
|
|
124
|
+
return True
|
|
125
|
+
|
|
126
|
+
def generate_calculate(self, node, **kwargs):
|
|
127
|
+
# Add calculatedExpression to Questionnaire item with FHIRPath
|
|
128
|
+
if hasattr(node, 'expression') and node.expression:
|
|
129
|
+
segment = getattr(node, 'segment', 'main')
|
|
130
|
+
if segment in self.questionnaires:
|
|
131
|
+
for item in self.questionnaires[segment]["item"]:
|
|
132
|
+
if item["linkId"] == get_export_name(node):
|
|
133
|
+
fhirpath_expr = self.convert_expression_to_fhirpath(node.expression)
|
|
134
|
+
item["calculatedExpression"] = {
|
|
135
|
+
"language": "text/fhirpath",
|
|
136
|
+
"expression": fhirpath_expr
|
|
137
|
+
}
|
|
138
|
+
break
|
|
139
|
+
# Still add to CQL for population if needed
|
|
140
|
+
if segment not in self.cql_libraries:
|
|
141
|
+
self.cql_libraries[segment] = f"library {segment}Library version '1.0.0'\n\n"
|
|
142
|
+
cql_expr = self.convert_expression_to_cql(node.expression)
|
|
143
|
+
self.cql_libraries[segment] += f"define {get_export_name(node)}: {cql_expr}\n"
|
|
144
|
+
return True
|
|
145
|
+
|
|
146
|
+
def generate_export(self, node, **kwargs):
|
|
147
|
+
# Generate FML for saving based on content_type
|
|
148
|
+
content_type = getattr(node, 'content_type', 'Observation')
|
|
149
|
+
if content_type not in self.fml_mappings:
|
|
150
|
+
self.fml_mappings[content_type] = f"map \"{content_type}\" {{\n"
|
|
151
|
+
# Add mapping rules
|
|
152
|
+
self.fml_mappings[content_type] += f" {get_export_name(node)} -> {content_type}.{get_export_name(node)}\n"
|
|
153
|
+
return True
|
|
154
|
+
|
|
155
|
+
def export(self, start_pages, version):
|
|
156
|
+
form_id = start_pages["main"].root.form_id or "fhir_form"
|
|
157
|
+
base_path = os.path.join(self.output_path, form_id)
|
|
158
|
+
if not os.path.exists(base_path):
|
|
159
|
+
os.makedirs(base_path)
|
|
160
|
+
|
|
161
|
+
# Export Questionnaires
|
|
162
|
+
for segment, q in self.questionnaires.items():
|
|
163
|
+
file_name = f"{segment}.json"
|
|
164
|
+
path = os.path.join(base_path, file_name)
|
|
165
|
+
with open(path, 'w') as f:
|
|
166
|
+
json.dump(q, f, indent=2)
|
|
167
|
+
|
|
168
|
+
# Export CQL
|
|
169
|
+
for segment, cql in self.cql_libraries.items():
|
|
170
|
+
file_name = f"{segment}.cql"
|
|
171
|
+
path = os.path.join(base_path, file_name)
|
|
172
|
+
with open(path, 'w') as f:
|
|
173
|
+
f.write(cql)
|
|
174
|
+
|
|
175
|
+
# Export FML
|
|
176
|
+
for content_type, fml in self.fml_mappings.items():
|
|
177
|
+
fml += "}\n"
|
|
178
|
+
file_name = f"{content_type}.map"
|
|
179
|
+
path = os.path.join(base_path, file_name)
|
|
180
|
+
with open(path, 'w') as f:
|
|
181
|
+
f.write(fml)
|
|
182
|
+
|
|
183
|
+
logger.info(f"Exported FHIR resources to {base_path}")
|
|
184
|
+
|
|
185
|
+
def map_tricc_type_to_fhir(self, tricc_type):
|
|
186
|
+
mapping = {
|
|
187
|
+
'text': 'string',
|
|
188
|
+
'integer': 'integer',
|
|
189
|
+
'decimal': 'decimal',
|
|
190
|
+
'select_one': 'choice',
|
|
191
|
+
'select_multiple': 'choice',
|
|
192
|
+
'date': 'date',
|
|
193
|
+
'time': 'time',
|
|
194
|
+
'datetime': 'dateTime',
|
|
195
|
+
'boolean': 'boolean'
|
|
196
|
+
}
|
|
197
|
+
return mapping.get(tricc_type, 'string')
|
|
198
|
+
|
|
199
|
+
def get_question_link(self, expression):
|
|
200
|
+
# Simplified, assume first reference
|
|
201
|
+
if isinstance(expression, TriccOperation) and hasattr(expression, 'reference'):
|
|
202
|
+
for r in expression.reference:
|
|
203
|
+
if isinstance(r, TriccReference):
|
|
204
|
+
return get_export_name(r.value)
|
|
205
|
+
return ""
|
|
206
|
+
|
|
207
|
+
def get_answer_value(self, expression):
|
|
208
|
+
# Simplified
|
|
209
|
+
return "true"
|
|
210
|
+
|
|
211
|
+
def get_tricc_operation_operand(self, r):
|
|
212
|
+
if isinstance(r, TriccOperation):
|
|
213
|
+
return self.get_tricc_operation_expression(r)
|
|
214
|
+
elif isinstance(r, TriccReference):
|
|
215
|
+
return get_export_name(r.value)
|
|
216
|
+
elif isinstance(r, TriccStatic):
|
|
217
|
+
if isinstance(r.value, str):
|
|
218
|
+
return f"'{r.value}'"
|
|
219
|
+
else:
|
|
220
|
+
return str(r.value)
|
|
221
|
+
elif isinstance(r, str):
|
|
222
|
+
return f"'{r}'"
|
|
223
|
+
elif isinstance(r, (int, float)):
|
|
224
|
+
return str(r)
|
|
225
|
+
elif isinstance(r, TriccNodeSelectOption):
|
|
226
|
+
return f"'{r.name}'"
|
|
227
|
+
elif issubclass(r.__class__, TriccNodeInputModel):
|
|
228
|
+
return get_export_name(r)
|
|
229
|
+
elif issubclass(r.__class__, TriccNodeBaseModel):
|
|
230
|
+
return get_export_name(r)
|
|
231
|
+
else:
|
|
232
|
+
raise NotImplementedError(f"This type of node {r.__class__} is not supported within an operation")
|
|
233
|
+
|
|
234
|
+
def convert_expression_to_cql(self, expression):
|
|
235
|
+
if isinstance(expression, TriccOperation):
|
|
236
|
+
return self.get_tricc_operation_expression(expression)
|
|
237
|
+
else:
|
|
238
|
+
return self.get_tricc_operation_operand(expression)
|
|
239
|
+
|
|
240
|
+
def convert_expression_to_fhirpath(self, expression):
|
|
241
|
+
# For FHIRPath, similar to CQL but in FHIR context
|
|
242
|
+
# For questionnaire, references to other questions
|
|
243
|
+
if isinstance(expression, TriccOperation):
|
|
244
|
+
return self.get_tricc_operation_expression_fhirpath(expression)
|
|
245
|
+
else:
|
|
246
|
+
return self.get_tricc_operation_operand_fhirpath(expression)
|
|
247
|
+
|
|
248
|
+
def get_tricc_operation_expression_fhirpath(self, operation):
|
|
249
|
+
ref_expressions = []
|
|
250
|
+
if not hasattr(operation, "reference"):
|
|
251
|
+
return self.get_tricc_operation_operand_fhirpath(operation)
|
|
252
|
+
for r in operation.reference:
|
|
253
|
+
if isinstance(r, list):
|
|
254
|
+
r_expr = [
|
|
255
|
+
(
|
|
256
|
+
self.get_tricc_operation_expression_fhirpath(sr)
|
|
257
|
+
if isinstance(sr, TriccOperation)
|
|
258
|
+
else self.get_tricc_operation_operand_fhirpath(sr)
|
|
259
|
+
)
|
|
260
|
+
for sr in r
|
|
261
|
+
]
|
|
262
|
+
elif isinstance(r, TriccOperation):
|
|
263
|
+
r_expr = self.get_tricc_operation_expression_fhirpath(r)
|
|
264
|
+
else:
|
|
265
|
+
r_expr = self.get_tricc_operation_operand_fhirpath(r)
|
|
266
|
+
if isinstance(r_expr, TriccReference):
|
|
267
|
+
r_expr = self.get_tricc_operation_operand_fhirpath(r_expr)
|
|
268
|
+
ref_expressions.append(r_expr)
|
|
269
|
+
|
|
270
|
+
if hasattr(self, f"tricc_operation_fhirpath_{operation.operator}"):
|
|
271
|
+
callable = getattr(self, f"tricc_operation_fhirpath_{operation.operator}")
|
|
272
|
+
return callable(ref_expressions)
|
|
273
|
+
else:
|
|
274
|
+
# Fallback to CQL operations
|
|
275
|
+
if hasattr(self, f"tricc_operation_{operation.operator}"):
|
|
276
|
+
callable = getattr(self, f"tricc_operation_{operation.operator}")
|
|
277
|
+
return callable(ref_expressions)
|
|
278
|
+
else:
|
|
279
|
+
raise NotImplementedError(
|
|
280
|
+
f"This type of operation '{operation.operator}' is not supported"
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
def get_tricc_operation_operand_fhirpath(self, r):
|
|
284
|
+
if isinstance(r, TriccOperation):
|
|
285
|
+
return self.get_tricc_operation_expression_fhirpath(r)
|
|
286
|
+
elif isinstance(r, TriccReference):
|
|
287
|
+
# In FHIRPath, reference to another question's answer
|
|
288
|
+
return f"%questionnaire.item.where(linkId='{get_export_name(r.value)}').answer.value"
|
|
289
|
+
elif isinstance(r, TriccStatic):
|
|
290
|
+
if isinstance(r.value, str):
|
|
291
|
+
return f"'{r.value}'"
|
|
292
|
+
else:
|
|
293
|
+
return str(r.value)
|
|
294
|
+
elif isinstance(r, str):
|
|
295
|
+
return f"'{r}'"
|
|
296
|
+
elif isinstance(r, (int, float)):
|
|
297
|
+
return str(r)
|
|
298
|
+
elif isinstance(r, TriccNodeSelectOption):
|
|
299
|
+
return f"'{r.name}'"
|
|
300
|
+
elif issubclass(r.__class__, TriccNodeInputModel):
|
|
301
|
+
return f"%questionnaire.item.where(linkId='{get_export_name(r)}').answer.value"
|
|
302
|
+
elif issubclass(r.__class__, TriccNodeBaseModel):
|
|
303
|
+
return f"%questionnaire.item.where(linkId='{get_export_name(r)}').answer.value"
|
|
304
|
+
else:
|
|
305
|
+
raise NotImplementedError(f"This type of node {r.__class__} is not supported within an operation")
|
|
306
|
+
|
|
307
|
+
# FHIRPath operations, same as CQL for now
|
|
308
|
+
def tricc_operation_fhirpath_equal(self, ref_expressions):
|
|
309
|
+
return f"{ref_expressions[0]} = {ref_expressions[1]}"
|
|
310
|
+
|
|
311
|
+
def tricc_operation_fhirpath_not_equal(self, ref_expressions):
|
|
312
|
+
return f"{ref_expressions[0]} != {ref_expressions[1]}"
|
|
313
|
+
|
|
314
|
+
def tricc_operation_fhirpath_and(self, ref_expressions):
|
|
315
|
+
return " and ".join(ref_expressions)
|
|
316
|
+
|
|
317
|
+
def tricc_operation_fhirpath_or(self, ref_expressions):
|
|
318
|
+
return " or ".join(ref_expressions)
|
|
319
|
+
|
|
320
|
+
def tricc_operation_fhirpath_not(self, ref_expressions):
|
|
321
|
+
return f"not {ref_expressions[0]}"
|
|
322
|
+
|
|
323
|
+
def tricc_operation_fhirpath_plus(self, ref_expressions):
|
|
324
|
+
return " + ".join(ref_expressions)
|
|
325
|
+
|
|
326
|
+
def tricc_operation_fhirpath_minus(self, ref_expressions):
|
|
327
|
+
if len(ref_expressions) > 1:
|
|
328
|
+
return " - ".join(ref_expressions)
|
|
329
|
+
return f"-{ref_expressions[0]}"
|
|
330
|
+
|
|
331
|
+
def tricc_operation_fhirpath_more(self, ref_expressions):
|
|
332
|
+
return f"{ref_expressions[0]} > {ref_expressions[1]}"
|
|
333
|
+
|
|
334
|
+
def tricc_operation_fhirpath_less(self, ref_expressions):
|
|
335
|
+
return f"{ref_expressions[0]} < {ref_expressions[1]}"
|
|
336
|
+
|
|
337
|
+
def tricc_operation_fhirpath_more_or_equal(self, ref_expressions):
|
|
338
|
+
return f"{ref_expressions[0]} >= {ref_expressions[1]}"
|
|
339
|
+
|
|
340
|
+
def tricc_operation_fhirpath_less_or_equal(self, ref_expressions):
|
|
341
|
+
return f"{ref_expressions[0]} <= {ref_expressions[1]}"
|
|
342
|
+
|
|
343
|
+
# Operation methods for CQL
|
|
344
|
+
def tricc_operation_equal(self, ref_expressions):
|
|
345
|
+
return f"{ref_expressions[0]} = {ref_expressions[1]}"
|
|
346
|
+
|
|
347
|
+
def tricc_operation_not_equal(self, ref_expressions):
|
|
348
|
+
return f"{ref_expressions[0]} != {ref_expressions[1]}"
|
|
349
|
+
|
|
350
|
+
def tricc_operation_and(self, ref_expressions):
|
|
351
|
+
return " and ".join(ref_expressions)
|
|
352
|
+
|
|
353
|
+
def tricc_operation_or(self, ref_expressions):
|
|
354
|
+
return " or ".join(ref_expressions)
|
|
355
|
+
|
|
356
|
+
def tricc_operation_not(self, ref_expressions):
|
|
357
|
+
return f"not {ref_expressions[0]}"
|
|
358
|
+
|
|
359
|
+
def tricc_operation_plus(self, ref_expressions):
|
|
360
|
+
return " + ".join(ref_expressions)
|
|
361
|
+
|
|
362
|
+
def tricc_operation_minus(self, ref_expressions):
|
|
363
|
+
if len(ref_expressions) > 1:
|
|
364
|
+
return " - ".join(ref_expressions)
|
|
365
|
+
return f"-{ref_expressions[0]}"
|
|
366
|
+
|
|
367
|
+
def tricc_operation_more(self, ref_expressions):
|
|
368
|
+
return f"{ref_expressions[0]} > {ref_expressions[1]}"
|
|
369
|
+
|
|
370
|
+
def tricc_operation_less(self, ref_expressions):
|
|
371
|
+
return f"{ref_expressions[0]} < {ref_expressions[1]}"
|
|
372
|
+
|
|
373
|
+
def tricc_operation_more_or_equal(self, ref_expressions):
|
|
374
|
+
return f"{ref_expressions[0]} >= {ref_expressions[1]}"
|
|
375
|
+
|
|
376
|
+
def tricc_operation_less_or_equal(self, ref_expressions):
|
|
377
|
+
return f"{ref_expressions[0]} <= {ref_expressions[1]}"
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import datetime
|
|
4
|
+
from tricc_oo.strategies.output.base_output_strategy import BaseOutPutStrategy
|
|
5
|
+
from tricc_oo.models.base import (
|
|
6
|
+
TriccOperation,
|
|
7
|
+
TriccStatic, TriccReference
|
|
8
|
+
)
|
|
9
|
+
from tricc_oo.models.tricc import (
|
|
10
|
+
TriccNodeSelectOption,
|
|
11
|
+
TriccNodeInputModel,
|
|
12
|
+
TriccNodeBaseModel
|
|
13
|
+
)
|
|
14
|
+
from tricc_oo.converters.tricc_to_xls_form import get_export_name
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger("default")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class HTMLStrategy(BaseOutPutStrategy):
|
|
20
|
+
processes = ["main"]
|
|
21
|
+
project = None
|
|
22
|
+
output_path = None
|
|
23
|
+
|
|
24
|
+
def __init__(self, project, output_path):
|
|
25
|
+
super().__init__(project, output_path)
|
|
26
|
+
self.html_content = ""
|
|
27
|
+
self.js_statements = []
|
|
28
|
+
|
|
29
|
+
def get_tricc_operation_expression(self, operation):
|
|
30
|
+
ref_expressions = []
|
|
31
|
+
if not hasattr(operation, "reference"):
|
|
32
|
+
return self.get_tricc_operation_operand(operation)
|
|
33
|
+
for r in operation.reference:
|
|
34
|
+
if isinstance(r, list):
|
|
35
|
+
r_expr = [
|
|
36
|
+
(
|
|
37
|
+
self.get_tricc_operation_expression(sr)
|
|
38
|
+
if isinstance(sr, TriccOperation)
|
|
39
|
+
else self.get_tricc_operation_operand(sr)
|
|
40
|
+
)
|
|
41
|
+
for sr in r
|
|
42
|
+
]
|
|
43
|
+
elif isinstance(r, TriccOperation):
|
|
44
|
+
r_expr = self.get_tricc_operation_expression(r)
|
|
45
|
+
else:
|
|
46
|
+
r_expr = self.get_tricc_operation_operand(r)
|
|
47
|
+
if isinstance(r_expr, TriccReference):
|
|
48
|
+
r_expr = self.get_tricc_operation_operand(r_expr)
|
|
49
|
+
ref_expressions.append(r_expr)
|
|
50
|
+
|
|
51
|
+
# build lower level
|
|
52
|
+
if hasattr(self, f"tricc_operation_{operation.operator}"):
|
|
53
|
+
callable = getattr(self, f"tricc_operation_{operation.operator}")
|
|
54
|
+
return callable(ref_expressions)
|
|
55
|
+
else:
|
|
56
|
+
raise NotImplementedError(
|
|
57
|
+
f"This type of operation '{operation.operator}' is not supported in this strategy"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def execute(self):
|
|
61
|
+
version = datetime.datetime.now().strftime("%Y%m%d%H%M")
|
|
62
|
+
logger.info(f"build version: {version}")
|
|
63
|
+
if "main" in self.project.start_pages:
|
|
64
|
+
self.process_base(self.project.start_pages, pages=self.project.pages, version=version)
|
|
65
|
+
else:
|
|
66
|
+
logger.critical("Main process required")
|
|
67
|
+
|
|
68
|
+
logger.info("generate the relevance based on edges")
|
|
69
|
+
self.process_relevance(self.project.start_pages, pages=self.project.pages)
|
|
70
|
+
|
|
71
|
+
logger.info("generate the calculate based on edges")
|
|
72
|
+
self.process_calculate(self.project.start_pages, pages=self.project.pages)
|
|
73
|
+
|
|
74
|
+
logger.info("generate the export format")
|
|
75
|
+
self.process_export(self.project.start_pages, pages=self.project.pages)
|
|
76
|
+
|
|
77
|
+
logger.info("print the export")
|
|
78
|
+
self.export(self.project.start_pages, version=version)
|
|
79
|
+
|
|
80
|
+
def generate_base(self, node, **kwargs):
|
|
81
|
+
# Generate base HTML for nodes
|
|
82
|
+
if hasattr(node, 'label') and node.label:
|
|
83
|
+
self.html_content += f"<label for='{node.get_name()}'>{node.label}</label>\n"
|
|
84
|
+
if hasattr(node, 'tricc_type'):
|
|
85
|
+
onchange = "onchange='updateForm()'"
|
|
86
|
+
if node.tricc_type == 'text':
|
|
87
|
+
self.html_content += f"<input type='text' id='{node.get_name()}' name='{node.get_name()}' {onchange} />\n" # noqa: E501
|
|
88
|
+
elif node.tricc_type == 'integer':
|
|
89
|
+
self.html_content += f"<input type='number' id='{node.get_name()}' name='{node.get_name()}' {onchange} />\n" # noqa: E501
|
|
90
|
+
# Add more types as needed
|
|
91
|
+
return True
|
|
92
|
+
|
|
93
|
+
def generate_relevance(self, node, **kwargs):
|
|
94
|
+
# Generate JS for skip logic (relevance)
|
|
95
|
+
if hasattr(node, 'expression') and node.expression:
|
|
96
|
+
relevance_js = f"if ({self.convert_expression_to_js(node.expression)}) {{ document.getElementById('{node.get_name()}').style.display = 'block'; }} else {{ document.getElementById('{node.get_name()}').style.display = 'none'; }}" # noqa: E501
|
|
97
|
+
self.js_statements.append(relevance_js)
|
|
98
|
+
return True
|
|
99
|
+
|
|
100
|
+
def generate_calculate(self, node, **kwargs):
|
|
101
|
+
# Generate JS for calculations
|
|
102
|
+
if hasattr(node, 'expression') and node.expression:
|
|
103
|
+
calc_js = f"document.getElementById('{node.get_name()}').value = {self.convert_expression_to_js(node.expression)};" # noqa: E501
|
|
104
|
+
self.js_statements.append(calc_js)
|
|
105
|
+
return True
|
|
106
|
+
|
|
107
|
+
def generate_export(self, node, **kwargs):
|
|
108
|
+
# For OpenMRS, export is part of building HTML
|
|
109
|
+
return True
|
|
110
|
+
|
|
111
|
+
def export(self, start_pages, version):
|
|
112
|
+
form_id = start_pages["main"].root.form_id or "openmrs_form"
|
|
113
|
+
file_name = f"{form_id}.html"
|
|
114
|
+
newpath = os.path.join(self.output_path, file_name)
|
|
115
|
+
if not os.path.exists(self.output_path):
|
|
116
|
+
os.makedirs(self.output_path)
|
|
117
|
+
|
|
118
|
+
js_function = f"""
|
|
119
|
+
function updateForm() {{
|
|
120
|
+
{"\n ".join(self.js_statements)}
|
|
121
|
+
}}
|
|
122
|
+
window.onload = updateForm;
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
full_html = f"""
|
|
126
|
+
<!DOCTYPE html>
|
|
127
|
+
<html>
|
|
128
|
+
<head>
|
|
129
|
+
<title>{start_pages["main"].root.label or 'OpenMRS Form'}</title>
|
|
130
|
+
</head>
|
|
131
|
+
<body>
|
|
132
|
+
<form id="{form_id}">
|
|
133
|
+
{self.html_content}
|
|
134
|
+
</form>
|
|
135
|
+
<script>
|
|
136
|
+
{js_function}
|
|
137
|
+
</script>
|
|
138
|
+
</body>
|
|
139
|
+
</html>
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
with open(newpath, 'w') as f:
|
|
143
|
+
f.write(full_html)
|
|
144
|
+
logger.info(f"Exported OpenMRS form to {newpath}")
|
|
145
|
+
|
|
146
|
+
def get_tricc_operation_operand(self, r):
|
|
147
|
+
if isinstance(r, TriccOperation):
|
|
148
|
+
return self.get_tricc_operation_expression(r)
|
|
149
|
+
elif isinstance(r, TriccReference):
|
|
150
|
+
return f"document.getElementById('{get_export_name(r.value)}').value"
|
|
151
|
+
elif isinstance(r, TriccStatic):
|
|
152
|
+
if isinstance(r.value, bool):
|
|
153
|
+
return str(r.value).lower()
|
|
154
|
+
if isinstance(r.value, str):
|
|
155
|
+
return f"'{r.value}'"
|
|
156
|
+
else:
|
|
157
|
+
return str(r.value)
|
|
158
|
+
elif isinstance(r, str):
|
|
159
|
+
return f"{r}"
|
|
160
|
+
elif isinstance(r, (int, float)):
|
|
161
|
+
return str(r)
|
|
162
|
+
elif isinstance(r, TriccNodeSelectOption):
|
|
163
|
+
return f"'{r.name}'"
|
|
164
|
+
elif issubclass(r.__class__, TriccNodeInputModel):
|
|
165
|
+
return f"document.getElementById('{get_export_name(r)}').value"
|
|
166
|
+
elif issubclass(r.__class__, TriccNodeBaseModel):
|
|
167
|
+
return f"document.getElementById('{get_export_name(r)}').value"
|
|
168
|
+
else:
|
|
169
|
+
raise NotImplementedError(f"This type of node {r.__class__} is not supported within an operation")
|
|
170
|
+
|
|
171
|
+
def convert_expression_to_js(self, expression):
|
|
172
|
+
if isinstance(expression, TriccOperation):
|
|
173
|
+
return self.get_tricc_operation_expression(expression)
|
|
174
|
+
else:
|
|
175
|
+
return self.get_tricc_operation_operand(expression)
|
|
176
|
+
|
|
177
|
+
# Implement operation methods as needed, similar to XLSForm but for JS
|
|
178
|
+
def tricc_operation_equal(self, ref_expressions):
|
|
179
|
+
return f"{ref_expressions[0]} === {ref_expressions[1]}"
|
|
180
|
+
|
|
181
|
+
def tricc_operation_not_equal(self, ref_expressions):
|
|
182
|
+
return f"{ref_expressions[0]} !== {ref_expressions[1]}"
|
|
183
|
+
|
|
184
|
+
def tricc_operation_and(self, ref_expressions):
|
|
185
|
+
if len(ref_expressions) == 1:
|
|
186
|
+
return ref_expressions[0]
|
|
187
|
+
if len(ref_expressions) > 1:
|
|
188
|
+
return " && ".join(ref_expressions)
|
|
189
|
+
else:
|
|
190
|
+
return "true"
|
|
191
|
+
|
|
192
|
+
def tricc_operation_or(self, ref_expressions):
|
|
193
|
+
if len(ref_expressions) == 1:
|
|
194
|
+
return ref_expressions[0]
|
|
195
|
+
if len(ref_expressions) > 1:
|
|
196
|
+
return " || ".join(ref_expressions)
|
|
197
|
+
else:
|
|
198
|
+
return "true"
|
|
199
|
+
|
|
200
|
+
def tricc_operation_not(self, ref_expressions):
|
|
201
|
+
return f"!({ref_expressions[0]})"
|
|
202
|
+
|
|
203
|
+
def tricc_operation_plus(self, ref_expressions):
|
|
204
|
+
return " + ".join(ref_expressions)
|
|
205
|
+
|
|
206
|
+
def tricc_operation_minus(self, ref_expressions):
|
|
207
|
+
if len(ref_expressions) > 1:
|
|
208
|
+
return " - ".join(map(str, ref_expressions))
|
|
209
|
+
elif len(ref_expressions) == 1:
|
|
210
|
+
return f"-{ref_expressions[0]}"
|
|
211
|
+
|
|
212
|
+
def tricc_operation_more(self, ref_expressions):
|
|
213
|
+
return f"{ref_expressions[0]} > {ref_expressions[1]}"
|
|
214
|
+
|
|
215
|
+
def tricc_operation_less(self, ref_expressions):
|
|
216
|
+
return f"{ref_expressions[0]} < {ref_expressions[1]}"
|
|
217
|
+
|
|
218
|
+
def tricc_operation_more_or_equal(self, ref_expressions):
|
|
219
|
+
return f"{ref_expressions[0]} >= {ref_expressions[1]}"
|
|
220
|
+
|
|
221
|
+
def tricc_operation_less_or_equal(self, ref_expressions):
|
|
222
|
+
return f"{ref_expressions[0]} <= {ref_expressions[1]}"
|
|
223
|
+
|
|
224
|
+
# Add more operations as needed...
|