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