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.
Files changed (47) hide show
  1. tests/build.py +20 -28
  2. tests/test_build.py +260 -0
  3. tests/test_cql.py +48 -109
  4. tests/to_ocl.py +15 -17
  5. tricc_oo/__init__.py +0 -6
  6. tricc_oo/converters/codesystem_to_ocl.py +51 -40
  7. tricc_oo/converters/cql/cqlLexer.py +1 -0
  8. tricc_oo/converters/cql/cqlListener.py +1 -0
  9. tricc_oo/converters/cql/cqlParser.py +1 -0
  10. tricc_oo/converters/cql/cqlVisitor.py +1 -0
  11. tricc_oo/converters/cql_to_operation.py +129 -123
  12. tricc_oo/converters/datadictionnary.py +45 -54
  13. tricc_oo/converters/drawio_type_map.py +146 -65
  14. tricc_oo/converters/tricc_to_xls_form.py +58 -28
  15. tricc_oo/converters/utils.py +4 -4
  16. tricc_oo/converters/xml_to_tricc.py +296 -235
  17. tricc_oo/models/__init__.py +2 -1
  18. tricc_oo/models/base.py +333 -305
  19. tricc_oo/models/calculate.py +66 -51
  20. tricc_oo/models/lang.py +26 -27
  21. tricc_oo/models/ocl.py +146 -161
  22. tricc_oo/models/ordered_set.py +15 -19
  23. tricc_oo/models/tricc.py +149 -89
  24. tricc_oo/parsers/xml.py +15 -30
  25. tricc_oo/serializers/planuml.py +4 -6
  26. tricc_oo/serializers/xls_form.py +110 -153
  27. tricc_oo/strategies/input/base_input_strategy.py +28 -32
  28. tricc_oo/strategies/input/drawio.py +59 -71
  29. tricc_oo/strategies/output/base_output_strategy.py +151 -65
  30. tricc_oo/strategies/output/dhis2_form.py +908 -0
  31. tricc_oo/strategies/output/fhir_form.py +377 -0
  32. tricc_oo/strategies/output/html_form.py +224 -0
  33. tricc_oo/strategies/output/openmrs_form.py +694 -0
  34. tricc_oo/strategies/output/spice.py +106 -127
  35. tricc_oo/strategies/output/xls_form.py +322 -244
  36. tricc_oo/strategies/output/xlsform_cdss.py +627 -142
  37. tricc_oo/strategies/output/xlsform_cht.py +252 -125
  38. tricc_oo/strategies/output/xlsform_cht_hf.py +13 -24
  39. tricc_oo/visitors/tricc.py +1424 -1033
  40. tricc_oo/visitors/utils.py +16 -16
  41. tricc_oo/visitors/xform_pd.py +91 -89
  42. {tricc_oo-1.5.13.dist-info → tricc_oo-1.6.8.dist-info}/METADATA +128 -84
  43. tricc_oo-1.6.8.dist-info/RECORD +52 -0
  44. tricc_oo-1.6.8.dist-info/licenses/LICENSE +373 -0
  45. {tricc_oo-1.5.13.dist-info → tricc_oo-1.6.8.dist-info}/top_level.txt +0 -0
  46. tricc_oo-1.5.13.dist-info/RECORD +0 -46
  47. {tricc_oo-1.5.13.dist-info → tricc_oo-1.6.8.dist-info}/WHEEL +0 -0
@@ -0,0 +1,694 @@
1
+ import logging
2
+ import os
3
+ import json
4
+ import uuid
5
+ from tricc_oo.visitors.tricc import (
6
+ is_ready_to_process,
7
+ process_reference,
8
+ generate_base,
9
+ generate_calculate,
10
+ walktrhough_tricc_node_processed_stached,
11
+ check_stashed_loop,
12
+ )
13
+ from tricc_oo.converters.tricc_to_xls_form import get_export_name
14
+ import datetime
15
+ from tricc_oo.strategies.output.base_output_strategy import BaseOutPutStrategy
16
+ from tricc_oo.models.base import (
17
+ not_clean, TriccOperation,
18
+ TriccStatic, TriccReference
19
+ )
20
+ from tricc_oo.models.tricc import (
21
+ TriccNodeSelectOption,
22
+ TriccNodeInputModel,
23
+ TriccNodeBaseModel,
24
+ TriccNodeSelect,
25
+ TriccNodeDisplayModel,
26
+ )
27
+
28
+ from tricc_oo.models.calculate import TriccNodeDisplayCalculateBase, TriccNodeDiagnosis
29
+ from tricc_oo.models.ordered_set import OrderedSet
30
+
31
+ logger = logging.getLogger("default")
32
+
33
+ # Namespace for deterministic UUIDs
34
+ UUID_NAMESPACE = uuid.UUID('12345678-1234-5678-9abc-def012345678')
35
+
36
+ CIEL_YES = "1065AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
37
+ CIEL_NO = "1066AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
38
+
39
+
40
+ class OpenMRSStrategy(BaseOutPutStrategy):
41
+ processes = ["main"]
42
+ project = None
43
+ output_path = None
44
+
45
+ def __init__(self, project, output_path):
46
+ super().__init__(project, output_path)
47
+ form_id = getattr(self.project.start_pages["main"], 'form_id', 'openmrs_form')
48
+ self.form_data = {
49
+ "$schema": "http://json.openmrs.org/form.schema.json",
50
+ "name": form_id,
51
+ "uuid": str(uuid.uuid5(UUID_NAMESPACE, form_id)),
52
+ "encounterType": str(uuid.uuid5(UUID_NAMESPACE, f"{form_id}_encounter_type")),
53
+ "processor": "EncounterFormProcessor",
54
+ "published": False,
55
+ "retired": False,
56
+ "version": "1.0",
57
+ "availableIntents": [
58
+ {
59
+ "intent": "*",
60
+ "display": form_id
61
+ }
62
+ ],
63
+ "referencedForms": [],
64
+ "encounter": form_id,
65
+ "pages": []
66
+ }
67
+ self.field_counter = 1
68
+ self.questions_temp = [] # Temporary storage for questions with ordering info
69
+ self.processing_order = 0 # Counter to track processing order
70
+ self.current_segment = None
71
+ self.current_activity = None
72
+ self.concept_map = {}
73
+ self.calculated_fields = [] # Store calculated fields to add to first section of each page
74
+ self.calculated_fields_added = set() # Track which pages have had calculated fields added
75
+ self.inject_version()
76
+
77
+ def get_export_name(self, r):
78
+ if isinstance(r, TriccNodeSelectOption):
79
+ return self.get_option_value(r.name)
80
+ elif isinstance(r, str):
81
+ return self.get_option_value(r)
82
+ elif isinstance(r, TriccStatic):
83
+ if isinstance(r.value, str):
84
+ return self.get_option_value(r.value)
85
+ elif isinstance(r.value, bool):
86
+ return str(r.value).lower()
87
+ else:
88
+ return r.value
89
+ else:
90
+ return get_export_name(r) # Assuming r is a node
91
+
92
+ def generate_id(self, name):
93
+ return str(uuid.uuid5(UUID_NAMESPACE, name))
94
+
95
+ def get_option_value(self, option_name):
96
+ if option_name == 'true':
97
+ return TriccStatic(True)
98
+ elif option_name == 'false':
99
+ return TriccStatic(False)
100
+ return self.concept_map.get(option_name, option_name)
101
+
102
+ def get_tricc_operation_expression(self, operation):
103
+ # Similar to HTML, but for JSON, perhaps convert to string expressions
104
+ ref_expressions = []
105
+ if not hasattr(operation, "reference"):
106
+ return self.get_tricc_operation_operand(operation)
107
+ for r in operation.reference:
108
+ if isinstance(r, list):
109
+ r_expr = [
110
+ (
111
+ self.get_tricc_operation_expression(sr)
112
+ if isinstance(sr, TriccOperation)
113
+ else self.get_tricc_operation_operand(sr)
114
+ )
115
+ for sr in r
116
+ ]
117
+ elif isinstance(r, TriccOperation):
118
+ r_expr = self.get_tricc_operation_expression(r)
119
+ else:
120
+ r_expr = self.get_tricc_operation_operand(r)
121
+ if isinstance(r_expr, TriccReference):
122
+ r_expr = self.get_tricc_operation_operand(r_expr)
123
+ elif isinstance(r_expr, TriccStatic) and isinstance(r_expr.value, bool):
124
+ r_expr = str(r_expr.value).lower()
125
+ ref_expressions.append(r_expr)
126
+
127
+ # build lower level
128
+ if hasattr(self, f"tricc_operation_{operation.operator}"):
129
+ callable = getattr(self, f"tricc_operation_{operation.operator}")
130
+ return callable(ref_expressions)
131
+ else:
132
+ raise NotImplementedError(
133
+ f"This type of operation '{operation.operator}' is not supported in this strategy"
134
+ )
135
+
136
+ def execute(self):
137
+ version = datetime.datetime.now().strftime("%Y%m%d%H%M")
138
+ logger.info(f"build version: {version}")
139
+ if "main" in self.project.start_pages:
140
+ self.process_base(self.project.start_pages, pages=self.project.pages, version=version)
141
+ else:
142
+ logger.critical("Main process required")
143
+
144
+ logger.info("generate the relevance based on edges")
145
+ # self.process_relevance(self.project.start_pages, pages=self.project.pages)
146
+
147
+ logger.info("generate the calculate based on edges")
148
+ self.process_calculate(self.project.start_pages, pages=self.project.pages)
149
+
150
+ logger.info("generate the export format")
151
+ self.process_export(self.project.start_pages, pages=self.project.pages)
152
+
153
+ logger.info("create calculation page")
154
+ self.create_calculation_page()
155
+
156
+ logger.info("print the export")
157
+ self.export(self.project.start_pages, version=version)
158
+
159
+ def map_tricc_type_to_rendering(self, node):
160
+ mapping = {
161
+ 'text': 'text',
162
+ 'integer': 'number',
163
+ 'decimal': 'number',
164
+ 'date': 'date',
165
+ 'datetime': 'datetime',
166
+ 'select_one': 'select',
167
+ 'select_multiple': 'multiCheckbox',
168
+ 'select_yesno': 'select',
169
+ 'not_available': 'checkbox',
170
+ 'note': 'markdown'
171
+ }
172
+
173
+ # if issubclass(node.__class__, TriccNodeSelectYesNo):
174
+ # return 'select'
175
+ return mapping.get(node.tricc_type, 'text')
176
+
177
+ def generate_base(self, node, processed_nodes, **kwargs):
178
+ if generate_base(node, processed_nodes, **kwargs):
179
+ if getattr(node, 'name', '') not in ('true', 'false'):
180
+ self.concept_map[node.name] = self.generate_id(self.get_export_name(node))
181
+ return True
182
+ return False
183
+
184
+ def generate_question(self, node):
185
+ if issubclass(node.__class__, TriccNodeDisplayModel) and not isinstance(node, TriccNodeSelectOption):
186
+ question = {
187
+ "label": getattr(node, 'label', '').replace('\u00a0', ' ').strip(),
188
+ "type": "obs" if issubclass(node.__class__, TriccNodeInputModel) else 'control',
189
+ "questionOptions": {
190
+ "rendering": self.map_tricc_type_to_rendering(node),
191
+ "concept": self.generate_id(self.get_export_name(node))
192
+ },
193
+ "required": bool(getattr(node, 'required', False)),
194
+ "unspecified": False,
195
+ "id": self.get_export_name(node),
196
+ "uuid": self.generate_id(self.get_export_name(node))
197
+ }
198
+ if node.image:
199
+ question['questionOptions']["imageUrl"] = node.image
200
+ if node.hint:
201
+ question["questionInfo"] = node.hint
202
+ if node.tricc_type in ['select_one', 'select_multiple']:
203
+ # labelTrue = None
204
+ # labelFalse = None
205
+ # Add answers if options
206
+ if hasattr(node, 'options'):
207
+ answers = []
208
+ for opt in node.options.values():
209
+ display = getattr(opt, 'label', opt.name)
210
+ # All options now use UUIDs
211
+ concept_val = self.get_option_value(opt.name)
212
+ if concept_val == TriccStatic(False):
213
+ concept_val = CIEL_NO
214
+ # labelFalse = display
215
+ if concept_val == TriccStatic(True):
216
+ concept_val = CIEL_YES
217
+ # labelTrue = display
218
+ answers.append({
219
+ "label": display,
220
+ "concept": concept_val,
221
+ })
222
+ question["questionOptions"]["answers"] = answers
223
+ else:
224
+ question["questionOptions"]["answers"] = []
225
+ # Set concept for the question itself if it's a coded question
226
+ # if issubclass(node.__class__, TriccNodeSelectYesNo):
227
+ # question["questionOptions"]["toggleOptions"] = {
228
+ # "labelTrue": labelTrue,
229
+ # "labelFalse": labelFalse
230
+ # }
231
+
232
+ relevance = None
233
+ if hasattr(node, 'relevance') and node.relevance:
234
+ relevance = node.relevance
235
+ if hasattr(node, 'expression') and node.expression:
236
+ relevance = node.expression
237
+ if relevance:
238
+ relevance_str = self.convert_expression_to_string(not_clean(relevance))
239
+ if relevance_str and relevance_str != 'false':
240
+ question["hide"] = {
241
+ "hideWhenExpression": f"{relevance_str}"
242
+ }
243
+ return question
244
+ elif issubclass(node.__class__, TriccNodeDisplayCalculateBase):
245
+ expression = getattr(node, 'expression', None)
246
+ if expression:
247
+ question = {
248
+ "id": self.get_export_name(node),
249
+ "type": "obs" if isinstance(node, TriccNodeDiagnosis) else "control",
250
+ "label": getattr(node, 'label', '').replace('\u00a0', ' ').strip(),
251
+ "hide": {
252
+ "hideWhenExpression": "true"
253
+ },
254
+ "questionOptions": {
255
+ "calculate": {
256
+ "calculateExpression": self.convert_expression_to_string(expression)
257
+ }
258
+ }
259
+ }
260
+ # Collect calculated fields to add to first section of each page
261
+ self.calculated_fields.append(question)
262
+ return None # Don't return the question, it will be added to first section
263
+ return None
264
+
265
+ def generate_calculate(self, node, processed_nodes, **kwargs):
266
+ return generate_calculate(node, processed_nodes, **kwargs)
267
+
268
+ def process_export(self, start_pages, **kwargs):
269
+ self.activity_export(start_pages["main"], **kwargs)
270
+
271
+ def activity_export(self, activity, processed_nodes=None, **kwargs):
272
+ if processed_nodes is None:
273
+ processed_nodes = OrderedSet()
274
+ stashed_nodes = OrderedSet()
275
+ # The stashed node are all the node that have all their prevnode processed but not from the same group
276
+ # This logic works only because the prev node are ordered by group/parent ..
277
+ groups = {}
278
+ cur_group = activity
279
+ groups[activity.id] = 0
280
+ path_len = 0
281
+ process = ["main"]
282
+ # keep the versions on the group id, max version
283
+ self.start_page(cur_group)
284
+ self.start_section(cur_group)
285
+ walktrhough_tricc_node_processed_stached(
286
+ activity.root,
287
+ self.generate_export,
288
+ processed_nodes,
289
+ stashed_nodes,
290
+ path_len,
291
+ cur_group=activity.root.group,
292
+ process=process,
293
+ recursive=False,
294
+ **kwargs
295
+ )
296
+ # we save the survey data frame
297
+ # MANAGE STASHED NODES
298
+ prev_stashed_nodes = stashed_nodes.copy()
299
+ loop_count = 0
300
+ len_prev_processed_nodes = 0
301
+ while len(stashed_nodes) > 0:
302
+ self.questions_temp = [] # Reset for new section
303
+ loop_count = check_stashed_loop(
304
+ stashed_nodes,
305
+ prev_stashed_nodes,
306
+ processed_nodes,
307
+ len_prev_processed_nodes,
308
+ loop_count,
309
+ )
310
+ prev_stashed_nodes = stashed_nodes.copy()
311
+ len_prev_processed_nodes = len(processed_nodes)
312
+ if len(stashed_nodes) > 0:
313
+ s_node = stashed_nodes.pop()
314
+ # while len(stashed_nodes)>0 and isinstance(s_node,TriccGroup):
315
+ # s_node = stashed_nodes.pop()
316
+ if s_node.group is None:
317
+ logger.critical("ERROR group is none for node {}".format(s_node.get_name()))
318
+ # arrange empty group
319
+ walktrhough_tricc_node_processed_stached(
320
+ s_node,
321
+ self.generate_export,
322
+ processed_nodes,
323
+ stashed_nodes,
324
+ path_len,
325
+ groups=groups,
326
+ cur_group=s_node.group,
327
+ recursive=False,
328
+ process=process,
329
+ **kwargs
330
+ )
331
+ # add end group if new node where added OR if the previous end group was removed
332
+ # if two line then empty group
333
+ if len(self.questions_temp) > 0:
334
+ # Add questions to current section
335
+ for q_item in sorted(self.questions_temp, key=lambda x: x['processing_order']):
336
+ if self.current_section:
337
+ self.current_section["questions"].append(q_item['question'])
338
+ cur_group = s_node.group
339
+
340
+ return processed_nodes
341
+
342
+ def generate_export(self, node, processed_nodes, **kwargs):
343
+ if not is_ready_to_process(node, processed_nodes, strict=False):
344
+ return False
345
+
346
+ # Process references to ensure dependencies are handled
347
+ if not process_reference(
348
+ node, processed_nodes, {}, replace_reference=False, codesystems=kwargs.get("codesystems", None)
349
+ ):
350
+ return False
351
+
352
+ if node not in processed_nodes:
353
+ if self.current_segment != getattr(node.activity.root, 'process', self.current_segment):
354
+ self.start_page(node.activity)
355
+ if self.current_activity != node.group:
356
+ self.start_section(node.activity)
357
+ question = self.generate_question(node)
358
+ if question:
359
+ # Store question with processing order
360
+ # self.questions_temp.append({
361
+ # 'question': question,
362
+ # 'processing_order': self.processing_order,
363
+ # 'node_id': getattr(node, 'id', '')
364
+ # })
365
+ self.processing_order += 1
366
+ self.field_counter += 1
367
+ self.form_data['pages'][-1]['sections'][-1]['questions'].append(question)
368
+
369
+ # Set form name from the start page label if available
370
+ if hasattr(self.project.start_pages["main"], 'label') and self.project.start_pages["main"].label:
371
+ self.form_data["name"] = self.project.start_pages["main"].label.strip()
372
+ elif hasattr(node, 'label') and node.label:
373
+ self.form_data["name"] = node.label.strip()
374
+ return True
375
+
376
+ def export(self, start_pages, version):
377
+ form_id = start_pages["main"].root.form_id or "openmrs_form"
378
+ file_name = f"{form_id}.json"
379
+ newpath = os.path.join(self.output_path, file_name)
380
+ if not os.path.exists(self.output_path):
381
+ os.makedirs(self.output_path)
382
+
383
+ with open(newpath, 'w') as f:
384
+ json.dump(self.form_data, f, indent=2)
385
+ logger.info(f"Exported OpenMRS form to {newpath}")
386
+
387
+ def get_tricc_operation_operand(self, r):
388
+ if isinstance(r, TriccOperation):
389
+ return self.get_tricc_operation_expression(r)
390
+ elif isinstance(r, TriccReference):
391
+ return self.get_export_name(r.value)
392
+ elif isinstance(r, TriccStatic):
393
+ if isinstance(r.value, bool):
394
+ return str(r.value).lower()
395
+ if isinstance(r.value, str):
396
+ return f"'{r.value}'"
397
+ else:
398
+ return str(r.value)
399
+ elif isinstance(r, bool):
400
+ return str(r).lower()
401
+ elif isinstance(r, str):
402
+ return f"{r}"
403
+ elif isinstance(r, (int, float)):
404
+ return str(r).lower()
405
+ elif isinstance(r, TriccNodeSelectOption):
406
+ option = self.get_option_value(r.name)
407
+ if r.name in ('true', 'false'):
408
+ return option
409
+ return f"'{option}'"
410
+ elif issubclass(r.__class__, TriccNodeInputModel):
411
+ return self.get_export_name(r)
412
+ elif issubclass(r.__class__, TriccNodeSelect):
413
+ return "(" + self.get_export_name(r) + " ?? [])"
414
+ elif issubclass(r.__class__, TriccNodeBaseModel):
415
+ return self.get_export_name(r)
416
+ else:
417
+ raise NotImplementedError(f"This type of node {r.__class__} is not supported within an operation")
418
+
419
+ def convert_expression_to_string(self, expression):
420
+ # Convert to string expression for JSON
421
+ if isinstance(expression, TriccOperation):
422
+ return self.get_tricc_operation_expression(expression)
423
+ else:
424
+ return self.get_tricc_operation_operand(expression)
425
+
426
+ # Operation methods similar, but for string expressions
427
+ def tricc_operation_equal(self, ref_expressions):
428
+ if ref_expressions[1] == TriccStatic(True) or ref_expressions[1] is True or ref_expressions[1] == 'true':
429
+ return f"{self._boolean(ref_expressions, '===', CIEL_YES, 'true')}"
430
+ elif ref_expressions[1] == TriccStatic(False) or ref_expressions[1] is False or ref_expressions[1] == 'false':
431
+ return f"{self._boolean(ref_expressions, '===', CIEL_NO, 'false')}"
432
+ return f"{ref_expressions[0]} === {ref_expressions[1]}"
433
+
434
+ def tricc_operation_not_equal(self, ref_expressions):
435
+ if ref_expressions[1] == TriccStatic(True) or ref_expressions[1] is True or ref_expressions[1] == 'true':
436
+ return f"!{self._boolean(ref_expressions, '===', CIEL_YES, 'true')}"
437
+ elif ref_expressions[1] == TriccStatic(False) or ref_expressions[1] is False or ref_expressions[1] == 'false':
438
+ return f"!{self._boolean(ref_expressions, '===', CIEL_NO, 'false')}"
439
+ return f"{ref_expressions[0]} !== {ref_expressions[1]}"
440
+
441
+ def tricc_operation_and(self, ref_expressions):
442
+ if len(ref_expressions) == 1:
443
+ return ref_expressions[0]
444
+ if len(ref_expressions) > 1:
445
+ return " && ".join(ref_expressions)
446
+ else:
447
+ return "true"
448
+
449
+ def tricc_operation_or(self, ref_expressions):
450
+ if len(ref_expressions) == 1:
451
+ return ref_expressions[0]
452
+ if len(ref_expressions) > 1:
453
+ return "(" + " || ".join(ref_expressions) + ")"
454
+ else:
455
+ return "true"
456
+
457
+ def tricc_operation_not(self, ref_expressions):
458
+ return f"!({ref_expressions[0]})"
459
+
460
+ def tricc_operation_plus(self, ref_expressions):
461
+ return "(" + " + ".join(ref_expressions) + ")"
462
+
463
+ def tricc_operation_minus(self, ref_expressions):
464
+ if len(ref_expressions) > 1:
465
+ return " - ".join(map(str, ref_expressions))
466
+ elif len(ref_expressions) == 1:
467
+ return f"-{ref_expressions[0]}"
468
+
469
+ def tricc_operation_more(self, ref_expressions):
470
+ return f"{ref_expressions[0]} > {ref_expressions[1]}"
471
+
472
+ def tricc_operation_less(self, ref_expressions):
473
+ return f"{ref_expressions[0]} < {ref_expressions[1]}"
474
+
475
+ def tricc_operation_more_or_equal(self, ref_expressions):
476
+ return f"{ref_expressions[0]} >= {ref_expressions[1]}"
477
+
478
+ def tricc_operation_less_or_equal(self, ref_expressions):
479
+ return f"{ref_expressions[0]} <= {ref_expressions[1]}"
480
+
481
+ def tricc_operation_selected(self, ref_expressions):
482
+ # For choice questions, returns true if the second reference (value) is included in the first (field)
483
+ return f"({ref_expressions[0]}.includes({ref_expressions[1]}))"
484
+
485
+ def tricc_operation_count(self, ref_expressions):
486
+ return f"{ref_expressions[0]}.length"
487
+
488
+ def tricc_operation_multiplied(self, ref_expressions):
489
+ return "*".join(ref_expressions)
490
+
491
+ def tricc_operation_divided(self, ref_expressions):
492
+ return f"{ref_expressions[0]} / {ref_expressions[1]}"
493
+
494
+ def tricc_operation_modulo(self, ref_expressions):
495
+ return f"{ref_expressions[0]} % {ref_expressions[1]}"
496
+
497
+ def tricc_operation_coalesce(self, ref_expressions):
498
+ return f"coalesce({','.join(ref_expressions)})"
499
+
500
+ def tricc_operation_module(self, ref_expressions):
501
+ return f"{ref_expressions[0]} % {ref_expressions[1]}"
502
+
503
+ def tricc_operation_native(self, ref_expressions):
504
+ if len(ref_expressions) > 0:
505
+ return f"{ref_expressions[0]}({','.join(ref_expressions[1:])})"
506
+
507
+ def tricc_operation_istrue(self, ref_expressions):
508
+ # return f"{ref_expressions[0]} === true"
509
+ return f"{self._boolean(ref_expressions, '===', CIEL_YES, 'true')}"
510
+
511
+ def tricc_operation_isfalse(self, ref_expressions):
512
+ # return f"{ref_expressions[0]} === false"
513
+ return f"{self._boolean(ref_expressions, '===', CIEL_NO, 'false')}"
514
+
515
+ def tricc_operation_parenthesis(self, ref_expressions):
516
+ return f"({ref_expressions[0]})"
517
+
518
+ def tricc_operation_between(self, ref_expressions):
519
+ return f"{ref_expressions[0]} >= {ref_expressions[1]} && {ref_expressions[0]} < {ref_expressions[2]}"
520
+
521
+ def tricc_operation_isnull(self, ref_expressions):
522
+ return f"isEmpty({ref_expressions[0]})"
523
+
524
+ def tricc_operation_isnotnull(self, ref_expressions):
525
+ return f"!isEmpty{ref_expressions[0]})"
526
+
527
+ def tricc_operation_isnottrue(self, ref_expressions):
528
+ # return f"{ref_expressions[0]} !== true"
529
+ return f"!{self._boolean(ref_expressions, '===', CIEL_YES, 'true')}"
530
+
531
+ def tricc_operation_isnotfalse(self, ref_expressions):
532
+ # return f"{ref_expressions[0]} !== false"
533
+ return f"!{self._boolean(ref_expressions, '===', CIEL_NO, 'false')}"
534
+
535
+ def _boolean(self, ref_expressions, operator, answer_uuid, bool_val='false'):
536
+ return f"({ref_expressions[0]} {operator} {bool_val} || {ref_expressions[0]} {operator} '{answer_uuid}')"
537
+
538
+ def tricc_operation_notexist(self, ref_expressions):
539
+ return f"typeof {ref_expressions[0]} === 'undefined'"
540
+
541
+ def tricc_operation_case(self, ref_expressions):
542
+ # Simplified, assuming list of conditions
543
+ parts = []
544
+ for i in range(0, len(ref_expressions), 2):
545
+ if i + 1 < len(ref_expressions):
546
+ parts.append(f"if({ref_expressions[i]}, {ref_expressions[i+1]})")
547
+ return " || ".join(parts) # Simplified
548
+
549
+ def tricc_operation_ifs(self, ref_expressions):
550
+ # Similar to case
551
+ return self.tricc_operation_case(ref_expressions[1:])
552
+
553
+ def tricc_operation_if(self, ref_expressions):
554
+ return f"if({ref_expressions[0]}, {ref_expressions[1]}, {ref_expressions[2]})"
555
+
556
+ def tricc_operation_contains(self, ref_expressions):
557
+ return f"contains({ref_expressions[0]}, {ref_expressions[1]})"
558
+
559
+ def tricc_operation_exists(self, ref_expressions):
560
+ parts = []
561
+ for ref in ref_expressions:
562
+ parts.append(f"!isEmpty{ref})")
563
+ return " && ".join(parts)
564
+
565
+ def tricc_operation_cast_number(self, ref_expressions):
566
+ return f"Number({ref_expressions[0]})"
567
+
568
+ def tricc_operation_cast_integer(self, ref_expressions):
569
+ return f"Number({ref_expressions[0]})"
570
+
571
+ def tricc_operation_zscore(self, ref_expressions):
572
+ # Simplified, assuming params
573
+ return f"zscore({','.join(ref_expressions)})"
574
+
575
+ def tricc_operation_datetime_to_decimal(self, ref_expressions):
576
+ return f"decimal-date-time({ref_expressions[0]})"
577
+
578
+ def tricc_operation_round(self, ref_expressions):
579
+ return f"round({ref_expressions[0]})"
580
+
581
+ def tricc_operation_izscore(self, ref_expressions):
582
+ return f"izscore({','.join(ref_expressions)})"
583
+
584
+ def tricc_operation_concatenate(self, ref_expressions):
585
+ return f"concat({','.join(ref_expressions)})"
586
+
587
+ def clean_sections(self):
588
+ if (
589
+ self.form_data['pages']
590
+ and self.form_data['pages'][-1]
591
+ and self.form_data['pages'][-1]['sections']
592
+ and self.form_data['pages'][-1]['sections'][-1]
593
+ ):
594
+ if len(self.form_data['pages'][-1]['sections'][-1]['questions']) == 0:
595
+ self.form_data['pages'][-1]['sections'].pop()
596
+
597
+ def clean_pages(self):
598
+ if self.form_data['pages'] and self.form_data['pages'][-1]:
599
+ if len(self.form_data['pages'][-1]['sections']) == 0:
600
+ self.form_data['pages'].pop()
601
+
602
+ def start_page(self, activity_node):
603
+ # Add more operations as needed...
604
+ """Start a new page for an activity"""
605
+ self.clean_sections()
606
+ self.clean_pages()
607
+ page_label = getattr(activity_node.root, 'process', None)
608
+ self.current_segment = page_label
609
+ # Set process from id if not set
610
+ default_label = f"Page {len(self.form_data['pages']) + 1}"
611
+
612
+ if page_label is None:
613
+ label = getattr(activity_node, 'label', None)
614
+ if label is None:
615
+ page_label = default_label
616
+ else:
617
+ page_label = label
618
+ page_label = page_label.replace('\u00a0', ' ').strip()
619
+ self.form_data["pages"].append({
620
+ "label": page_label,
621
+ "sections": []
622
+ })
623
+ if activity_node.relevance:
624
+ relevance_str = self.convert_expression_to_string(not_clean(activity_node.relevance))
625
+ if relevance_str and relevance_str != 'false':
626
+ self.form_data["pages"][-1]["hide"] = {
627
+ "hideWhenExpression": f"{relevance_str}"
628
+ }
629
+ logger.debug(f"Started page: {page_label}")
630
+
631
+ def start_section(self, group_node):
632
+ """Start a new section for a group"""
633
+ self.clean_sections()
634
+ self.current_activity = group_node
635
+ # Set process from id if not set
636
+ default_label = f"Section {len(self.form_data['pages'][-1]['sections']) + 1}"
637
+ if hasattr(group_node, 'root'):
638
+ section_label = getattr(group_node.root, 'label', None)
639
+ else:
640
+ section_label = getattr(group_node, 'label', None)
641
+ if section_label is None:
642
+ label = getattr(group_node, 'label', None)
643
+ if label is None:
644
+ section_label = default_label
645
+ else:
646
+ section_label = label
647
+ section_label = section_label.replace('\u00a0', ' ').strip()
648
+ self.form_data['pages'][-1]['sections'].append({
649
+ "label": section_label,
650
+ "questions": []
651
+ })
652
+
653
+ if group_node.relevance:
654
+ relevance_str = self.convert_expression_to_string(not_clean(group_node.relevance))
655
+ if relevance_str and relevance_str != 'false':
656
+ self.form_data["pages"][-1]['sections'][-1]["hide"] = {
657
+ "hideWhenExpression": f"{relevance_str}"
658
+ }
659
+ logger.debug(f"Started section: {section_label}")
660
+
661
+ def create_calculation_page(self):
662
+ """Create a dedicated page for all calculated fields"""
663
+ if self.calculated_fields:
664
+ self.clean_sections()
665
+ self.clean_pages()
666
+ page = {
667
+ "label": "Calculations",
668
+ "sections": [
669
+ {
670
+ "label": "Calculations",
671
+ "questions": self.calculated_fields
672
+ }
673
+ ]
674
+ }
675
+ self.form_data["pages"].append(page)
676
+ logger.debug("Created calculation page")
677
+
678
+ def inject_version(self):
679
+ # Add hidden version field using version() function
680
+ question = {
681
+ "id": "version",
682
+ "type": "control",
683
+ "label": "",
684
+ "hide": {
685
+ "hideWhenExpression": "true"
686
+ },
687
+ "questionOptions": {
688
+ "calculate": {
689
+ "calculateExpression": "version()"
690
+ }
691
+ }
692
+ }
693
+ # Collect calculated fields to add to first section of each page
694
+ self.calculated_fields.append(question)