tricc-oo 1.0.1__py3-none-any.whl → 1.4.15__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 (66) hide show
  1. tests/build.py +213 -0
  2. tests/test_cql.py +197 -0
  3. tests/to_ocl.py +51 -0
  4. {tricc → tricc_oo}/__init__.py +3 -1
  5. tricc_oo/converters/codesystem_to_ocl.py +169 -0
  6. tricc_oo/converters/cql/cqlLexer.py +822 -0
  7. tricc_oo/converters/cql/cqlListener.py +1632 -0
  8. tricc_oo/converters/cql/cqlParser.py +11204 -0
  9. tricc_oo/converters/cql/cqlVisitor.py +913 -0
  10. tricc_oo/converters/cql_to_operation.py +402 -0
  11. tricc_oo/converters/datadictionnary.py +115 -0
  12. tricc_oo/converters/drawio_type_map.py +222 -0
  13. tricc_oo/converters/tricc_to_xls_form.py +61 -0
  14. tricc_oo/converters/utils.py +65 -0
  15. tricc_oo/converters/xml_to_tricc.py +1003 -0
  16. tricc_oo/models/__init__.py +4 -0
  17. tricc_oo/models/base.py +732 -0
  18. tricc_oo/models/calculate.py +216 -0
  19. tricc_oo/models/ocl.py +281 -0
  20. tricc_oo/models/ordered_set.py +125 -0
  21. tricc_oo/models/tricc.py +418 -0
  22. tricc_oo/parsers/xml.py +138 -0
  23. tricc_oo/serializers/__init__.py +0 -0
  24. tricc_oo/serializers/xls_form.py +745 -0
  25. tricc_oo/strategies/__init__.py +0 -0
  26. tricc_oo/strategies/input/__init__.py +0 -0
  27. tricc_oo/strategies/input/base_input_strategy.py +111 -0
  28. tricc_oo/strategies/input/drawio.py +317 -0
  29. tricc_oo/strategies/output/base_output_strategy.py +148 -0
  30. tricc_oo/strategies/output/spice.py +365 -0
  31. tricc_oo/strategies/output/xls_form.py +697 -0
  32. tricc_oo/strategies/output/xlsform_cdss.py +189 -0
  33. tricc_oo/strategies/output/xlsform_cht.py +200 -0
  34. tricc_oo/strategies/output/xlsform_cht_hf.py +334 -0
  35. tricc_oo/visitors/__init__.py +0 -0
  36. tricc_oo/visitors/tricc.py +2198 -0
  37. tricc_oo/visitors/utils.py +17 -0
  38. tricc_oo/visitors/xform_pd.py +264 -0
  39. tricc_oo-1.4.15.dist-info/METADATA +219 -0
  40. tricc_oo-1.4.15.dist-info/RECORD +46 -0
  41. {tricc_oo-1.0.1.dist-info → tricc_oo-1.4.15.dist-info}/WHEEL +1 -1
  42. tricc_oo-1.4.15.dist-info/top_level.txt +2 -0
  43. tricc/converters/mc_to_tricc.py +0 -542
  44. tricc/converters/tricc_to_xls_form.py +0 -553
  45. tricc/converters/utils.py +0 -44
  46. tricc/converters/xml_to_tricc.py +0 -740
  47. tricc/models/tricc.py +0 -1093
  48. tricc/parsers/xml.py +0 -81
  49. tricc/serializers/xls_form.py +0 -364
  50. tricc/strategies/input/base_input_strategy.py +0 -80
  51. tricc/strategies/input/drawio.py +0 -246
  52. tricc/strategies/input/medalcreator.py +0 -168
  53. tricc/strategies/output/base_output_strategy.py +0 -92
  54. tricc/strategies/output/xls_form.py +0 -194
  55. tricc/strategies/output/xlsform_cdss.py +0 -46
  56. tricc/strategies/output/xlsform_cht.py +0 -106
  57. tricc/visitors/tricc.py +0 -375
  58. tricc_oo-1.0.1.dist-info/LICENSE +0 -78
  59. tricc_oo-1.0.1.dist-info/METADATA +0 -229
  60. tricc_oo-1.0.1.dist-info/RECORD +0 -26
  61. tricc_oo-1.0.1.dist-info/top_level.txt +0 -2
  62. venv/bin/vba_extract.py +0 -78
  63. {tricc → tricc_oo}/converters/__init__.py +0 -0
  64. {tricc → tricc_oo}/models/lang.py +0 -0
  65. {tricc/serializers → tricc_oo/parsers}/__init__.py +0 -0
  66. {tricc → tricc_oo}/serializers/planuml.py +0 -0
@@ -0,0 +1,1003 @@
1
+ import base64
2
+ import os
3
+ import re
4
+ from curses.ascii import isalnum, isalpha, isdigit
5
+
6
+ from numpy import isnan
7
+
8
+ from tricc_oo.converters.utils import clean_name, remove_html
9
+ from tricc_oo.converters.cql_to_operation import transform_cql_to_operation
10
+ from tricc_oo.models.tricc import *
11
+ from tricc_oo.models.base import TriccNodeType, OPERATION_LIST
12
+ from tricc_oo.models.ocl import get_data_type
13
+ from tricc_oo.converters.drawio_type_map import TYPE_MAP
14
+ from tricc_oo.parsers.xml import (
15
+ get_edges_list,
16
+ get_mxcell,
17
+ get_elm,
18
+ get_mxcell_parent_list,
19
+ get_tricc_type,
20
+ get_tricc_type_list,
21
+ )
22
+ import hashlib
23
+ from tricc_oo.visitors.tricc import *
24
+ from tricc_oo.converters.datadictionnary import add_concept
25
+
26
+ TRICC_YES_LABEL = ["yes", "oui"]
27
+ TRICC_NO_LABEL = ["no", "non"]
28
+ TRICC_FOLLOW_LABEL = ["follow", "suivre", "continue"]
29
+ NO_LABEL = "NO_LABEL"
30
+ TRICC_LIST_NAME = "list_{0}"
31
+ import logging
32
+ DISPLAY_ATTRIBUTES = [
33
+ 'label',
34
+ 'hint',
35
+ 'help'
36
+ ]
37
+ logger = logging.getLogger("default")
38
+
39
+
40
+
41
+ def get_all_nodes(diagram, activity, nodes):
42
+ for tricc_type in TYPE_MAP:
43
+ if TYPE_MAP[tricc_type]["model"]:
44
+ list = get_tricc_type_list(diagram, TYPE_MAP[tricc_type]["objects"], tricc_type)
45
+ add_tricc_base_node(
46
+ diagram,
47
+ nodes,
48
+ TYPE_MAP[tricc_type]["model"],
49
+ list,
50
+ activity,
51
+ attributes=TYPE_MAP[tricc_type]["attributes"],
52
+ mandatory_attributes=TYPE_MAP[tricc_type]["mandatory_attributes"],
53
+ has_options=TYPE_MAP[tricc_type].get('has_options', None)
54
+ )
55
+
56
+ return nodes
57
+
58
+ def create_activity(diagram, media_path, project):
59
+
60
+ external_id = diagram.attrib.get("id")
61
+ id = get_id( external_id, diagram.attrib.get("id"))
62
+ root = create_root_node(diagram)
63
+ name = diagram.attrib.get("name")
64
+ form_id = diagram.attrib.get("name", None)
65
+ if root is not None:
66
+ activity = TriccNodeActivity(
67
+ root=root,
68
+ name=get_rand_name(f"a{id}"),
69
+ id=id,
70
+ external_id=external_id,
71
+ label=name,
72
+ form_id=form_id,
73
+ )
74
+ if root.relevance is not None:
75
+ activity.applicability=root.relevance
76
+ # activity definition is never instanciated
77
+ if isinstance(root, TriccNodeActivityStart):
78
+ activity.instance = 0
79
+ # add the group on the root node
80
+ root.group = activity
81
+ activity.group = activity
82
+ edges = get_edges(diagram)
83
+ if edges and len(edges) > 0:
84
+ activity.edges = edges
85
+ nodes = get_nodes(diagram, activity)
86
+ for n in nodes.values():
87
+
88
+ if (
89
+ issubclass(n.__class__, (TriccNodeDisplayModel, TriccNodeDisplayCalculateBase))
90
+ and not isinstance(n, (TriccRhombusMixIn, TriccNodeRhombus, TriccNodeDisplayBridge))
91
+ and not n.name.startswith('label_')
92
+ ):
93
+ system = n.name.split('.')[0] if '.' in n.name else 'tricc'
94
+ if isinstance(n, TriccNodeSelectOption) and isinstance(n.select, TriccNodeSelectNotAvailable):
95
+ add_concept(project.code_systems, system, n.select.name, n.label, {"datatype": 'Boolean'})
96
+ elif not isinstance(n, TriccNodeSelectNotAvailable):
97
+ add_concept(project.code_systems, system, n.name, n.label, {"datatype": get_data_type(n.tricc_type)})
98
+ if getattr(n,'save', None):
99
+ system = n.save.split('.')[0] if '.' in n.save else 'tricc'
100
+ add_concept(project.code_systems, system, n.save, n.label, {"datatype": get_data_type(n.tricc_type)})
101
+
102
+ groups = get_groups(diagram, nodes, activity)
103
+ if groups and len(groups) > 0:
104
+ activity.groups = groups
105
+ if nodes and len(nodes) > 0:
106
+ activity.nodes = nodes
107
+
108
+ images = process_edges(diagram, media_path, activity, nodes)
109
+ # link back the activity
110
+ activity.root.activity = activity
111
+ manage_dangling_calculate(activity)
112
+
113
+ if activity is not None:
114
+ if activity.root is not None:
115
+ project.pages[activity.id] = activity
116
+ if activity.root.tricc_type == TriccNodeType.start:
117
+ if "main" not in project.start_pages and (
118
+ activity.root.process == "main" or activity.root.process is None
119
+ ):
120
+ project.start_pages["main"] = activity
121
+ activity.root.process = 'main'
122
+ elif activity.root.process is not None:
123
+ if activity.root.process not in project.start_pages:
124
+ project.start_pages[activity.root.process] = []
125
+ project.start_pages[activity.root.process].append(activity)
126
+ else:
127
+ logger.warning(
128
+ "Page {0} has a start node but there is already a start node in page {1}".format(
129
+ activity.label, start_page.label
130
+ )
131
+ )
132
+ if images:
133
+ project.images += images
134
+ for node in list(filter(lambda p_node: isinstance(p_node, TriccNodeSelectNotAvailable),list(activity.nodes.values()))):
135
+ prev_node = None
136
+ prev_edges = list(filter(lambda p_e: p_e.target == node.id,list(activity.edges)))
137
+ if len(prev_edges):
138
+ prev_node = [n for n in activity.nodes.values() if n.id in [p_e.source for p_e in prev_edges]]
139
+ if prev_node:
140
+ node.parent = prev_node[0]
141
+ if not node.parent :
142
+ logger.critical(f"unable to find the parent of the NotApplicable node {node.get_name()}")
143
+ exit(1)
144
+
145
+
146
+
147
+ else:
148
+ return None, None
149
+ logger.warning("root not found for page {0}".format(name))
150
+
151
+
152
+ def manage_dangling_calculate(activity):
153
+ dangling = {}
154
+ for node in activity.nodes.values():
155
+ prev_nodes = [
156
+ activity.nodes[n.source]
157
+ for n in list(
158
+ filter(
159
+ lambda x: (x.target == node.id or x.target == node)
160
+ and (
161
+ x.source in activity.nodes
162
+ or x.source in activity.nodes.values()
163
+ ),
164
+ activity.edges,
165
+ )
166
+ )
167
+ ]
168
+ if len(prev_nodes) == 0 and issubclass(node.__class__, TriccNodeCalculateBase):
169
+ dangling[node.id] = node
170
+ if len(dangling) > 0:
171
+ activity.calculates += list(dangling.values())
172
+ # wait = get_activity_wait([activity.root], [activity.root], dangling.values(), edge_only=True)
173
+ # activity.nodes.update(dangling)
174
+
175
+
176
+ def process_edges(diagram, media_path, activity, nodes):
177
+ end_found = False
178
+ images = []
179
+ for edge in activity.edges:
180
+ # enrich nodes
181
+ if edge.target not in nodes:
182
+ activity.unused_edges.append(edge)
183
+ elif edge.source not in nodes and edge.target in nodes:
184
+ enriched, image = enrich_node(diagram, media_path, edge, nodes[edge.target], activity)
185
+ if enriched is None:
186
+ activity.unused_edges.append(edge)
187
+ if image is not None:
188
+ images.append({"file_path": enriched, "image_content": image})
189
+
190
+ elif isinstance(nodes[edge.target], (TriccNodeActivityEnd)) or (isinstance(nodes[edge.target], (TriccNodeEnd)) and isinstance(activity.root, TriccNodeMainStart )):
191
+ end_found = True
192
+ if (
193
+ edge.target in nodes
194
+ and issubclass(nodes[edge.target].__class__, TriccRhombusMixIn)
195
+ and edge.source != nodes[edge.target].path.id
196
+ ):
197
+ edge.target = nodes[edge.target].path.id
198
+ # modify edge for selectyesNo
199
+ if edge.source in nodes and isinstance(
200
+ nodes[edge.source], TriccNodeSelectYesNo
201
+ ):
202
+ process_yesno_edge(edge, nodes)
203
+
204
+ # create calculate based on edges label
205
+ elif edge.value is not None:
206
+ label = edge.value.strip()
207
+ processed = False
208
+ calc = None
209
+ if (
210
+ label.lower() in TRICC_FOLLOW_LABEL
211
+ ):
212
+ if isinstance(nodes[edge.source], TriccNodeRhombus):
213
+ edge.source = nodes[edge.source].path.id
214
+ processed = True
215
+ elif label.lower() in (TRICC_YES_LABEL) or label == "":
216
+ # do nothinbg for yes
217
+ processed = True
218
+ elif re.search(r"^\-?[0-9]+([.,][0-9]+)?$", edge.value.strip()):
219
+ calc = process_factor_edge(edge, nodes)
220
+ elif label.lower() in TRICC_NO_LABEL:
221
+ calc = process_exclusive_edge(edge, nodes)
222
+ elif any(reserved in label.lower() for reserved in ([str(o) for o in list(TriccOperator)] + list(OPERATION_LIST.keys()) + ['$this'])):
223
+ # manage comment
224
+ calc = process_condition_edge(edge, nodes)
225
+ else:
226
+ logger.warning(f"unsupported edge label {label} in {diagram.attrib.get('name', diagram.attrib['id'])}")
227
+ processed = True
228
+ if calc is not None:
229
+ processed = True
230
+ nodes[calc.id] = calc
231
+ # add edge between calc and
232
+ set_prev_next_node(calc, nodes[edge.target], edge_only=True)
233
+ edge.target = calc.id
234
+ if not processed:
235
+ logger.warning(
236
+ "Edge between {0} and {1} with label '{2}' could not be interpreted: {3}".format(
237
+ nodes[edge.source].get_name(),
238
+ nodes[edge.target].get_name(),
239
+ edge.value.strip(),
240
+ "not management found",
241
+ )
242
+ )
243
+ if not end_found:
244
+ fake_end = TriccNodeActivityEnd(id=generate_id(f"e{activity.name}"), activity=activity, group=activity)
245
+ last_nodes = [
246
+ n for n in list(activity.nodes.values())
247
+ if (
248
+ issubclass(
249
+ n.__class__,
250
+ (
251
+ TriccNodeInputModel,
252
+ TriccNodeText,
253
+ TriccNodeNote,
254
+ )
255
+ ) and (
256
+ not any([n.id == e.source for e in activity.edges])
257
+ )
258
+ )
259
+ ]
260
+ if last_nodes:
261
+ for n in last_nodes:
262
+ set_prev_next_node(n, fake_end, edge_only=True)
263
+ activity.nodes[fake_end.id] = fake_end
264
+ # take all last nodes
265
+ else:
266
+ logger.warning(f"Activity {activity.label} have no end, calculated might be included in the end definition")
267
+ last_nodes = [
268
+ n for n in list(activity.nodes.values())
269
+ if (
270
+ not any([n.id == e.source for e in activity.edges])
271
+ )
272
+ ]
273
+ if last_nodes:
274
+ for n in last_nodes:
275
+ set_prev_next_node(n, fake_end, edge_only=True)
276
+ activity.nodes[fake_end.id] = fake_end
277
+ else:
278
+ logger.critical(f"cannot guess end for {activity.get_name()}")
279
+ exit(1)
280
+
281
+ return images
282
+
283
+ def get_id(elm_id, activity_id):
284
+ return str(elm_id) if len(elm_id)>8 else str(activity_id) + str(elm_id)
285
+
286
+ def _get_name(name, id, act_id):
287
+ if (
288
+ name is not None
289
+ and (name.endswith(("_", ".")))
290
+ ):
291
+ return name + get_id(id, act_id)
292
+ return name
293
+
294
+
295
+ def get_nodes(diagram, activity):
296
+ nodes = {activity.root.id: activity.root}
297
+ get_all_nodes(diagram, activity, nodes)
298
+ new_nodes = {}
299
+ node_to_remove = []
300
+ activity_end_node = None
301
+ end_node = None
302
+ for node in nodes.values():
303
+ # clean name
304
+ if (
305
+ hasattr(node, "name")
306
+ ):
307
+ node.name = _get_name(node.name, node.id, activity.id)
308
+ if issubclass(node.__class__, TriccRhombusMixIn) and node.path is None:
309
+ # generate rhombuse path
310
+ calc = inject_bridge_path(node, {**nodes, **new_nodes})
311
+ if calc:
312
+ node.path = calc
313
+ new_nodes[calc.id] = calc
314
+ else:
315
+ node.path = activity.root
316
+ # add the edge between trhombus and its path
317
+ elif isinstance(node, TriccNodeGoTo):
318
+ # find if the node has next nodes, if yes, add a bridge + Rhoimbus
319
+ path = inject_bridge_path(node, {**nodes, **new_nodes})
320
+ if path:
321
+ new_nodes[path.id] = path
322
+ else:
323
+ logger.critical(f"goto without in edges {node.get_name()}")
324
+ # action after the activity
325
+ next_nodes_id = [e.target for e in activity.edges if e.source == node.id]
326
+ if len(next_nodes_id) > 0:
327
+
328
+ calc = get_activity_wait(
329
+ path, [node], next_nodes_id, node, edge_only=True
330
+ )
331
+ new_nodes[calc.id] = calc
332
+ for goto_next_node in next_nodes_id:
333
+ remove_prev_next(node, goto_next_node, activity)
334
+ elif isinstance(node, TriccNodeActivityEnd):
335
+ if not activity_end_node:
336
+ activity_end_node = node
337
+ else:
338
+ merge_node(node, activity_end_node)
339
+ node_to_remove.append(node.id)
340
+ # add activity relevance to calculate
341
+ elif (
342
+ issubclass(node.__class__, TriccNodeDisplayCalculateBase)
343
+ and not getattr(node, 'relevance', None)
344
+ and node.activity.applicability
345
+ ):
346
+ node.applicability = node.activity.applicability
347
+
348
+
349
+ nodes.update(new_nodes)
350
+
351
+ for key in node_to_remove:
352
+ del nodes[key]
353
+ edge_list = activity.edges.copy()
354
+ for edge in edge_list:
355
+ if edge.source in node_to_remove or edge.target in node_to_remove:
356
+ activity.edges.remove(edge)
357
+
358
+ return nodes
359
+
360
+
361
+ def create_root_node(diagram):
362
+ node = None
363
+ elm = get_tricc_type(diagram, "object", TriccNodeType.start)
364
+ if elm is None:
365
+ elm = get_tricc_type(diagram, "UserObject", TriccNodeType.start)
366
+ if elm is not None:
367
+ external_id = elm.attrib.get("id")
368
+ id = get_id( external_id, diagram.attrib.get("id"))
369
+ node = TriccNodeMainStart(
370
+ id=id,
371
+ external_id=external_id,
372
+ parent=elm.attrib.get("parent"),
373
+ name="ms" + id,
374
+ label=elm.attrib.get("label"),
375
+ form_id=elm.attrib.get("form_id"),
376
+ relevance=elm.attrib.get("relevance"),
377
+ process=elm.attrib.get('process', 'main'),
378
+ )
379
+ else:
380
+ elm = get_tricc_type(diagram, "object", TriccNodeType.activity_start)
381
+ if elm is None:
382
+ elm = get_tricc_type(diagram, "UserObject", TriccNodeType.activity_start)
383
+ if elm is not None:
384
+ external_id = elm.attrib.get("id")
385
+ id = get_id( external_id, diagram.attrib.get("id"))
386
+ node = TriccNodeActivityStart(
387
+ id=id,
388
+ external_id=external_id,
389
+ #parent=elm.attrib.get("parent"),
390
+ name="ma" + id,
391
+ label=diagram.attrib.get("name"),
392
+ relevance=elm.attrib.get("relevance"),
393
+ instance=int(
394
+ elm.attrib.get("instance")
395
+ if elm.attrib.get("instance") is not None
396
+ else 1
397
+ ),
398
+ )
399
+ load_expressions(node)
400
+ return node
401
+
402
+
403
+ # converter XML item to object
404
+
405
+
406
+ def set_additional_attributes(attribute_names, elm, node):
407
+ if not isinstance(attribute_names, list):
408
+ attribute_names = [attribute_names]
409
+ for attributename in attribute_names:
410
+ attribute = elm.attrib.get(attributename)
411
+ if attribute is not None:
412
+ # input expression can add a condition to either relevance (display) or calculate expression
413
+ if attributename == "expression_inputs":
414
+ attribute = [attribute]
415
+ elif attributename == "instance":
416
+ attribute = int(attribute)
417
+ else:
418
+ attribute
419
+ setattr(node, attributename, attribute)
420
+
421
+
422
+
423
+
424
+ def get_select_options(diagram, select_node, nodes):
425
+ options = {}
426
+ i = 0
427
+ list = get_mxcell_parent_list(diagram, select_node.external_id, TriccNodeType.select_option)
428
+ options_name_list = []
429
+ for elm in list:
430
+ name = elm.attrib.get("name")
431
+ if name in options_name_list and not name.endswith("_"):
432
+ logger.critical(
433
+ "Select question {0} have twice the option name {1}".format(
434
+ select_node.get_name(), name
435
+ )
436
+ )
437
+ else:
438
+ options_name_list.append(name)
439
+
440
+ external_id = elm.attrib.get("id")
441
+ id = get_id(external_id, diagram.attrib.get('id'))
442
+ option = TriccNodeSelectOption(
443
+ id=id,
444
+ label=elm.attrib.get("label"),
445
+ name=name,
446
+ select=select_node,
447
+ list_name=select_node.list_name,
448
+ activity=select_node.activity,
449
+ group=select_node.group,
450
+ )
451
+ set_additional_attributes(["save", "relevance"], elm, option)
452
+ load_expressions(option)
453
+ options[i] = option
454
+ nodes[id] = option
455
+ i += 1
456
+ if len(list) == 0:
457
+ logger.critical("select {} does not have any option".format(select_node.label))
458
+ else:
459
+ return options
460
+
461
+ ##TBR START
462
+
463
+
464
+ def get_max_version(dict):
465
+ max_version = None
466
+ for id, sim_node in dict.items():
467
+ if max_version is None or max_version.version < sim_node.version:
468
+ max_version = sim_node
469
+ return max_version
470
+
471
+
472
+ def update_calc_version(calculates, name):
473
+ if name in calculates and len(calculates[name]) > 1:
474
+ ordered_list = sorted(list(calculates[name].values()), key=lambda x: x.path_len)
475
+ i = 1
476
+ len_max = len(calculates[name])
477
+ for elm in ordered_list:
478
+ elm.version = i
479
+ elm.last = (i == len_max)
480
+ i += 1
481
+
482
+
483
+ def get_max_named_version(calculates, name):
484
+ max = 0
485
+ if name in calculates:
486
+ for node in calculates[name].values():
487
+ if node.version > max:
488
+ max = node.version
489
+ return max
490
+
491
+
492
+
493
+ def inject_bridge_path(node, nodes):
494
+ calc_name = "p" + node.id
495
+ calc_id = generate_id(calc_name)
496
+
497
+ data = {
498
+ "id": calc_id,
499
+ "group": node.group,
500
+ "activity": node.activity,
501
+ "label": "path: " + node.get_name(),
502
+ "name": calc_name,
503
+ "path_len": node.path_len,
504
+ }
505
+ prev_nodes = [
506
+ nodes[n.source]
507
+ for n in list(
508
+ filter(
509
+ lambda x: (x.target == node.id or x.target == node)
510
+ and x.source in nodes,
511
+ node.activity.edges,
512
+ )
513
+ )
514
+ ]
515
+ if (
516
+ sum(
517
+ [
518
+ (
519
+ 0
520
+ if issubclass(
521
+ n.__class__, (TriccNodeDisplayCalculateBase, TriccNodeRhombus)
522
+ )
523
+ else 1
524
+ )
525
+ for n in prev_nodes
526
+ ]
527
+ )
528
+ > 0
529
+ ): # and len(node.prev_nodes)>1:
530
+ calc = TriccNodeDisplayBridge(**data)
531
+ else:
532
+ calc = TriccNodeBridge(**data)
533
+
534
+ for e in node.activity.edges:
535
+ if e.target == node.id:
536
+ if e.source in node.activity.nodes and len(node.activity.nodes[e.source].next_nodes):
537
+ set_prev_next_node(node.activity[e.source], node, edge_only=False, replaced_node=node)
538
+ else:
539
+ e.target = calc.id
540
+
541
+ # add edge between bridge and node
542
+ set_prev_next_node(calc, node, edge_only=True)
543
+
544
+ node.path_len += 1
545
+ return calc
546
+
547
+
548
+ def enrich_node(diagram, media_path, edge, node, activity, help_before=False):
549
+ if edge.target == node.id:
550
+ # get node and process type
551
+ type, message = get_message(diagram, edge.source_external_id)
552
+ if type is not None:
553
+ if type == 'help':
554
+ help = TriccNodeMoreInfo(
555
+ id=generate_id(),
556
+ name=f"{node.name}.more_info",
557
+ label=message,
558
+ parent=node,
559
+ required=None,
560
+ )
561
+ #node.help = message
562
+ if help_before:
563
+ inject_node_before(help, node, activity)
564
+ else:
565
+ set_prev_next_node(node, help, edge_only=True, activity=activity)
566
+ activity.nodes[help.id] = help
567
+ return help, None
568
+
569
+ if type in (TriccNodeType.start, TriccNodeType.activity_start):
570
+ return True
571
+ elif hasattr(node, type):
572
+ if message is not None:
573
+ setattr(node, type, message)
574
+ return True, None
575
+ else:
576
+ logger.warning(
577
+ "A attribute box of type {0} and value {1} is attached to an object not compatible {2}".format(
578
+ type, message, node.get_name()
579
+ )
580
+ )
581
+ return False, None
582
+ else:
583
+ image, payload = get_image(diagram, media_path, edge.source_external_id)
584
+ if image is not None:
585
+ if hasattr(node, "image"):
586
+ node.image = image
587
+ return image, payload
588
+ else:
589
+ logger.warning("image not supported for {} ".format(node.get_name()))
590
+ return None, None
591
+ else:
592
+ logger.warning(f"edge from an unsuported node {edge.source_external_id}")
593
+
594
+ return None, None
595
+
596
+
597
+ get_style_dict = lambda style: dict(item.split('=', 1) for item in style.split(';') if '=' in item)
598
+
599
+
600
+ def severity_from_color(color):
601
+ if color == '#fff2cc':
602
+ return 'moderate'
603
+ elif color == '#f8cecc':
604
+ return 'severe'
605
+ else:
606
+ return 'light'
607
+
608
+
609
+
610
+ def add_tricc_base_node(
611
+ diagram, nodes, type, list, group, attributes=[], mandatory_attributes=[], has_options=None
612
+ ):
613
+ for elm in list:
614
+ external_id = elm.attrib.get("id")
615
+ id = get_id( external_id, diagram.attrib.get("id"))
616
+ parent = elm.attrib.get("parent")
617
+ node = type(
618
+ external_id=external_id,
619
+ id=id,
620
+ #parent=parent,
621
+ group=group,
622
+ activity=group,
623
+ **set_mandatory_attribute(elm, mandatory_attributes, diagram),
624
+ )
625
+ if has_options:
626
+ node.options = get_select_options(diagram, node, nodes)
627
+ for o in node.options:
628
+ nodes[node.options[o].id] = node.options[o]
629
+ elif type == TriccNodeSelectNotAvailable:
630
+ node.options = get_select_not_available_options(node, group, node.label)
631
+ node.label = NO_LABEL
632
+ nodes[node.options[0].id] = node.options[0]
633
+ elif type == TriccNodeSelectYesNo:
634
+ node.list_name = "yes_no"
635
+ node.options = get_select_yes_no_options(node, group)
636
+ nodes[node.options[0].id] = node.options[0]
637
+ nodes[node.options[1].id] = node.options[1]
638
+ elif type == TriccNodeProposedDiagnosis and getattr(node, 'severity', '') == None:
639
+ mxcell = get_mxcell(diagram, external_id)
640
+ styles = get_style_dict(mxcell.attrib.get('style',''))
641
+ if 'fillColor' in styles and styles['fillColor'] != 'none':
642
+ node.severity = severity_from_color(styles['fillColor'])
643
+
644
+
645
+
646
+ set_additional_attributes(attributes, elm, node)
647
+ load_expressions(node)
648
+ nodes[id] = node
649
+
650
+
651
+ def load_expressions(node):
652
+ if getattr(node, 'expression', None):
653
+ node.expression = parse_expression('', node.expression)
654
+ if getattr(node, 'relevance', None):
655
+ node.relevance = parse_expression('', node.relevance)
656
+ if getattr(node, 'default', None):
657
+ node.default = parse_expression('', node.default)
658
+ if getattr(node, 'reference', None):
659
+ if isinstance(node, TriccNodeRhombus):
660
+ node.label = remove_html(node.label)
661
+ node.expression_reference = parse_expression(node.label, node.reference)
662
+ else:
663
+ node.expression_reference = parse_expression('', node.reference)
664
+
665
+ node.reference = node.expression_reference.get_references()
666
+
667
+
668
+
669
+
670
+ def parse_expression(label=None, expression=None):
671
+ if expression:
672
+ ref_pattern = r'(\$\{[^\}]+\})'
673
+ # only if simple ref
674
+ if not re.search(ref_pattern, expression):
675
+ operation = transform_cql_to_operation(expression, label)
676
+ if isinstance(operation, TriccReference):
677
+ if label:
678
+ if label[0] == '[' and label[-1] == ']':
679
+ operation = TriccOperation(
680
+ operator=TriccOperator.SELECTED,
681
+ reference=[
682
+ operation,
683
+ TriccReference(operation.value + label)
684
+ ]
685
+ )
686
+ else:
687
+ for operator in OPERATION_LIST:
688
+ if operator in label:
689
+ if operator == '==':
690
+ operator = '='
691
+ terms = label.split(operator)
692
+ operation = transform_cql_to_operation(
693
+ f"{expression} {operator} {terms[1].replace('?', '').strip()}",
694
+ label
695
+ )
696
+ break
697
+ # implied is true
698
+ if isinstance(operation, TriccReference):
699
+ operation = TriccOperation(
700
+ operator=TriccOperator.ISTRUE,
701
+ reference=[
702
+ operation,
703
+ ]
704
+ )
705
+
706
+ else:
707
+ pass
708
+
709
+ else:
710
+ operation = transform_cql_to_operation(
711
+ expression.replace('${', '"').replace('}', '"'),
712
+ label
713
+ )
714
+
715
+ if operation is None:
716
+ logger.warning(f"unable to parse: {expression} ")
717
+ return expression
718
+ return operation
719
+
720
+
721
+ def set_mandatory_attribute(elm, mandatory_attributes, diagram=None):
722
+ param = {}
723
+ diagram_id = diagram.attrib.get('id')
724
+ for attributes in mandatory_attributes:
725
+ if attributes == 'name':
726
+ name = elm.attrib.get("name")
727
+ id = elm.attrib.get("id")
728
+ attribute_value = _get_name(name, id, diagram_id)
729
+ elif attributes == 'list_name':
730
+ name = elm.attrib.get("name")
731
+ id = elm.attrib.get("id")
732
+ attribute_value = TRICC_LIST_NAME.format(clean_str(_get_name(name, id, diagram_id), replace_dots= True))
733
+ else:
734
+ attribute_value = elm.attrib.get(attributes)
735
+ if attribute_value is None:
736
+ if elm.attrib.get("label") is not None:
737
+ display_name = elm.attrib.get("label")
738
+ elif elm.attrib.get("name") is not None:
739
+ display_name = elm.attrib.get("name")
740
+ else:
741
+ display_name = elm.attrib.get("id")
742
+
743
+ if attributes == "source":
744
+ if elm.attrib.get("target") is not None:
745
+ logger.critical(
746
+ "the attibute target is {}".format(elm.attrib.get("target"))
747
+ )
748
+ elif attributes == "target":
749
+ if elm.attrib.get("source") is not None:
750
+ logger.critical(
751
+ "the attibute target is {}".format(elm.attrib.get("source"))
752
+ )
753
+
754
+ logger.critical(
755
+ "the attibute {} is mandatory but not found in {} within group {}".format(
756
+ attributes, display_name, diagram.attrib.get('name') if diagram is not None else ""
757
+ )
758
+ )
759
+ exit(1)
760
+ if attributes == "link":
761
+ param[attributes] = clean_link(attribute_value)
762
+
763
+ elif attribute_value is not None:
764
+ if attributes in DISPLAY_ATTRIBUTES:
765
+ param[attributes] = remove_html(attribute_value.strip())
766
+ else:
767
+ param[attributes] = attribute_value.strip() if isinstance(attribute_value, str) else attribute_value
768
+ return param
769
+
770
+
771
+ def clean_link(link):
772
+ # link have the format "data:page/id,DiagramID"
773
+ link_parts = link.split(",")
774
+ if link_parts[0] == "data:page/id" and len(link_parts) == 2:
775
+ return link_parts[1]
776
+
777
+
778
+ def get_groups(diagram, nodes, parent_group):
779
+ groups = {}
780
+ list = get_tricc_type_list(diagram, "object", TriccNodeType.page)
781
+ for elm in list:
782
+ add_group(elm, diagram, nodes, groups, parent_group)
783
+ return groups
784
+
785
+
786
+ def add_group(elm, diagram, nodes, groups, parent_group):
787
+ external_id = elm.attrib.get("id")
788
+ id = get_id(external_id, diagram.attrib.get("id"))
789
+ if id not in groups:
790
+ group = TriccGroup(
791
+ name=elm.attrib.get("name"),
792
+ label=elm.attrib.get("label"),
793
+ id=id,
794
+ external_id=external_id,
795
+ group=parent_group,
796
+ )
797
+ # get elememt witn parent = id and tricc_type defiend
798
+ list_child = get_tricc_type_list(
799
+ diagram, ["object", "UserObject"], tricc_type=None, parent_id=id
800
+ )
801
+ add_group_to_child(group, diagram, list_child, nodes, groups, parent_group)
802
+ if group is not None:
803
+ groups[group.id] = group
804
+ return group
805
+
806
+
807
+ def add_group_to_child(group, diagram, list_child, nodes, groups, parent_group):
808
+ for child_elm in list_child:
809
+ if child_elm.attrib.get("tricc_type") == TriccNodeType.container_hint_media:
810
+ list_sub_child = get_tricc_type_list(
811
+ diagram,
812
+ ["object", "UserObject"],
813
+ tricc_type=None,
814
+ parent_id=child_elm.attrib.get("id"),
815
+ )
816
+ add_group_to_child(
817
+ group, diagram, list_sub_child, nodes, groups, parent_group
818
+ )
819
+ elif child_elm.attrib.get("tricc_type") == TriccNodeType.page:
820
+ child_group_id = child_elm.attrib.get("id")
821
+ if not child_group_id in groups:
822
+ child_group = add_group(child_elm, diagram, nodes, groups, group)
823
+ else:
824
+ child_group = groups[child_group_id]
825
+ child_group.group = group
826
+ else:
827
+ child_id = child_elm.attrib.get("id")
828
+ if child_id is not None and child_id in nodes:
829
+ nodes[child_id].group = group
830
+
831
+
832
+ def get_image(diagram, path, id):
833
+ elm = get_mxcell(diagram, id)
834
+ if elm is not None:
835
+ style = elm.attrib.get("style")
836
+ file_name, payload = add_image_from_style(style, path)
837
+ if file_name is not None:
838
+ return file_name, payload
839
+ return None, None
840
+
841
+
842
+ def add_image_from_style(style, path):
843
+ image_attrib = None
844
+ if style is not None and "image=data:image/" in style:
845
+ image_attrib = style.split("image=data:image/")
846
+ if image_attrib is not None and len(image_attrib)== 2:
847
+ image_parts = image_attrib[1].split(",")
848
+ if len(image_parts) == 2:
849
+ payload = image_parts[1][:-1]
850
+ image_name = hashlib.md5(payload.encode('utf-8')).hexdigest()
851
+ path = os.path.join(path, "images")
852
+ file_name = os.path.join(path, image_name + "." + image_parts[0])
853
+ if not (
854
+ os.path.isdir(path)
855
+ ): # check if it exists, because if it does, error will be raised
856
+ # (later change to make folder complaint to CHT)
857
+ os.makedirs(path, exist_ok=True)
858
+ with open(file_name, "wb") as fh:
859
+ fh.write(base64.decodebytes(payload.encode("ascii")))
860
+ image_path = os.path.basename(file_name)
861
+ return image_path, payload
862
+ return None, None
863
+
864
+
865
+ def get_contained_main_node(diagram, id):
866
+ list = get_mxcell_parent_list(diagram, id, media_nodes)
867
+ if isinstance(list, List) and len(list) > 0:
868
+ # use only the first one
869
+ return list[0]
870
+
871
+
872
+ def get_message(diagram, id):
873
+ elm = get_elm(diagram, id)
874
+ if elm is not None:
875
+ type = elm.attrib.get("odk_type")
876
+ if type is not None:
877
+ if type.endswith("-message"):
878
+ type = type[:-8]
879
+ return type, elm.attrib.get("label")
880
+ # use only the first one
881
+ return None, None
882
+
883
+
884
+ def get_edges(diagram):
885
+ edges = []
886
+ list = get_edges_list(diagram)
887
+ for elm in list:
888
+ external_id = elm.attrib.get("id")
889
+ id = get_id(external_id, diagram.attrib.get("id"))
890
+ edge = TriccEdge(
891
+ id=id,
892
+ **set_mandatory_attribute(
893
+ elm, ["source", "parent", "target"], diagram
894
+ ),
895
+ )
896
+ edge.source_external_id = edge.source
897
+ edge.target_external_id = edge.target
898
+ edge.source = get_id(edge.source, diagram.attrib.get("id"))
899
+ edge.target = get_id(edge.target, diagram.attrib.get("id"))
900
+ set_additional_attributes(["value"], elm, edge)
901
+ if edge.value is not None:
902
+ edge.value = remove_html(edge.value)
903
+ edges.append(edge)
904
+ return edges
905
+
906
+
907
+ ## Process edges
908
+
909
+
910
+ def process_factor_edge(edge, nodes):
911
+ factor = edge.value.strip()
912
+ if factor != 1:
913
+ return TriccNodeCalculate(
914
+ id=edge.id,
915
+ expression_reference=TriccOperation(TriccOperator.MULTIPLIED, [nodes[edge.source],TriccStatic(factor)]),
916
+ reference=[nodes[edge.source]],
917
+ activity=nodes[edge.source].activity,
918
+ group=nodes[edge.source].group,
919
+ label="factor {}".format(factor),
920
+ )
921
+ return None
922
+
923
+
924
+ def process_condition_edge(edge, nodes):
925
+ label = edge.value.strip()
926
+ node = nodes[edge.source]
927
+ node_ref = f'"{nodes[edge.source].name}"'
928
+ if '$this' in label:
929
+ operation = parse_expression('', expression=label.replace('$this', node_ref ))
930
+ else:
931
+ operation = parse_expression(label, expression=node_ref)
932
+ if operation and isinstance(operation, TriccOperation):
933
+ # insert rhombus
934
+ return TriccNodeRhombus(
935
+ id=edge.id,
936
+ expression_reference=operation,
937
+ reference=operation.get_references(),
938
+ path=nodes[edge.source],
939
+ activity=nodes[edge.source].activity,
940
+ group=nodes[edge.source].group,
941
+ label=label
942
+ )
943
+
944
+
945
+
946
+
947
+
948
+ def process_exclusive_edge(edge, nodes):
949
+ error = None
950
+ if issubclass(nodes[edge.source].__class__, TriccNodeCalculateBase):
951
+ # insert Negate
952
+ if not isinstance(nodes[edge.target], TriccNodeExclusive) or not isinstance(
953
+ nodes[edge.source], TriccNodeExclusive
954
+ ):
955
+ return TriccNodeExclusive(
956
+ id=edge.id,
957
+ activity=nodes[edge.target].activity,
958
+ group=nodes[edge.target].group,
959
+ )
960
+ else:
961
+ error = "No after or before a exclusice/negate node"
962
+ else:
963
+ error = "label not after a yesno nor a calculate"
964
+ if error is not None:
965
+ logger.warning(
966
+ "Edge between {0} and {1} with label '{2}' could not be interpreted: {3}".format(
967
+ nodes[edge.source].get_name(),
968
+ nodes[edge.target].get_name(),
969
+ edge.value.strip(),
970
+ error,
971
+ )
972
+ )
973
+ return None
974
+
975
+
976
+ def process_yesno_edge(edge, nodes):
977
+ if edge.value is None:
978
+ logger.critical(
979
+ "yesNo {} node with labelless edges".format(nodes[edge.source].get_name())
980
+ )
981
+ exit(1)
982
+ label = edge.value.strip().lower()
983
+ yes_option = None
984
+ no_option = None
985
+ for option in nodes[edge.source].options.values():
986
+ if option.label.lower() == "yes":
987
+ yes_option = option
988
+ else:
989
+ no_option = option
990
+ if label.lower() in TRICC_FOLLOW_LABEL:
991
+ pass
992
+ elif label.lower() in TRICC_YES_LABEL:
993
+ edge.source = yes_option.id
994
+ edge.source_external_id = None
995
+ elif label.lower() in TRICC_NO_LABEL:
996
+ edge.source = no_option.id
997
+ edge.source_external_id = None
998
+ else:
999
+ logger.warning(
1000
+ "edge {0} is coming from select {1}".format(
1001
+ edge.id, nodes[edge.source].get_name()
1002
+ )
1003
+ )