tricc-oo 1.6.11__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")
@@ -73,7 +74,7 @@ def start_group(
73
74
  group=cur_group.group,
74
75
  activity=cur_group.activity,
75
76
  name=get_export_group_name(name),
76
- expression=cur_group.relevance.copy()
77
+ expression=cur_group.relevance
77
78
  )
78
79
 
79
80
  if calc not in cur_group.activity.calculates:
@@ -103,7 +104,18 @@ def start_group(
103
104
  if not relevance:
104
105
  relevance_expression_str = ""
105
106
  elif isinstance(relevance_expression, (TriccOperation, TriccStatic)):
106
- 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)
107
119
 
108
120
  # group
109
121
  values = []
@@ -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
 
@@ -821,34 +821,22 @@ class XLSFormCHTStrategy(XLSFormCDSSStrategy):
821
821
  return jar_path
822
822
 
823
823
  def _ensure_odk_validate_jar(self):
824
- """Ensure ODK Validate JAR is available by extracting from medic zip."""
824
+ """Ensure ODK Validate JAR is available by downloading from GitHub releases."""
825
825
  jar_path = os.path.join(os.path.dirname(__file__), "ODK_Validate.jar")
826
826
 
827
827
  # Check if JAR already exists
828
828
  if os.path.exists(jar_path):
829
829
  return jar_path
830
830
 
831
- # Extract JAR from medic zip
832
- medic_zip_path = os.path.join(os.path.dirname(__file__), "xls2xform-medic")
833
- if not os.path.exists(medic_zip_path):
834
- logger.error("xls2xform-medic zip not found, cannot extract ODK Validate JAR")
835
- return None
836
-
831
+ # Download JAR from GitHub releases
832
+ jar_url = "https://github.com/getodk/validate/releases/download/v1.20.0/ODK-Validate-v1.20.0.jar"
837
833
  try:
838
- with zipfile.ZipFile(medic_zip_path, 'r') as zip_ref:
839
- # Extract the JAR from the zip
840
- jar_in_zip = "site-packages/pyxform/validators/odk_validate/bin/ODK_Validate.jar"
841
- zip_ref.extract(jar_in_zip, os.path.dirname(__file__))
842
-
843
- # Move to final location
844
- extracted_jar = os.path.join(os.path.dirname(__file__), jar_in_zip)
845
- shutil.move(extracted_jar, jar_path)
846
-
847
- logger.info(f"Extracted ODK Validate JAR to {jar_path}")
834
+ import urllib.request
835
+ urllib.request.urlretrieve(jar_url, jar_path)
836
+ logger.info(f"Downloaded ODK Validate JAR to {jar_path}")
848
837
  return jar_path
849
-
850
838
  except Exception as e:
851
- logger.error(f"Failed to extract ODK Validate JAR: {str(e)}")
839
+ logger.error(f"Failed to download ODK Validate JAR: {str(e)}")
852
840
  return None
853
841
 
854
842
  def tricc_operation_zscore(self, ref_expressions):
@@ -185,9 +185,9 @@ def get_version_inheritance(node, last_version, processed_nodes):
185
185
  path_len=node.path_len + 1,
186
186
  expression_reference=TriccOperation(
187
187
  TriccOperator.COALESCE,
188
- [TriccReference(node.save), last_version],
188
+ [node, last_version],
189
189
  ),
190
- reference=[TriccReference(node.name)],
190
+ reference=[node, last_version],
191
191
  activity=node.activity,
192
192
  group=node.group,
193
193
  label=f"Save calculation for {node.label}",
@@ -805,7 +805,7 @@ def process_operation_reference(
805
805
  operation,
806
806
  node,
807
807
  processed_nodes,
808
- calculates,
808
+ calculates=None,
809
809
  used_calculates=None,
810
810
  replace_reference=False,
811
811
  warn=False,
@@ -1740,16 +1740,17 @@ PARENT_GROUP_PRIORITY = 6000
1740
1740
  ACTIVE_ACTIVITY_PRIORITY = 5000
1741
1741
  NON_START_ACTIVITY_PRIORITY = 4000
1742
1742
  ACTIVE_ACTIVITY_LOWER_PRIORITY = 3000
1743
- FLOW_CALCULATE_NODE_PRIORITY = 6500
1744
- RHOMBUS_PRIORITY = 1000
1745
- DEFAULT_PRIORITY = 2000
1746
-
1743
+ FLOW_CALCULATE_NODE_PRIORITY_TOP_UP = 50
1744
+ RHOMBUS_PRIORITY_TO_UP = 50
1747
1745
 
1746
+
1748
1747
  def reorder_node_list(node_list, group, processed_nodes):
1749
1748
  # Cache active activities for O(1) lookup
1750
1749
  active_activities = {n.activity for n in processed_nodes}
1751
-
1750
+ MAP_PRIORITIES = {}
1752
1751
  def get_priority(node):
1752
+ if node.id in MAP_PRIORITIES:
1753
+ return MAP_PRIORITIES[node.id]
1753
1754
  # Cache attributes to avoid repeated getattr calls
1754
1755
  priority = int(getattr(node, "priority", 0) or 0)
1755
1756
  node_group = getattr(node, "group", None)
@@ -1757,12 +1758,7 @@ def reorder_node_list(node_list, group, processed_nodes):
1757
1758
 
1758
1759
  # Check for same group
1759
1760
  if group is not None and node_group and node_group.id == group.id:
1760
- priority += SAME_GROUP_PRIORITY
1761
- elif (
1762
- issubclass(node.__class__, TriccNodeDisplayCalculateBase) or
1763
- isinstance(node, TriccNodeEnd)
1764
- ) and not isinstance(node, TriccNodeActivityEnd) and hasattr(node, 'prev_nodes') and len(node.prev_nodes) > 0:
1765
- priority += FLOW_CALCULATE_NODE_PRIORITY
1761
+ priority += SAME_GROUP_PRIORITY
1766
1762
  # Check for parent group
1767
1763
  elif hasattr(group, "group") and group.group and node_group and node_group.id == group.group.id:
1768
1764
  priority += PARENT_GROUP_PRIORITY
@@ -1776,11 +1772,21 @@ def reorder_node_list(node_list, group, processed_nodes):
1776
1772
  elif activity and activity in active_activities:
1777
1773
  priority += ACTIVE_ACTIVITY_LOWER_PRIORITY
1778
1774
  # Check for rhombus nodes
1775
+
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
1779
1782
  elif issubclass(node.__class__, TriccRhombusMixIn):
1780
- priority += RHOMBUS_PRIORITY
1781
- else:
1782
- priority += DEFAULT_PRIORITY
1783
+ priority += RHOMBUS_PRIORITY_TO_UP
1783
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
+
1784
1790
  return priority
1785
1791
 
1786
1792
  # Sort in place, highest priority first
@@ -2258,7 +2264,12 @@ def get_none_option(node):
2258
2264
  def get_count_terms_details(prev_node, processed_nodes, get_overall_exp, negate=False, process=None):
2259
2265
  opt_none = get_none_option(prev_node)
2260
2266
  if opt_none:
2261
- 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}")
2262
2273
  else:
2263
2274
  operation_none = TriccOperation(TriccOperator.SELECTED, [prev_node, TriccStatic("opt_none")])
2264
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.11
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=rIlTIccdIDQQv0dECcB8c9hDg5sMJShmUYsNTlSoZXg,22575
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=RXacGiVAib5bF_0ea564YhuJ1ASs20MuMuIeZXiP5-M,29019
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=9jAVSxFfklJcLzwLvZAV1_sRuD8616HSgOSl8wdqPTE,110227
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.11.dist-info/licenses/LICENSE,sha256=Pz2eACSxkhsGfW9_iN60pgy-enjnbGTj8df8O3ebnQQ,16726
49
- tricc_oo-1.6.11.dist-info/METADATA,sha256=VG-L_K4eTy4Fta8iaXuymORn6v9PCLSHH2E3qD9yk24,8600
50
- tricc_oo-1.6.11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
51
- tricc_oo-1.6.11.dist-info/top_level.txt,sha256=NvbfMNAiy9m4b1unBsqpeOQWh4IgA1Xa33BtKA4abxk,15
52
- tricc_oo-1.6.11.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()