tricc-oo 1.6.12__py3-none-any.whl → 1.6.13__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.
@@ -31,16 +31,16 @@ def extract_properties_metadata(fhir_cs: CodeSystem) -> Dict[str, Dict]:
31
31
 
32
32
  property_types[prop.code] = {
33
33
  "name": prop.code,
34
- "datatype": ocl_type,
34
+ "dataType": ocl_type,
35
35
  "description": prop.description if hasattr(prop, "description") else "",
36
36
  }
37
37
  return property_types
38
38
 
39
39
 
40
40
  def get_fhir_concept_datatype(concept):
41
- datatype = extract_concept_properties(concept, ["datatype"])
41
+ datatype = extract_concept_properties(concept, ["dataType"])
42
42
  if datatype:
43
- return datatype["datatype"]
43
+ return datatype["dataType"]
44
44
  else:
45
45
  return OclConstants.DATA_TYPE_NONE
46
46
 
@@ -84,7 +84,7 @@ def get_attributes_from_concept_properties(concept, property_types: Dict) -> Lis
84
84
  "type": "Attribute",
85
85
  "attribute_type": code,
86
86
  "value": value,
87
- "value_type": property_types[code]["datatype"],
87
+ "value_type": property_types[code]["dataType"],
88
88
  }
89
89
  )
90
90
  return attributes
@@ -103,7 +103,7 @@ def check_and_add_concept(code_system: CodeSystem, code: str, display: str, attr
103
103
  # TODO support other type of Codesystem Concept Property Value
104
104
  existing_attributes
105
105
  if p.valueString != v:
106
- logger.warning(f"conflicting value for property {k}: {p.valueString} != {v}")
106
+ logger.warning(f"conflicting value for concept `{concept.code}` property ` {k}`: {p.valueString} != {v}")
107
107
  if not existing_attributes:
108
108
  new_concept.property.append(CodeSystemConceptProperty(code=k, valueString=v))
109
109
 
@@ -47,7 +47,7 @@ TYPE_MAP = {
47
47
  },
48
48
  TriccNodeType.note: {
49
49
  "objects": ["UserObject", "object"],
50
- "attributes": ["relevance", "priority", "context_type"],
50
+ "attributes": ["relevance", "priority", "concept_type"],
51
51
  "mandatory_attributes": ["label", "name"],
52
52
  "model": TriccNodeNote,
53
53
  },
@@ -76,7 +76,7 @@ TYPE_MAP = {
76
76
  "priority",
77
77
  "trigger",
78
78
  "default",
79
- "context_type",
79
+ "concept_type",
80
80
  ],
81
81
  "mandatory_attributes": ["label", "name", "list_name"],
82
82
  "model": TriccNodeSelectOne,
@@ -94,7 +94,7 @@ TYPE_MAP = {
94
94
  "priority",
95
95
  "trigger",
96
96
  "default",
97
- "context_type",
97
+ "concept_type",
98
98
  ],
99
99
  "mandatory_attributes": ["label", "name", "list_name"],
100
100
  "model": TriccNodeSelectMultiple,
@@ -112,7 +112,7 @@ TYPE_MAP = {
112
112
  "priority",
113
113
  "trigger",
114
114
  "default",
115
- "context_type",
115
+ "concept_type",
116
116
  ],
117
117
  "mandatory_attributes": ["label", "name"],
118
118
  "model": TriccNodeDecimal,
@@ -130,7 +130,7 @@ TYPE_MAP = {
130
130
  "priority",
131
131
  "trigger",
132
132
  "default",
133
- "context_type",
133
+ "concept_type",
134
134
  ],
135
135
  "mandatory_attributes": ["label", "name"],
136
136
  "model": TriccNodeInteger,
@@ -145,7 +145,7 @@ TYPE_MAP = {
145
145
  "default",
146
146
  "constraint",
147
147
  "constraint_message",
148
- "context_type",
148
+ "concept_type",
149
149
  ],
150
150
  "mandatory_attributes": ["label", "name"],
151
151
  "model": TriccNodeText,
@@ -160,7 +160,7 @@ TYPE_MAP = {
160
160
  "default",
161
161
  "constraint",
162
162
  "constraint_message",
163
- "context_type",
163
+ "concept_type",
164
164
  ],
165
165
  "mandatory_attributes": ["label", "name"],
166
166
  "model": TriccNodeDate,
@@ -183,7 +183,7 @@ TYPE_MAP = {
183
183
  "save",
184
184
  "reference",
185
185
  "trigger",
186
- "context_type",
186
+ "concept_type",
187
187
  "remote_reference",
188
188
  ],
189
189
  "mandatory_attributes": ["name", "label"],
@@ -217,7 +217,7 @@ TYPE_MAP = {
217
217
  },
218
218
  TriccNodeType.not_available: {
219
219
  "objects": ["UserObject", "object"],
220
- "attributes": ["context_type"],
220
+ "attributes": ["concept_type"],
221
221
  "mandatory_attributes": ["label", "name", "list_name"],
222
222
  "model": TriccNodeSelectNotAvailable,
223
223
  },
@@ -233,7 +233,7 @@ TYPE_MAP = {
233
233
  "priority",
234
234
  "trigger",
235
235
  "default",
236
- "context_type",
236
+ "concept_type",
237
237
  ],
238
238
  "mandatory_attributes": ["label", "name", "list_name"],
239
239
  "model": TriccNodeSelectYesNo,
@@ -295,7 +295,7 @@ TYPE_MAP = {
295
295
  },
296
296
  TriccNodeType.input: {
297
297
  "objects": ["UserObject", "object"],
298
- "attributes": ["save", "reference", "datatype", "context_type"],
298
+ "attributes": ["save", "reference", "data_type", "concept_type"],
299
299
  "mandatory_attributes": ["name", "label"],
300
300
  "model": TriccNodeInput,
301
301
  },
@@ -128,7 +128,7 @@ def create_activity(diagram, media_path, project):
128
128
  for n in nodes.values():
129
129
 
130
130
  if (
131
- issubclass(n.__class__, (TriccNodeDisplayModel, TriccNodeDisplayCalculateBase))
131
+ issubclass(n.__class__, (TriccNodeDisplayModel, TriccNodeDisplayCalculateBase, TriccNodeInput))
132
132
  and not isinstance(n, (TriccRhombusMixIn, TriccNodeRhombus, TriccNodeDisplayBridge))
133
133
  and not n.name.startswith("label_") # FIXME
134
134
  ):
@@ -139,7 +139,7 @@ def create_activity(diagram, media_path, project):
139
139
  system,
140
140
  n.select.name,
141
141
  n.label,
142
- {"datatype": "Boolean", "contextType": get_context_type(n)},
142
+ {"dataType": "Boolean", "conceptType": get_concept_type(n)},
143
143
  )
144
144
  elif not isinstance(n, TriccNodeSelectNotAvailable):
145
145
  add_concept(
@@ -148,8 +148,20 @@ def create_activity(diagram, media_path, project):
148
148
  n.name,
149
149
  n.label,
150
150
  {
151
- "datatype": get_data_type(n.tricc_type),
152
- "contextType": get_context_type(n),
151
+ "dataType": get_data_type(n.tricc_type),
152
+ "conceptType": get_concept_type(n),
153
+ },
154
+ )
155
+ elif not issubclass(n.__class__, TriccNodeCalculate):
156
+ system = n.name.split(".")[0] if "." in n.name else "calculate"
157
+ add_concept(
158
+ project.code_systems,
159
+ system,
160
+ n.name,
161
+ n.label,
162
+ {
163
+ "dataType": get_data_type(n.tricc_type),
164
+ "conceptType": get_concept_type(n),
153
165
  },
154
166
  )
155
167
  if getattr(n, "save", None):
@@ -160,8 +172,8 @@ def create_activity(diagram, media_path, project):
160
172
  n.save,
161
173
  n.label,
162
174
  {
163
- "datatype": get_data_type(n.tricc_type),
164
- "contextType": get_context_type(n),
175
+ "dataType": get_data_type(n.tricc_type),
176
+ "conceptType": get_concept_type(n),
165
177
  },
166
178
  )
167
179
 
@@ -175,7 +187,7 @@ def create_activity(diagram, media_path, project):
175
187
  # link back the activity
176
188
  activity.root.activity = activity
177
189
  manage_dangling_calculate(activity)
178
-
190
+ # assign the process
179
191
  if activity is not None:
180
192
  if activity.root is not None:
181
193
  project.pages[activity.id] = activity
@@ -197,6 +209,7 @@ def create_activity(diagram, media_path, project):
197
209
  )
198
210
  if images:
199
211
  project.images += images
212
+ # Assign parent to NotAvailable
200
213
  for node in list(
201
214
  filter(
202
215
  lambda p_node: isinstance(p_node, TriccNodeSelectNotAvailable),
@@ -477,10 +490,10 @@ def set_additional_attributes(attribute_names, elm, node):
477
490
  setattr(node, attributename, attribute)
478
491
 
479
492
 
480
- def get_context_type(node):
481
- context_type = getattr(node, "context_type", None)
482
- if context_type:
483
- return context_type
493
+ def get_concept_type(node):
494
+ concept_type = getattr(node, "concept_type", None)
495
+ if concept_type:
496
+ return concept_type
484
497
  if isinstance(node, TriccNodeSelectMultiple):
485
498
  return "Question"
486
499
  elif isinstance(node, TriccNodeSelectOption):
@@ -535,7 +548,7 @@ def get_select_options(diagram, select_node, nodes):
535
548
  activity=select_node.activity,
536
549
  group=select_node.group,
537
550
  )
538
- set_additional_attributes(["save", "relevance", "context_type"], elm, option)
551
+ set_additional_attributes(["save", "relevance", "concept_type"], elm, option)
539
552
  load_expressions(option)
540
553
  options[i] = option
541
554
  nodes[id] = option
@@ -802,9 +815,11 @@ def set_mandatory_attribute(elm, mandatory_attributes, diagram=None):
802
815
  id = elm.attrib.get("id")
803
816
  attribute_value = _get_name(name, id, diagram_id)
804
817
  elif attributes == "list_name":
805
- name = elm.attrib.get("name")
806
- id = elm.attrib.get("id")
807
- attribute_value = TRICC_LIST_NAME.format(clean_str(_get_name(name, id, diagram_id), replace_dots=True))
818
+ attribute_value = elm.attrib.get("list_name", None)
819
+ if not attribute_value:
820
+ name = elm.attrib.get("name")
821
+ id = elm.attrib.get("id")
822
+ attribute_value = TRICC_LIST_NAME.format(clean_str(_get_name(name, id, diagram_id), replace_dots=True))
808
823
  else:
809
824
  attribute_value = elm.attrib.get(attributes)
810
825
  if attribute_value is None:
@@ -32,6 +32,7 @@ from tricc_oo.visitors.tricc import (
32
32
  get_applicability_expression,
33
33
  get_prev_instance_skip_expression,
34
34
  get_process_skip_expression,
35
+ process_operation_reference,
35
36
  )
36
37
 
37
38
  logger = logging.getLogger("default")
@@ -64,7 +65,36 @@ def start_group(
64
65
  groups[name] = 0
65
66
  relevance = relevance and cur_group.relevance is not None and cur_group.relevance != ""
66
67
  past_instances = len(getattr(cur_group.base_instance, "instances", []))
68
+ group_calc_required = relevance is not None and (len(str(relevance)) > 100 or past_instances > 1)
67
69
  calc = None
70
+ if group_calc_required and getattr(cur_group.relevance, 'operator', None) != TriccOperator.ISTRUE:
71
+
72
+ calc = TriccNodeCalculate(
73
+ id=generate_id(get_export_group_name(name)),
74
+ group=cur_group.group,
75
+ activity=cur_group.activity,
76
+ name=get_export_group_name(name),
77
+ expression=cur_group.relevance
78
+ )
79
+
80
+ if calc not in cur_group.activity.calculates:
81
+ process_reference(
82
+ calc,
83
+ processed_nodes,
84
+ calculates=kwargs.get('calculates', None),
85
+ used_calculates=kwargs.get('used_calculates', None),
86
+ replace_reference=True,
87
+ warn=False,
88
+ codesystems=kwargs.get('codesystems', None)
89
+ )
90
+ cur_group.activity.calculates.append(calc)
91
+ cur_group.activity.nodes[calc.id] = calc
92
+ processed_nodes.add(calc)
93
+
94
+ cur_group.relevance = TriccOperation(
95
+ TriccOperator.ISTRUE,
96
+ [calc]
97
+ )
68
98
 
69
99
  relevance_expression = cur_group.relevance
70
100
  relevance_expression = get_applicability_expression(cur_group, processed_nodes, process, relevance_expression)
@@ -74,7 +104,18 @@ def start_group(
74
104
  if not relevance:
75
105
  relevance_expression_str = ""
76
106
  elif isinstance(relevance_expression, (TriccOperation, TriccStatic)):
77
- relevance_expression_str = strategy.get_tricc_operation_expression(relevance_expression)
107
+ relevance_expression = process_operation_reference(
108
+ relevance_expression,
109
+ cur_group,
110
+ processed_nodes=processed_nodes,
111
+ calculates=kwargs.get('calculates', None),
112
+ used_calculates=kwargs.get('used_calculates', None),
113
+ replace_reference=True,
114
+ warn=False,
115
+ codesystems=kwargs.get('codesystems', None),
116
+ ) or relevance_expression
117
+ if relevance_expression:
118
+ relevance_expression_str = strategy.get_tricc_operation_expression(relevance_expression)
78
119
 
79
120
  # group
80
121
  values = []
@@ -95,7 +136,23 @@ def start_group(
95
136
  values.append(get_xfrom_trad(strategy, cur_group, column, SURVEY_MAP))
96
137
  df_survey.loc[len(df_survey)] = values
97
138
 
98
- # Group calculates are now created during process_calculate phase
139
+ # calc
140
+ if calc and len(df_calculate[df_calculate["name"] == get_export_group_name(name)]) == 0:
141
+ calc_values = []
142
+ for column in SURVEY_MAP:
143
+ if column == "type":
144
+ calc_values.append("calculate")
145
+ elif column == "name":
146
+ value = get_export_name(calc)
147
+ calc_values.append(value)
148
+ elif column == "calculation":
149
+ calc_values.append(f"number({strategy.get_tricc_operation_expression(calc.expression)})")
150
+ elif column == "relevance":
151
+ calc_values.append("")
152
+ else:
153
+ calc_values.append(get_xfrom_trad(strategy, cur_group, column, SURVEY_MAP))
154
+
155
+ df_calculate.loc[len(df_calculate)] = calc_values
99
156
 
100
157
 
101
158
  # def add_background_color(input_string, color):
@@ -16,7 +16,7 @@ import datetime
16
16
  from tricc_oo.strategies.output.base_output_strategy import BaseOutPutStrategy
17
17
  from tricc_oo.models.base import (
18
18
  not_clean, TriccOperation,
19
- TriccStatic, TriccReference, TriccOperator
19
+ TriccStatic, TriccReference
20
20
  )
21
21
  from tricc_oo.models.tricc import (
22
22
  TriccNodeSelectOption,
@@ -27,8 +27,6 @@ from tricc_oo.models.tricc import (
27
27
  TriccNodeActivity,
28
28
  TriccNodeSelect,
29
29
  TriccNodeSelectYesNo,
30
- TriccNodeNote,
31
- TriccNodeMoreInfo,
32
30
  )
33
31
  from tricc_oo.models.calculate import TriccNodeDisplayCalculateBase
34
32
  from tricc_oo.models.ordered_set import OrderedSet
@@ -46,7 +44,7 @@ class DHIS2Strategy(BaseOutPutStrategy):
46
44
 
47
45
  def __init__(self, project, output_path):
48
46
  super().__init__(project, output_path)
49
- form_id = getattr(self.project.start_pages["main"].root, 'form_id', 'dhis2_program')
47
+ form_id = getattr(self.project.start_pages["main"], 'form_id', 'dhis2_program')
50
48
  self.program_metadata = {
51
49
  "id": self.generate_id(form_id),
52
50
  "name": form_id,
@@ -206,17 +204,13 @@ class DHIS2Strategy(BaseOutPutStrategy):
206
204
  return False
207
205
 
208
206
  if node not in processed_nodes:
209
- # Skip relevance generation for TriccNodeMoreInfo as they don't create DataElements
210
- if isinstance(node, TriccNodeMoreInfo):
211
- return True
212
-
213
207
  relevance = None
214
208
  if hasattr(node, 'relevance') and node.relevance:
215
209
  relevance = node.relevance
216
210
  if hasattr(node, 'expression') and node.expression:
217
211
  relevance = node.expression
218
212
  if relevance:
219
- relevance_str = self.convert_expression_to_string(relevance)
213
+ relevance_str = self.convert_expression_to_string(not_clean(relevance))
220
214
  if relevance_str and relevance_str != 'false':
221
215
  # Create program rule action for hiding/showing based on relevance
222
216
  rule_id = self.generate_id(f"rule_{node.get_name()}_relevance")
@@ -231,10 +225,8 @@ class DHIS2Strategy(BaseOutPutStrategy):
231
225
  "activity_ref": node, # Temporary reference to be replaced with section ID
232
226
  "programRule": {"id": rule_id},
233
227
  }
234
- self.program_rule_actions.append(program_rule_action)
235
- elif issubclass(node.__class__, TriccNodeDisplayModel) and not issubclass(node.__class__, TriccNodeCalculateBase) and not isinstance(node, (TriccNodeNote, TriccNodeMoreInfo, TriccNodeSelectOption)):
236
- # For regular nodes that get DataElements, use HIDEFIELD action
237
- # Exclude TriccNodeNote and TriccNodeMoreInfo as they don't get DataElements
228
+ else:
229
+ # For regular nodes, use HIDEFIELD action
238
230
  program_rule_action = {
239
231
  "id": action_id,
240
232
  "programRuleActionType": "HIDEFIELD",
@@ -243,45 +235,18 @@ class DHIS2Strategy(BaseOutPutStrategy):
243
235
  },
244
236
  "programRule": {"id": rule_id}
245
237
  }
246
- self.program_rule_actions.append(program_rule_action)
238
+ self.program_rule_actions.append(program_rule_action)
247
239
 
248
240
  # Create program rule referencing the action
249
- condition = self.simplify_expression(f"({relevance_str})==false") # Negate for hide when true
241
+ condition = self.simplify_expression(f"!({relevance_str})") # Negate for hide when true
250
242
  condition = self.simplify_expression(condition)
251
243
  self.program_rules.append({
252
244
  "id": rule_id,
253
245
  "name": f"Hide `{self.get_export_name(node)}` when condition met",
254
- "description": f"Hide `{self.get_display(node)[:128]}` based on relevance",
246
+ "description": f"Hide `{self.get_display(node)}` based on relevance",
255
247
  "condition": condition,
256
248
  "programRuleActions": [{"id": action_id}]
257
249
  })
258
-
259
- # Check if field should be mandatory based on 'required' attribute
260
- if bool(getattr(node, 'required', False)):
261
- # Create program rule for mandatory field using relevance function
262
- mandatory_rule_id = self.generate_id(f"rule_{node.get_name()}_mandatory")
263
- mandatory_action_id = self.generate_id(f"action_{mandatory_rule_id}")
264
-
265
- mandatory_program_rule_action = {
266
- "id": mandatory_action_id,
267
- "programRuleActionType": "SETMANDATORYFIELD",
268
- "dataElement": {
269
- "id": self.generate_id(self.get_export_name(node))
270
- },
271
- "programRule": {"id": mandatory_rule_id}
272
- }
273
- self.program_rule_actions.append(mandatory_program_rule_action)
274
-
275
- # Create program rule for mandatory field - use relevance condition if available
276
- mandatory_condition = relevance_str if relevance_str and relevance_str != 'false' else "true"
277
- self.program_rules.append({
278
- "id": mandatory_rule_id,
279
- "name": f"Make `{self.get_export_name(node)}` mandatory",
280
- "description": f"Set `{self.get_display(node)[:128]}` as mandatory field",
281
- "condition": mandatory_condition,
282
- "programRuleActions": [{"id": mandatory_action_id}]
283
- })
284
-
285
250
  return True
286
251
 
287
252
  def generate_data_element(self, node):
@@ -309,8 +274,8 @@ class DHIS2Strategy(BaseOutPutStrategy):
309
274
  "id": de_id,
310
275
  "name": self.get_export_name(node),
311
276
  "shortName": node.name[:50],
312
- "displayFormName": "More infromation" if isinstance(node, TriccNodeMoreInfo) else self.get_display(node),
313
- "formName": "More infromation" if isinstance(node, TriccNodeMoreInfo) else self.get_display(node),
277
+ "displayFormName": self.get_display(node),
278
+ "formName": self.get_display(node),
314
279
  "valueType": value_type,
315
280
  "domainType": "TRACKER",
316
281
  "aggregationType": "NONE"
@@ -547,36 +512,8 @@ class DHIS2Strategy(BaseOutPutStrategy):
547
512
  return False
548
513
 
549
514
  if node not in processed_nodes:
550
- logger.debug(f"generate_export processing node: {node.get_name()} type: {type(node)}")
551
- # Special handling for TriccNodeNote - transform into section description
552
- if isinstance(node, TriccNodeNote):
553
- logger.info(f"Found TriccNodeNote: {node.get_name()}")
554
- self.handle_note_as_section_description(node, processed_nodes, **kwargs)
555
- elif isinstance(node, TriccNodeMoreInfo):
556
- logger.info(f"Found TriccNodeMoreInfo: {node.get_name()}")
557
- # Create boolean checkbox data element for moreinfo
558
- data_element = self.generate_data_element(node)
559
- if data_element:
560
- # Add to program stage
561
- if self.program_metadata["programStages"]:
562
- psde_id = self.generate_id(f"psde_{node.name}")
563
- psde = {
564
- "id": psde_id,
565
- "dataElement": {"id": data_element["id"]},
566
- "compulsory": False
567
- }
568
- self.program_metadata["programStages"][-1]["programStageDataElements"].append(psde)
569
-
570
- # Add data element to current section
571
- if self.current_section and self.current_section in self.sections:
572
- self.sections[self.current_section]["dataElements"].append({"id": data_element["id"]})
573
-
574
- # Handle the note section with checkbox conditioning
575
- checkbox_reference = TriccReference(node.name)
576
- checkbox_condition = TriccOperation(TriccOperator.ISTRUE, [checkbox_reference])
577
- self.handle_note_as_section_description(node, processed_nodes, additional_condition=checkbox_condition, **kwargs)
578
515
  # Skip creating data elements for calculate nodes - they should only be program rule variables
579
- elif not issubclass(node.__class__, TriccNodeCalculateBase):
516
+ if not issubclass(node.__class__, TriccNodeCalculateBase):
580
517
  data_element = self.generate_data_element(node)
581
518
  if data_element:
582
519
  # Add to program stage
@@ -585,7 +522,7 @@ class DHIS2Strategy(BaseOutPutStrategy):
585
522
  psde = {
586
523
  "id": psde_id,
587
524
  "dataElement": {"id": data_element["id"]},
588
- "compulsory": False
525
+ "compulsory": bool(getattr(node, 'required', False))
589
526
  }
590
527
  self.program_metadata["programStages"][-1]["programStageDataElements"].append(psde)
591
528
 
@@ -595,136 +532,6 @@ class DHIS2Strategy(BaseOutPutStrategy):
595
532
 
596
533
  return True
597
534
 
598
- def handle_note_as_section_description(self, node, processed_nodes, additional_condition=None, **kwargs):
599
- """Transform TriccNodeNote into section description and create hide logic with section duplication
600
-
601
- Args:
602
- node: The node to process (TriccNodeNote or TriccNodeMoreInfo)
603
- processed_nodes: Set of already processed nodes
604
- additional_condition: Optional additional condition to combine with existing relevance
605
- """
606
- logger.info(f"Processing note {node.get_name()} for section description")
607
- if not self.current_section or self.current_section not in self.sections:
608
- logger.warning(f"No current section found for note {node.get_name()}")
609
- return
610
-
611
- # 1. Duplicate the previous section with incremented ID (like XLS form group numbering)
612
- original_section = self.sections[self.current_section]
613
- section_name = f"{original_section['name']}_{node.name}"
614
- duplicated_section_id = self.generate_id(section_name)
615
- duplicated_section = {
616
- "id": duplicated_section_id,
617
- "name": section_name, # Use incremented naming pattern
618
- "sortOrder": len(self.sections),
619
- "programStage": {"id": self.program_metadata["programStages"][-1]["id"]},
620
- "dataElements": original_section.get("dataElements", []).copy(), # Copy data elements
621
- "activity_ref": original_section.get("activity_ref")
622
- }
623
-
624
- # 2. Create a new section for the note/moreinfo with name and ID from the node
625
- note_section_id = self.generate_id(self.get_export_name(node))
626
- note_section_name = self.get_export_name(node)
627
-
628
- note_section = {
629
- "id": note_section_id,
630
- "name": note_section_name,
631
- "sortOrder": len(self.sections),
632
- "programStage": {"id": self.program_metadata["programStages"][-1]["id"]},
633
- "dataElements": [],
634
- "activity_ref": original_section.get("activity_ref") # Same activity reference
635
- }
636
-
637
- # Set section description to the note's label
638
- if hasattr(node, 'label') and node.label:
639
- note_section["description"] = node.label.replace('\u00a0', ' ').strip()
640
- logger.info(f"Set note section {note_section_id} description to: {note_section['description']}")
641
- elif hasattr(node, 'name') and node.name:
642
- note_section["description"] = node.name.replace('\u00a0', ' ').strip()
643
- logger.info(f"Set note section {note_section_id} description to: {note_section['description']}")
644
- else:
645
- note_section["description"] = str(node.id)
646
- logger.info(f"Set note section {note_section_id} description to: {note_section['description']}")
647
-
648
- self.sections[note_section_id] = note_section
649
-
650
- # Add note section to program stage
651
- if self.program_metadata["programStages"]:
652
- self.program_metadata["programStages"][-1]["programStageSections"].append({"id": note_section_id})
653
-
654
- logger.info(f"Created note section {note_section_id} with name '{note_section_name}'")
655
-
656
- # 3. Inject the duplicated section as the new current section
657
- self.sections[duplicated_section_id] = duplicated_section
658
-
659
- # Add duplicated section to program stage
660
- if self.program_metadata["programStages"]:
661
- self.program_metadata["programStages"][-1]["programStageSections"].append({"id": duplicated_section_id})
662
-
663
- logger.info(f"Duplicated section {self.current_section} as {duplicated_section_id}")
664
-
665
- self.current_section = duplicated_section_id
666
- logger.info(f"Set current section to duplicated section {duplicated_section_id}")
667
-
668
- # Create hide logic for the note section based on combined relevance
669
- combined_relevance = None
670
-
671
- # Get parent activity relevance from the original section's activity_ref
672
- parent_activity = original_section.get("activity_ref")
673
- if parent_activity and hasattr(parent_activity, 'relevance') and parent_activity.relevance:
674
- combined_relevance = parent_activity.relevance
675
-
676
- # Combine with note relevance if it exists
677
- if hasattr(node, 'relevance') and node.relevance:
678
- if combined_relevance:
679
- # Combine using AND operation
680
- combined_relevance = TriccOperation(
681
- TriccOperator.AND,
682
- [combined_relevance, node.relevance]
683
- )
684
- else:
685
- combined_relevance = node.relevance
686
-
687
- # Combine with additional condition if provided
688
- if additional_condition:
689
- if combined_relevance:
690
- # Combine using AND operation
691
- combined_relevance = TriccOperation(
692
- TriccOperator.AND,
693
- [combined_relevance, additional_condition]
694
- )
695
- else:
696
- combined_relevance = additional_condition
697
-
698
- # Create hide logic if there's relevance - applied to the note section
699
- if combined_relevance:
700
- relevance_str = self.convert_expression_to_string(not_clean(combined_relevance))
701
- if relevance_str and relevance_str != 'false':
702
- # Create program rule action for hiding the note section
703
- rule_id = self.generate_id(f"rule_{node.get_name()}_note_hide_section")
704
- action_id = self.generate_id(f"action_{rule_id}")
705
-
706
- program_rule_action = {
707
- "id": action_id,
708
- "programRuleActionType": "HIDESECTION",
709
- "activity_ref": parent_activity, # Use activity reference like other HIDESECTION actions
710
- "programRule": {"id": rule_id},
711
- }
712
- self.program_rule_actions.append(program_rule_action)
713
-
714
- # Create program rule referencing the action
715
- condition = self.simplify_expression(f"({relevance_str})==false") # Negate for hide when true
716
- condition = self.simplify_expression(condition)
717
- rule_name = f"Hide note section `{note_section_name}` based on relevance"
718
- if additional_condition:
719
- rule_name = f"Hide moreinfo section `{note_section_name}` when checkbox false"
720
- self.program_rules.append({
721
- "id": rule_id,
722
- "name": rule_name,
723
- "description": f"Hide note section `{self.get_display(node)[:128]}` based on combined relevance",
724
- "condition": condition,
725
- "programRuleActions": [{"id": action_id}]
726
- })
727
-
728
535
  def clean_section(self, program_stages_payload):
729
536
  """Clean sections by removing empty ones and merging sections with same activity_ref"""
730
537
  sections_to_remove = set()
@@ -735,11 +542,11 @@ class DHIS2Strategy(BaseOutPutStrategy):
735
542
  section_id = section["id"]
736
543
  activity_ref = section.get("activity_ref")
737
544
  # Remove empty sections
738
- if not section.get("dataElements") and not section.get("description"):
545
+ if not section.get("dataElements"):
739
546
  sections_to_remove.add(section_id)
740
547
 
741
548
  # Check for sections with same activity_ref
742
- elif activity_ref == prev_activity_ref and not section.get("description"):
549
+ elif activity_ref == prev_activity_ref:
743
550
  # Merge this section into the existing one
744
551
  existing_section = self.sections[prev_section_id]
745
552
 
@@ -838,8 +645,6 @@ class DHIS2Strategy(BaseOutPutStrategy):
838
645
  # Non-activity actions (HIDEFIELD) can be added directly
839
646
  program_rule_actions_payload.append(action)
840
647
 
841
- # Filter out rules that reference non-existent actions
842
- valid_action_ids = {action["id"] for action in program_rule_actions_payload}
843
648
  if self.program_rules:
844
649
  program_rules_payload = [
845
650
  {
@@ -847,7 +652,6 @@ class DHIS2Strategy(BaseOutPutStrategy):
847
652
  "program": {"id": self.program_metadata["id"]}
848
653
  }
849
654
  for rule in self.program_rules
850
- if all(action_ref["id"] in valid_action_ids for action_ref in rule["programRuleActions"])
851
655
  ]
852
656
 
853
657
  if self.program_rule_variables:
@@ -906,7 +710,7 @@ class DHIS2Strategy(BaseOutPutStrategy):
906
710
  return self.get_tricc_operation_expression(r)
907
711
  elif isinstance(r, TriccReference):
908
712
  # Use variable name from concept_map
909
- node_id = self.concept_map.get(r.value, self.get_export_name(r.value))
713
+ node_id = self.concept_map.get(r.value.name, self.get_export_name(r.value))
910
714
  return f"#{{{node_id}}}"
911
715
  elif isinstance(r, TriccStatic):
912
716
  if isinstance(r.value, bool):
@@ -97,7 +97,7 @@ class FHIRStrategy(BaseOutPutStrategy):
97
97
  "type": self.map_tricc_type_to_fhir(node.tricc_type if hasattr(node, 'tricc_type') else 'text')
98
98
  }
99
99
  if hasattr(node, 'options') and node.options:
100
- item["answerOption"] = [{"valueString": opt.name} for opt in node.options.values()]
100
+ item["answerOption"] = [{"valueString": opt.name} for opt in node.options]
101
101
  self.questionnaires[segment]["item"].append(item)
102
102
  return True
103
103
 
@@ -829,7 +829,7 @@ class XLSFormCHTStrategy(XLSFormCDSSStrategy):
829
829
  return jar_path
830
830
 
831
831
  # Download JAR from GitHub releases
832
- jar_url = "https://github.com/getodk/validate/releases/download/v1.19.2/validate.jar"
832
+ jar_url = "https://github.com/getodk/validate/releases/download/v1.20.0/ODK-Validate-v1.20.0.jar"
833
833
  try:
834
834
  import urllib.request
835
835
  urllib.request.urlretrieve(jar_url, jar_path)
@@ -57,16 +57,6 @@ logger = logging.getLogger("default")
57
57
  ONE_QUESTION_AT_A_TIME = False
58
58
 
59
59
 
60
- def get_main_activity(activity, processed_nodes):
61
- """Traverse up the activity hierarchy to find the main activity (root activity)."""
62
- activities = list(set(
63
- n.activity for n in activity.relevance.get_references()
64
- if hasattr(n, 'activity') and n.activity != activity and n.activity.root in processed_nodes
65
- ))
66
- if activities:
67
- return activities[0]
68
-
69
-
70
60
  def merge_node(from_node, to_node):
71
61
  if from_node.activity != to_node.activity:
72
62
  logger.critical("Cannot merge nodes from different activities")
@@ -133,7 +123,7 @@ def get_last_version(name, processed_nodes, _list=None):
133
123
  def get_node_expressions(node, processed_nodes, process=None):
134
124
  get_overall_exp = issubclass(
135
125
  node.__class__,
136
- (TriccNodeDisplayCalculateBase, TriccNodeProposedDiagnosis, TriccNodeDiagnosis, TriccNodeActivity)
126
+ (TriccNodeDisplayCalculateBase, TriccNodeProposedDiagnosis, TriccNodeDiagnosis)
137
127
  ) and not isinstance(node, (TriccNodeDisplayBridge))
138
128
  expression = None
139
129
  # in case of recursive call processed_nodes will be None
@@ -195,9 +185,9 @@ def get_version_inheritance(node, last_version, processed_nodes):
195
185
  path_len=node.path_len + 1,
196
186
  expression_reference=TriccOperation(
197
187
  TriccOperator.COALESCE,
198
- [TriccReference(node.save), last_version],
188
+ [node, last_version],
199
189
  ),
200
- reference=[TriccReference(node.name)],
190
+ reference=[node, last_version],
201
191
  activity=node.activity,
202
192
  group=node.group,
203
193
  label=f"Save calculation for {node.label}",
@@ -255,29 +245,7 @@ def load_calculate(
255
245
  if hasattr(node, "relevance") and (node.relevance is None or isinstance(node.relevance, TriccOperation)):
256
246
  node.relevance = get_node_expressions(node, processed_nodes=processed_nodes, process=process)
257
247
  # manage not Available
258
- if isinstance(node, TriccNodeActivity) and isinstance(node.root, TriccNodeActivityStart):
259
- past_instances = len(getattr(node.base_instance, "instances", []))
260
- group_calc_required = False # (len(str(node.relevance)) > 50 or past_instances > 1 or len(node.calculates)>0)
261
- calc = None
262
-
263
- if group_calc_required and getattr(node.relevance, 'operator', None) != TriccOperator.ISTRUE:
264
- main_activity = get_main_activity(node, processed_nodes)
265
- if main_activity:
266
- calc = TriccNodeCalculate(
267
- id=generate_id(f"gc.{node.id}"),
268
- group=main_activity.group,
269
- activity=main_activity,
270
- name=f"gc.{node.name}",
271
- expression_reference=node.relevance.copy()
272
- )
273
- main_activity.calculates.append(calc)
274
- main_activity.nodes[calc.id] = calc
275
- processed_nodes.add(calc)
276
- node.relevance = TriccOperation(
277
- TriccOperator.ISTRUE,
278
- [calc]
279
- )
280
- elif isinstance(node, TriccNodeSelectNotAvailable):
248
+ if isinstance(node, TriccNodeSelectNotAvailable):
281
249
  # update the checkbox
282
250
  if node.parent:
283
251
  if len(node.prev_nodes) == 1:
@@ -837,7 +805,7 @@ def process_operation_reference(
837
805
  operation,
838
806
  node,
839
807
  processed_nodes,
840
- calculates,
808
+ calculates=None,
841
809
  used_calculates=None,
842
810
  replace_reference=False,
843
811
  warn=False,
@@ -1767,21 +1735,22 @@ def replace_next_node(prev_node, next_node, old_node):
1767
1735
 
1768
1736
 
1769
1737
  # Priority constants
1770
- SAME_GROUP_PRIORITY = 4000
1771
- PARENT_GROUP_PRIORITY = 3000
1772
- ACTIVE_ACTIVITY_PRIORITY = 1400
1773
- NON_START_ACTIVITY_PRIORITY = 1300
1774
- ACTIVE_ACTIVITY_LOWER_PRIORITY = 1200
1775
- FLOW_CALCULATE_NODE_PRIORITY = 2000
1776
- RHOMBUS_PRIORITY = 1000
1777
- DEFAULT_PRIORITY = 1100
1778
-
1779
-
1738
+ SAME_GROUP_PRIORITY = 7000
1739
+ PARENT_GROUP_PRIORITY = 6000
1740
+ ACTIVE_ACTIVITY_PRIORITY = 5000
1741
+ NON_START_ACTIVITY_PRIORITY = 4000
1742
+ ACTIVE_ACTIVITY_LOWER_PRIORITY = 3000
1743
+ FLOW_CALCULATE_NODE_PRIORITY_TOP_UP = 50
1744
+ RHOMBUS_PRIORITY_TO_UP = 50
1745
+
1746
+
1780
1747
  def reorder_node_list(node_list, group, processed_nodes):
1781
1748
  # Cache active activities for O(1) lookup
1782
1749
  active_activities = {n.activity for n in processed_nodes}
1783
-
1750
+ MAP_PRIORITIES = {}
1784
1751
  def get_priority(node):
1752
+ if node.id in MAP_PRIORITIES:
1753
+ return MAP_PRIORITIES[node.id]
1785
1754
  # Cache attributes to avoid repeated getattr calls
1786
1755
  priority = int(getattr(node, "priority", 0) or 0)
1787
1756
  node_group = getattr(node, "group", None)
@@ -1789,12 +1758,7 @@ def reorder_node_list(node_list, group, processed_nodes):
1789
1758
 
1790
1759
  # Check for same group
1791
1760
  if group is not None and node_group and node_group.id == group.id:
1792
- priority += SAME_GROUP_PRIORITY
1793
- elif (
1794
- issubclass(node.__class__, TriccNodeDisplayCalculateBase) or
1795
- isinstance(node, TriccNodeEnd)
1796
- ) and not isinstance(node, TriccNodeActivityEnd) and hasattr(node, 'prev_nodes') and len(node.prev_nodes) > 0:
1797
- priority += FLOW_CALCULATE_NODE_PRIORITY
1761
+ priority += SAME_GROUP_PRIORITY
1798
1762
  # Check for parent group
1799
1763
  elif hasattr(group, "group") and group.group and node_group and node_group.id == group.group.id:
1800
1764
  priority += PARENT_GROUP_PRIORITY
@@ -1808,13 +1772,21 @@ def reorder_node_list(node_list, group, processed_nodes):
1808
1772
  elif activity and activity in active_activities:
1809
1773
  priority += ACTIVE_ACTIVITY_LOWER_PRIORITY
1810
1774
  # Check for rhombus nodes
1811
- elif issubclass(node.__class__, TriccRhombusMixIn):
1812
- priority += RHOMBUS_PRIORITY
1813
- else:
1814
- priority += DEFAULT_PRIORITY
1815
1775
 
1816
- priority = max(priority, max((get_priority(prev_node) for prev_node in node.prev_nodes), default=0))
1817
1776
 
1777
+ if (
1778
+ issubclass(node.__class__, TriccNodeDisplayCalculateBase) or
1779
+ isinstance(node, TriccNodeEnd)
1780
+ ) and not isinstance(node, TriccNodeActivityEnd) and hasattr(node, 'prev_nodes') and len(node.prev_nodes) > 0:
1781
+ priority += FLOW_CALCULATE_NODE_PRIORITY_TOP_UP
1782
+ elif issubclass(node.__class__, TriccRhombusMixIn):
1783
+ priority += RHOMBUS_PRIORITY_TO_UP
1784
+
1785
+ if node.prev_nodes:
1786
+ priority = max(priority, *[get_priority(p) for p in node.prev_nodes])
1787
+
1788
+ MAP_PRIORITIES[node.id] = priority
1789
+
1818
1790
  return priority
1819
1791
 
1820
1792
  # Sort in place, highest priority first
@@ -1863,7 +1835,6 @@ def get_node_expression(in_node, processed_nodes, get_overall_exp=False, is_prev
1863
1835
  expression = None
1864
1836
  negate_expression = None
1865
1837
  node = in_node
1866
-
1867
1838
  if isinstance(node, (TriccNodeActivityStart, TriccNodeMainStart)):
1868
1839
  if is_prev and get_overall_exp:
1869
1840
  expression = get_node_expression(
@@ -2293,7 +2264,12 @@ def get_none_option(node):
2293
2264
  def get_count_terms_details(prev_node, processed_nodes, get_overall_exp, negate=False, process=None):
2294
2265
  opt_none = get_none_option(prev_node)
2295
2266
  if opt_none:
2296
- operation_none = TriccOperation(TriccOperator.SELECTED, [prev_node, TriccStatic(opt_none)])
2267
+ if isinstance(opt_none, str):
2268
+ operation_none = TriccOperation(TriccOperator.SELECTED, [prev_node, TriccStatic(opt_none)])
2269
+ elif issubclass(opt_none.__class__, TriccBaseModel):
2270
+ operation_none = TriccOperation(TriccOperator.SELECTED, [prev_node, opt_none])
2271
+ else:
2272
+ logger.critical(f"unexpected none option value {opt_none}")
2297
2273
  else:
2298
2274
  operation_none = TriccOperation(TriccOperator.SELECTED, [prev_node, TriccStatic("opt_none")])
2299
2275
  if isinstance(prev_node, TriccNodeSelectYesNo):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tricc-oo
3
- Version: 1.6.12
3
+ Version: 1.6.13
4
4
  Summary: Python library that converts CDSS L2 in L3
5
5
  Project-URL: Homepage, https://github.com/SwissTPH/tricc
6
6
  Project-URL: Issues, https://github.com/SwissTPH/tricc/issues
@@ -1,16 +1,15 @@
1
1
  tests/build.py,sha256=Qbxvjkj_Wk2nQ-WjaMGiE1FIe3SRmJMRIgeoMoxqlfQ,6748
2
- tests/test_build.py,sha256=5t8iliPe_0XwoZjSGkHxUbZaNOWBfc6SpIQijh9DLUA,10037
3
2
  tests/test_cql.py,sha256=dAsLMqVaS6qxnq62fg5KqTFu6UG6pHO6Ab3NZ1c9T3Y,5248
4
3
  tests/to_ocl.py,sha256=4e-i65K3UM6wHgdVcrZcM9AyL1bahIsXJiZTXhhHgQk,2048
5
4
  tricc_oo/__init__.py,sha256=oWCE1ubmC_6iqaWOMgTei4eXVQgV202Ia-tXS1NnW_4,139
6
5
  tricc_oo/converters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- tricc_oo/converters/codesystem_to_ocl.py,sha256=Fh7Vk73OsxljZKu1k6H9uzYwz334tpQTMZBjWWbamYE,6151
6
+ tricc_oo/converters/codesystem_to_ocl.py,sha256=V_oZNVUVaYHgzJtDiBMSrvIJnBWmDNFBDTTHEkOfpXk,6151
8
7
  tricc_oo/converters/cql_to_operation.py,sha256=PUyV_YpUY98Ox0H_F_CN3UUf_I-BhFZVOcWWKTtwecM,14492
9
- tricc_oo/converters/datadictionnary.py,sha256=T2HLCBo4Am1p0kFqSH1r0PqbD8AC2IGuWkbvMvSCru0,3658
10
- tricc_oo/converters/drawio_type_map.py,sha256=UCPiGs7Lw0bigKScmZUnmOhACBz-FiDq92jHkI7RTSQ,9113
8
+ tricc_oo/converters/datadictionnary.py,sha256=JasqlLKiZzKdidsA1xc2SJ_Af1Xr6A3sKfzDynto8Ho,3686
9
+ tricc_oo/converters/drawio_type_map.py,sha256=Zp8J9iHNSJkIVrmRSM0_d4vA1X8wFPLKb8nCMPUMXKU,9114
11
10
  tricc_oo/converters/tricc_to_xls_form.py,sha256=39hwWgYNitGE-AuKtjUwNLz39tEpwc7nd9gT_gw5wjc,3842
12
11
  tricc_oo/converters/utils.py,sha256=JZrtrvvOfXwdkw49pKauzinOcauWwsy-CVcw36TjyLo,1684
13
- tricc_oo/converters/xml_to_tricc.py,sha256=yqnU5xg1SVoQ8vra0gOvIPwoh80pyTI332y9qJCvcRs,39654
12
+ tricc_oo/converters/xml_to_tricc.py,sha256=ea8LNEPDe32q74AJCbEjxaLt_Po47oH45K_G8fo7TzE,40388
14
13
  tricc_oo/converters/cql/cqlLexer.py,sha256=8HArbRphcrpnAG4uogJ2rHv4tc1WLzjN0B1uFeYILAc,49141
15
14
  tricc_oo/converters/cql/cqlListener.py,sha256=fA7-8DcS2Q69ckwjdg57-OfFHBxjTZFdoSKrtw7Hffc,57538
16
15
  tricc_oo/converters/cql/cqlParser.py,sha256=x3KdrwX9nwENSEJ5Ex7_l5NMnu3kWBO0uLdYu4moTq0,414745
@@ -26,27 +25,27 @@ tricc_oo/parsers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,
26
25
  tricc_oo/parsers/xml.py,sha256=uzkb1y18MHfqVFmZqVh0sKT4cx6u0-NcAT_lV_gHBt8,4208
27
26
  tricc_oo/serializers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
27
  tricc_oo/serializers/planuml.py,sha256=t57587-6L3aDncpHh58lS77Zft8yxDE9DPtXx2BeUSU,132
29
- tricc_oo/serializers/xls_form.py,sha256=-nT5pQG6mKIT_iJWUUjtfn8GzeGzQCR61zlTvxRmoLE,20723
28
+ tricc_oo/serializers/xls_form.py,sha256=ydaD5l_CsMEhJlSO0XvXPdxrwsDw4HiRjTOPHogTQdw,23071
30
29
  tricc_oo/strategies/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
30
  tricc_oo/strategies/input/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
31
  tricc_oo/strategies/input/base_input_strategy.py,sha256=BEODXS74na1QRRcJVQ4cxiD8F7uRqaLyhE3QzKpGVvk,3891
33
32
  tricc_oo/strategies/input/drawio.py,sha256=uXAUPhXOeg0Uk_BNqlCqFBW4cWNox4VfH559bj1fhC0,12767
34
33
  tricc_oo/strategies/output/base_output_strategy.py,sha256=i9L5CVUqkEAMNyBsdHJ4xA7Nptr3myHr_fHHveDX1cU,8928
35
- tricc_oo/strategies/output/dhis2_form.py,sha256=O5sBcwG_i6Vx3T5-Be4MmE50CsW4obikS3p8MOOJu1o,49317
36
- tricc_oo/strategies/output/fhir_form.py,sha256=yDDJqdkt4PvSDEy4kmgKM8V_o757CDlE7813atyyDHM,15749
34
+ tricc_oo/strategies/output/dhis2_form.py,sha256=jW9NW72_61ch1bHm8ShIH4xsJH-HMlZGPTT5txJxMUk,38278
35
+ tricc_oo/strategies/output/fhir_form.py,sha256=hbL921pe1Doun4IQrJuZ_Sq2fCh98G3grYie5olC4uc,15740
37
36
  tricc_oo/strategies/output/html_form.py,sha256=qSleEZOMV_-Z04y-i-ucyd5rgAYWAyjPwMrw0IHtCRM,8604
38
37
  tricc_oo/strategies/output/openmrs_form.py,sha256=ne6TwAyhafR-WDs27QTKKFl85VD5sij_VEJtK6ZjOIE,28996
39
38
  tricc_oo/strategies/output/spice.py,sha256=QMeoismVC3PdbvwTK0PtUjWX9jl9780fbQIXn76fMXw,10761
40
39
  tricc_oo/strategies/output/xls_form.py,sha256=_pNTND7n-55EjRphJ1hSVtRYa-UkXlmwpam2OKQ8o_w,30860
41
40
  tricc_oo/strategies/output/xlsform_cdss.py,sha256=X00Lt5MzV8TX14dR4dFI1MqllI5S1e13bKbeysWM9uA,17435
42
- tricc_oo/strategies/output/xlsform_cht.py,sha256=gztZBUJj4LdIX1jRvrG3hnLa7qpp0IZP_GpBi7bEzvc,28511
41
+ tricc_oo/strategies/output/xlsform_cht.py,sha256=Zy8aggR1sTBP0b33RGbfUpk8pRppI1LGQEif4E1l49A,28523
43
42
  tricc_oo/strategies/output/xlsform_cht_hf.py,sha256=xm6SKirV3nMZvM2w54_zJcXAeAgAkq-EEqGEjnOWv6c,988
44
43
  tricc_oo/visitors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
- tricc_oo/visitors/tricc.py,sha256=PpEjW4qXbtjfvvLz4iFw4Nj0Khzida1WSBh8_ZjF9sA,112142
44
+ tricc_oo/visitors/tricc.py,sha256=WvWrL2NAsGjoo2XBLzuKU0L9uNRYE8kRVdkwo8SMwEQ,110715
46
45
  tricc_oo/visitors/utils.py,sha256=j83aAq5s5atXi3OC0jc_uJd54a8XrHHmizeeEbWZQJg,421
47
46
  tricc_oo/visitors/xform_pd.py,sha256=ryAnI3V9x3eTmJ2LNsUZfvl0_yfCqo6oBgeSu-WPqaE,9613
48
- tricc_oo-1.6.12.dist-info/licenses/LICENSE,sha256=Pz2eACSxkhsGfW9_iN60pgy-enjnbGTj8df8O3ebnQQ,16726
49
- tricc_oo-1.6.12.dist-info/METADATA,sha256=Ifx_BtNteuGhT5iysOEJdF-0NXpFWuOGuiA_KRAt6YE,8600
50
- tricc_oo-1.6.12.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
51
- tricc_oo-1.6.12.dist-info/top_level.txt,sha256=NvbfMNAiy9m4b1unBsqpeOQWh4IgA1Xa33BtKA4abxk,15
52
- tricc_oo-1.6.12.dist-info/RECORD,,
47
+ tricc_oo-1.6.13.dist-info/licenses/LICENSE,sha256=Pz2eACSxkhsGfW9_iN60pgy-enjnbGTj8df8O3ebnQQ,16726
48
+ tricc_oo-1.6.13.dist-info/METADATA,sha256=QgvsAxKLVdd7mCifr9ZT-RsVQxuxIJIvgfRmfheuRDY,8600
49
+ tricc_oo-1.6.13.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
50
+ tricc_oo-1.6.13.dist-info/top_level.txt,sha256=NvbfMNAiy9m4b1unBsqpeOQWh4IgA1Xa33BtKA4abxk,15
51
+ tricc_oo-1.6.13.dist-info/RECORD,,
tests/test_build.py DELETED
@@ -1,260 +0,0 @@
1
- import unittest
2
- import subprocess
3
- import sys
4
- import os
5
- import tempfile
6
- import shutil
7
- from pathlib import Path
8
- import pandas as pd
9
- from pyxform import create_survey_from_xls
10
-
11
-
12
- class TestBuildScript(unittest.TestCase):
13
- """Test cases for the build.py script with different argument combinations."""
14
-
15
- def setUp(self):
16
- """Set up test fixtures."""
17
- self.test_data_dir = Path(__file__).parent / "data"
18
- self.test_output_dir = Path(__file__).parent / "output"
19
- self.demo_file = self.test_data_dir / "demo.drawio"
20
-
21
- # Ensure test data exists
22
- self.assertTrue(self.demo_file.exists(), f"Test data file {self.demo_file} does not exist")
23
-
24
- def run_build_script(self, args):
25
- """Helper method to run build.py with given arguments."""
26
- cmd = [sys.executable, str(Path(__file__).parent / "build.py")] + args
27
- result = subprocess.run(cmd, capture_output=True, text=True, cwd=Path(__file__).parent.parent)
28
- return result
29
-
30
- def test_basic_build_with_demo_file(self):
31
- """Test basic build with demo.drawio file."""
32
- with tempfile.TemporaryDirectory() as temp_dir:
33
- args = [
34
- "-i", str(self.demo_file),
35
- "-o", temp_dir,
36
- "-l", "i"
37
- ]
38
- result = self.run_build_script(args)
39
- self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
40
-
41
- def test_build_with_directory_input(self):
42
- """Test build with directory containing drawio files."""
43
- with tempfile.TemporaryDirectory() as temp_dir:
44
- args = [
45
- "-i", str(self.test_data_dir),
46
- "-o", temp_dir,
47
- "-l", "i"
48
- ]
49
- result = self.run_build_script(args)
50
- self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
51
-
52
- def test_build_with_xlsform_strategy(self):
53
- """Test build with XLSFormStrategy (default)."""
54
- with tempfile.TemporaryDirectory() as temp_dir:
55
- args = [
56
- "-i", str(self.demo_file),
57
- "-o", temp_dir,
58
- "-O", "XLSFormStrategy",
59
- "-l", "i"
60
- ]
61
- result = self.run_build_script(args)
62
- self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
63
-
64
- def test_build_with_html_strategy(self):
65
- """Test build with HTMLStrategy."""
66
- with tempfile.TemporaryDirectory() as temp_dir:
67
- args = [
68
- "-i", str(self.demo_file),
69
- "-o", temp_dir,
70
- "-O", "HTMLStrategy",
71
- "-l", "i"
72
- ]
73
- result = self.run_build_script(args)
74
- self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
75
-
76
- def test_build_with_fhir_strategy(self):
77
- """Test build with FHIRStrategy."""
78
- with tempfile.TemporaryDirectory() as temp_dir:
79
- args = [
80
- "-i", str(self.demo_file),
81
- "-o", temp_dir,
82
- "-O", "FHIRStrategy",
83
- "-l", "i"
84
- ]
85
- result = self.run_build_script(args)
86
- self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
87
-
88
- def test_build_with_dhis2_strategy(self):
89
- """Test build with DHIS2Strategy."""
90
- with tempfile.TemporaryDirectory() as temp_dir:
91
- args = [
92
- "-i", str(self.demo_file),
93
- "-o", temp_dir,
94
- "-O", "DHIS2Strategy",
95
- "-l", "i"
96
- ]
97
- result = self.run_build_script(args)
98
- self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
99
-
100
- def test_build_with_openmrs_strategy(self):
101
- """Test build with OpenMRSStrategy."""
102
- with tempfile.TemporaryDirectory() as temp_dir:
103
- args = [
104
- "-i", str(self.demo_file),
105
- "-o", temp_dir,
106
- "-O", "OpenMRSStrategy",
107
- "-l", "i"
108
- ]
109
- result = self.run_build_script(args)
110
- self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
111
-
112
- def test_build_with_cht_strategy(self):
113
- """Test build with XLSFormCHTStrategy."""
114
- with tempfile.TemporaryDirectory() as temp_dir:
115
- args = [
116
- "-i", str(self.demo_file),
117
- "-o", temp_dir,
118
- "-O", "XLSFormCHTStrategy",
119
- "-l", "i"
120
- ]
121
- result = self.run_build_script(args)
122
- self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
123
-
124
- def test_build_with_cht_hf_strategy(self):
125
- """Test build with XLSFormCHTHFStrategy."""
126
- with tempfile.TemporaryDirectory() as temp_dir:
127
- args = [
128
- "-i", str(self.demo_file),
129
- "-o", temp_dir,
130
- "-O", "XLSFormCHTHFStrategy",
131
- "-l", "i"
132
- ]
133
- result = self.run_build_script(args)
134
- self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
135
-
136
- def test_build_with_cdss_strategy(self):
137
- """Test build with XLSFormCDSSStrategy."""
138
- with tempfile.TemporaryDirectory() as temp_dir:
139
- args = [
140
- "-i", str(self.demo_file),
141
- "-o", temp_dir,
142
- "-O", "XLSFormCDSSStrategy",
143
- "-l", "i"
144
- ]
145
- result = self.run_build_script(args)
146
- self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
147
-
148
- # def test_build_with_spice_strategy(self):
149
- # """Test build with SpiceStrategy."""
150
- # with tempfile.TemporaryDirectory() as temp_dir:
151
- # args = [
152
- # "-i", str(self.demo_file),
153
- # "-o", temp_dir,
154
- # "-O", "SpiceStrategy",
155
- # "-l", "i"
156
- # ]
157
- # result = self.run_build_script(args)
158
- # self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
159
-
160
- def test_build_with_debug_level(self):
161
- """Test build with debug logging level."""
162
- with tempfile.TemporaryDirectory() as temp_dir:
163
- args = [
164
- "-i", str(self.demo_file),
165
- "-o", temp_dir,
166
- "-l", "d"
167
- ]
168
- result = self.run_build_script(args)
169
- self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
170
-
171
- def test_build_with_trad_option(self):
172
- """Test build with translation option."""
173
- with tempfile.TemporaryDirectory() as temp_dir:
174
- args = [
175
- "-i", str(self.demo_file),
176
- "-o", temp_dir,
177
- "-t",
178
- "-l", "i"
179
- ]
180
- result = self.run_build_script(args)
181
- self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
182
-
183
- def test_build_with_form_id(self):
184
- """Test build with custom form ID."""
185
- with tempfile.TemporaryDirectory() as temp_dir:
186
- args = [
187
- "-i", str(self.demo_file),
188
- "-o", temp_dir,
189
- "-d", "test_form_123",
190
- "-l", "i"
191
- ]
192
- result = self.run_build_script(args)
193
- self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
194
-
195
- def test_build_missing_input(self):
196
- """Test build with missing input file."""
197
- args = ["-o", "/tmp/test_output"]
198
- result = self.run_build_script(args)
199
- self.assertNotEqual(result.returncode, 0, "Build should fail with missing input")
200
-
201
- def test_build_invalid_input_file(self):
202
- """Test build with invalid input file."""
203
- with tempfile.TemporaryDirectory() as temp_dir:
204
- args = [
205
- "-i", "/nonexistent/file.drawio",
206
- "-o", temp_dir,
207
- "-l", "i"
208
- ]
209
- result = self.run_build_script(args)
210
- self.assertNotEqual(result.returncode, 0, "Build should fail with invalid input file")
211
-
212
- def validate_xls_form(self, xls_path):
213
- """Helper method to validate XLS form using ODK libraries."""
214
- try:
215
- # Convert XLS to XML using pyxform
216
- survey = create_survey_from_xls(xls_path)
217
- xml_output = survey.to_xml()
218
-
219
- # Basic validation - check if XML was generated successfully
220
- # In a real scenario, you might want to use odk_validate command line tool
221
- if xml_output and len(xml_output.strip()) > 0:
222
- return True, "Validation successful"
223
- else:
224
- return False, "Empty XML output"
225
- except Exception as e:
226
- return False, str(e)
227
-
228
- def test_xlsform_strategy_validation(self):
229
- """Test XLSFormStrategy output validation with ODK libraries."""
230
- with tempfile.TemporaryDirectory() as temp_dir:
231
- xls_output = Path(temp_dir) / "demo_tricc.xlsx"
232
- args = [
233
- "-i", str(self.demo_file),
234
- "-o", temp_dir,
235
- "-O", "XLSFormStrategy",
236
- "-l", "i"
237
- ]
238
- result = self.run_build_script(args)
239
- self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
240
-
241
- # Check if XLS file was created
242
- self.assertTrue(xls_output.exists(), f"XLS file {xls_output} was not created")
243
-
244
- def test_xlsform_cdss_strategy_validation(self):
245
- """Test XLSFormCDSSStrategy output validation with ODK libraries."""
246
- with tempfile.TemporaryDirectory() as temp_dir:
247
- xls_output = Path(temp_dir) / "demo_tricc.xlsx"
248
- args = [
249
- "-i", str(self.demo_file),
250
- "-o", temp_dir,
251
- "-O", "XLSFormCDSSStrategy",
252
- "-l", "i"
253
- ]
254
- result = self.run_build_script(args)
255
- self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
256
-
257
-
258
-
259
- if __name__ == "__main__":
260
- unittest.main()