tricc-oo 1.6.8__py3-none-any.whl → 1.6.9__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.
@@ -625,6 +625,8 @@ def enrich_node(diagram, media_path, edge, node, activity, help_before=False):
625
625
  name=f"{node.name}.more_info",
626
626
  label=message,
627
627
  parent=node,
628
+ group=node.group,
629
+ activity=node.activity,
628
630
  required=None,
629
631
  )
630
632
  # node.help = message
@@ -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
19
+ TriccStatic, TriccReference, TriccOperator
20
20
  )
21
21
  from tricc_oo.models.tricc import (
22
22
  TriccNodeSelectOption,
@@ -27,6 +27,8 @@ from tricc_oo.models.tricc import (
27
27
  TriccNodeActivity,
28
28
  TriccNodeSelect,
29
29
  TriccNodeSelectYesNo,
30
+ TriccNodeNote,
31
+ TriccNodeMoreInfo,
30
32
  )
31
33
  from tricc_oo.models.calculate import TriccNodeDisplayCalculateBase
32
34
  from tricc_oo.models.ordered_set import OrderedSet
@@ -44,7 +46,7 @@ class DHIS2Strategy(BaseOutPutStrategy):
44
46
 
45
47
  def __init__(self, project, output_path):
46
48
  super().__init__(project, output_path)
47
- form_id = getattr(self.project.start_pages["main"], 'form_id', 'dhis2_program')
49
+ form_id = getattr(self.project.start_pages["main"].root, 'form_id', 'dhis2_program')
48
50
  self.program_metadata = {
49
51
  "id": self.generate_id(form_id),
50
52
  "name": form_id,
@@ -204,6 +206,10 @@ class DHIS2Strategy(BaseOutPutStrategy):
204
206
  return False
205
207
 
206
208
  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
+
207
213
  relevance = None
208
214
  if hasattr(node, 'relevance') and node.relevance:
209
215
  relevance = node.relevance
@@ -225,8 +231,10 @@ class DHIS2Strategy(BaseOutPutStrategy):
225
231
  "activity_ref": node, # Temporary reference to be replaced with section ID
226
232
  "programRule": {"id": rule_id},
227
233
  }
228
- else:
229
- # For regular nodes, use HIDEFIELD action
234
+ self.program_rule_actions.append(program_rule_action)
235
+ elif not issubclass(node.__class__, TriccNodeCalculateBase) and not isinstance(node, (TriccNodeNote, TriccNodeMoreInfo)):
236
+ # For regular nodes that get DataElements, use HIDEFIELD action
237
+ # Exclude TriccNodeNote and TriccNodeMoreInfo as they don't get DataElements
230
238
  program_rule_action = {
231
239
  "id": action_id,
232
240
  "programRuleActionType": "HIDEFIELD",
@@ -235,18 +243,45 @@ class DHIS2Strategy(BaseOutPutStrategy):
235
243
  },
236
244
  "programRule": {"id": rule_id}
237
245
  }
238
- self.program_rule_actions.append(program_rule_action)
246
+ self.program_rule_actions.append(program_rule_action)
239
247
 
240
248
  # Create program rule referencing the action
241
- condition = self.simplify_expression(f"!({relevance_str})") # Negate for hide when true
249
+ condition = self.simplify_expression(f"({relevance_str})==false") # Negate for hide when true
242
250
  condition = self.simplify_expression(condition)
243
251
  self.program_rules.append({
244
252
  "id": rule_id,
245
253
  "name": f"Hide `{self.get_export_name(node)}` when condition met",
246
- "description": f"Hide `{self.get_display(node)}` based on relevance",
254
+ "description": f"Hide `{self.get_display(node)[:128]}` based on relevance",
247
255
  "condition": condition,
248
256
  "programRuleActions": [{"id": action_id}]
249
257
  })
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
+
250
285
  return True
251
286
 
252
287
  def generate_data_element(self, node):
@@ -512,8 +547,16 @@ class DHIS2Strategy(BaseOutPutStrategy):
512
547
  return False
513
548
 
514
549
  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
+ self.handle_note_as_section_description(node, processed_nodes, **kwargs)
515
558
  # Skip creating data elements for calculate nodes - they should only be program rule variables
516
- if not issubclass(node.__class__, TriccNodeCalculateBase):
559
+ elif not issubclass(node.__class__, TriccNodeCalculateBase):
517
560
  data_element = self.generate_data_element(node)
518
561
  if data_element:
519
562
  # Add to program stage
@@ -522,7 +565,7 @@ class DHIS2Strategy(BaseOutPutStrategy):
522
565
  psde = {
523
566
  "id": psde_id,
524
567
  "dataElement": {"id": data_element["id"]},
525
- "compulsory": bool(getattr(node, 'required', False))
568
+ "compulsory": False
526
569
  }
527
570
  self.program_metadata["programStages"][-1]["programStageDataElements"].append(psde)
528
571
 
@@ -532,6 +575,115 @@ class DHIS2Strategy(BaseOutPutStrategy):
532
575
 
533
576
  return True
534
577
 
578
+ def handle_note_as_section_description(self, node, processed_nodes, **kwargs):
579
+ """Transform TriccNodeNote into section description and create hide logic with section duplication"""
580
+ logger.info(f"Processing note {node.get_name()} for section description")
581
+ if not self.current_section or self.current_section not in self.sections:
582
+ logger.warning(f"No current section found for note {node.get_name()}")
583
+ return
584
+
585
+ # 1. Duplicate the previous section with incremented ID (like XLS form group numbering)
586
+ original_section = self.sections[self.current_section]
587
+ section_name = f"{original_section['name']}_{node.name}"
588
+ duplicated_section_id = self.generate_id(section_name)
589
+ duplicated_section = {
590
+ "id": duplicated_section_id,
591
+ "name": section_name, # Use incremented naming pattern
592
+ "sortOrder": len(self.sections),
593
+ "programStage": {"id": self.program_metadata["programStages"][-1]["id"]},
594
+ "dataElements": original_section.get("dataElements", []).copy(), # Copy data elements
595
+ "activity_ref": original_section.get("activity_ref")
596
+ }
597
+ self.sections[duplicated_section_id] = duplicated_section
598
+
599
+ # Add duplicated section to program stage
600
+ if self.program_metadata["programStages"]:
601
+ self.program_metadata["programStages"][-1]["programStageSections"].append({"id": duplicated_section_id})
602
+
603
+ logger.info(f"Duplicated section {self.current_section} as {duplicated_section_id}")
604
+
605
+ # 2. Create a new section for the note/moreinfo with name and ID from the node
606
+ note_section_id = self.generate_id(self.get_export_name(node))
607
+ note_section_name = self.get_export_name(node)
608
+
609
+ note_section = {
610
+ "id": note_section_id,
611
+ "name": note_section_name,
612
+ "sortOrder": len(self.sections),
613
+ "programStage": {"id": self.program_metadata["programStages"][-1]["id"]},
614
+ "dataElements": [],
615
+ "activity_ref": original_section.get("activity_ref") # Same activity reference
616
+ }
617
+
618
+ # Set section description to the note's label
619
+ if hasattr(node, 'label') and node.label:
620
+ note_section["description"] = node.label.replace('\u00a0', ' ').strip()
621
+ logger.info(f"Set note section {note_section_id} description to: {note_section['description']}")
622
+ elif hasattr(node, 'name') and node.name:
623
+ note_section["description"] = node.name.replace('\u00a0', ' ').strip()
624
+ logger.info(f"Set note section {note_section_id} description to: {note_section['description']}")
625
+ else:
626
+ note_section["description"] = str(node.id)
627
+ logger.info(f"Set note section {note_section_id} description to: {note_section['description']}")
628
+
629
+ self.sections[note_section_id] = note_section
630
+
631
+ # Add note section to program stage
632
+ if self.program_metadata["programStages"]:
633
+ self.program_metadata["programStages"][-1]["programStageSections"].append({"id": note_section_id})
634
+
635
+ logger.info(f"Created note section {note_section_id} with name '{note_section_name}'")
636
+
637
+ # 3. Inject the duplicated section as the new current section
638
+ self.current_section = duplicated_section_id
639
+ logger.info(f"Set current section to duplicated section {duplicated_section_id}")
640
+
641
+ # Create hide logic for the note section based on combined relevance
642
+ combined_relevance = None
643
+
644
+ # Get parent activity relevance from the original section's activity_ref
645
+ parent_activity = original_section.get("activity_ref")
646
+ if parent_activity and hasattr(parent_activity, 'relevance') and parent_activity.relevance:
647
+ combined_relevance = parent_activity.relevance
648
+
649
+ # Combine with note relevance if it exists
650
+ if hasattr(node, 'relevance') and node.relevance:
651
+ if combined_relevance:
652
+ # Combine using AND operation
653
+ combined_relevance = TriccOperation(
654
+ TriccOperator.AND,
655
+ [combined_relevance, node.relevance]
656
+ )
657
+ else:
658
+ combined_relevance = node.relevance
659
+
660
+ # Create hide logic if there's relevance - applied to the note section
661
+ if combined_relevance:
662
+ relevance_str = self.convert_expression_to_string(not_clean(combined_relevance))
663
+ if relevance_str and relevance_str != 'false':
664
+ # Create program rule action for hiding the note section
665
+ rule_id = self.generate_id(f"rule_{node.get_name()}_note_hide_section")
666
+ action_id = self.generate_id(f"action_{rule_id}")
667
+
668
+ program_rule_action = {
669
+ "id": action_id,
670
+ "programRuleActionType": "HIDESECTION",
671
+ "activity_ref": parent_activity, # Use activity reference like other HIDESECTION actions
672
+ "programRule": {"id": rule_id},
673
+ }
674
+ self.program_rule_actions.append(program_rule_action)
675
+
676
+ # Create program rule referencing the action
677
+ condition = self.simplify_expression(f"({relevance_str})==false") # Negate for hide when true
678
+ condition = self.simplify_expression(condition)
679
+ self.program_rules.append({
680
+ "id": rule_id,
681
+ "name": f"Hide note section `{note_section_name}` based on relevance",
682
+ "description": f"Hide note section `{self.get_display(node)[:128]}` based on combined relevance",
683
+ "condition": condition,
684
+ "programRuleActions": [{"id": action_id}]
685
+ })
686
+
535
687
  def clean_section(self, program_stages_payload):
536
688
  """Clean sections by removing empty ones and merging sections with same activity_ref"""
537
689
  sections_to_remove = set()
@@ -542,11 +694,11 @@ class DHIS2Strategy(BaseOutPutStrategy):
542
694
  section_id = section["id"]
543
695
  activity_ref = section.get("activity_ref")
544
696
  # Remove empty sections
545
- if not section.get("dataElements"):
697
+ if not section.get("dataElements") and not section.get("description"):
546
698
  sections_to_remove.add(section_id)
547
699
 
548
700
  # Check for sections with same activity_ref
549
- elif activity_ref == prev_activity_ref:
701
+ elif activity_ref == prev_activity_ref and not section.get("description"):
550
702
  # Merge this section into the existing one
551
703
  existing_section = self.sections[prev_section_id]
552
704
 
@@ -645,6 +797,8 @@ class DHIS2Strategy(BaseOutPutStrategy):
645
797
  # Non-activity actions (HIDEFIELD) can be added directly
646
798
  program_rule_actions_payload.append(action)
647
799
 
800
+ # Filter out rules that reference non-existent actions
801
+ valid_action_ids = {action["id"] for action in program_rule_actions_payload}
648
802
  if self.program_rules:
649
803
  program_rules_payload = [
650
804
  {
@@ -652,6 +806,7 @@ class DHIS2Strategy(BaseOutPutStrategy):
652
806
  "program": {"id": self.program_metadata["id"]}
653
807
  }
654
808
  for rule in self.program_rules
809
+ if all(action_ref["id"] in valid_action_ids for action_ref in rule["programRuleActions"])
655
810
  ]
656
811
 
657
812
  if self.program_rule_variables:
@@ -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]
100
+ item["answerOption"] = [{"valueString": opt.name} for opt in node.options.values()]
101
101
  self.questionnaires[segment]["item"].append(item)
102
102
  return True
103
103
 
@@ -1758,6 +1758,11 @@ def reorder_node_list(node_list, group, processed_nodes):
1758
1758
  # Check for same group
1759
1759
  if group is not None and node_group and node_group.id == group.id:
1760
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
1766
  # Check for parent group
1762
1767
  elif hasattr(group, "group") and group.group and node_group and node_group.id == group.group.id:
1763
1768
  priority += PARENT_GROUP_PRIORITY
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tricc-oo
3
- Version: 1.6.8
3
+ Version: 1.6.9
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
@@ -10,7 +10,7 @@ tricc_oo/converters/datadictionnary.py,sha256=T2HLCBo4Am1p0kFqSH1r0PqbD8AC2IGuWk
10
10
  tricc_oo/converters/drawio_type_map.py,sha256=UCPiGs7Lw0bigKScmZUnmOhACBz-FiDq92jHkI7RTSQ,9113
11
11
  tricc_oo/converters/tricc_to_xls_form.py,sha256=wsWv4aA0QssY7ry9R7KsuuMzVfovj9fwE3i9AtCum0c,3842
12
12
  tricc_oo/converters/utils.py,sha256=JZrtrvvOfXwdkw49pKauzinOcauWwsy-CVcw36TjyLo,1684
13
- tricc_oo/converters/xml_to_tricc.py,sha256=PEBe8N-JIGJMVX2FO3UVxRcy5GrGsMvgcArEwUXmr6o,39572
13
+ tricc_oo/converters/xml_to_tricc.py,sha256=yqnU5xg1SVoQ8vra0gOvIPwoh80pyTI332y9qJCvcRs,39654
14
14
  tricc_oo/converters/cql/cqlLexer.py,sha256=8HArbRphcrpnAG4uogJ2rHv4tc1WLzjN0B1uFeYILAc,49141
15
15
  tricc_oo/converters/cql/cqlListener.py,sha256=fA7-8DcS2Q69ckwjdg57-OfFHBxjTZFdoSKrtw7Hffc,57538
16
16
  tricc_oo/converters/cql/cqlParser.py,sha256=x3KdrwX9nwENSEJ5Ex7_l5NMnu3kWBO0uLdYu4moTq0,414745
@@ -32,8 +32,8 @@ tricc_oo/strategies/input/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJW
32
32
  tricc_oo/strategies/input/base_input_strategy.py,sha256=BEODXS74na1QRRcJVQ4cxiD8F7uRqaLyhE3QzKpGVvk,3891
33
33
  tricc_oo/strategies/input/drawio.py,sha256=uXAUPhXOeg0Uk_BNqlCqFBW4cWNox4VfH559bj1fhC0,12767
34
34
  tricc_oo/strategies/output/base_output_strategy.py,sha256=i9L5CVUqkEAMNyBsdHJ4xA7Nptr3myHr_fHHveDX1cU,8928
35
- tricc_oo/strategies/output/dhis2_form.py,sha256=jW9NW72_61ch1bHm8ShIH4xsJH-HMlZGPTT5txJxMUk,38278
36
- tricc_oo/strategies/output/fhir_form.py,sha256=hbL921pe1Doun4IQrJuZ_Sq2fCh98G3grYie5olC4uc,15740
35
+ tricc_oo/strategies/output/dhis2_form.py,sha256=RFMUrB3TNJJi3kecBVThY6X1GF_TVk8XgXkye8fRkIc,47035
36
+ tricc_oo/strategies/output/fhir_form.py,sha256=yDDJqdkt4PvSDEy4kmgKM8V_o757CDlE7813atyyDHM,15749
37
37
  tricc_oo/strategies/output/html_form.py,sha256=qSleEZOMV_-Z04y-i-ucyd5rgAYWAyjPwMrw0IHtCRM,8604
38
38
  tricc_oo/strategies/output/openmrs_form.py,sha256=ne6TwAyhafR-WDs27QTKKFl85VD5sij_VEJtK6ZjOIE,28996
39
39
  tricc_oo/strategies/output/spice.py,sha256=QMeoismVC3PdbvwTK0PtUjWX9jl9780fbQIXn76fMXw,10761
@@ -42,11 +42,11 @@ tricc_oo/strategies/output/xlsform_cdss.py,sha256=X00Lt5MzV8TX14dR4dFI1MqllI5S1e
42
42
  tricc_oo/strategies/output/xlsform_cht.py,sha256=eKAc6LLDnvdZ5m8a2Vk6eAhvPbUvOfykgYr0ou3an9k,27164
43
43
  tricc_oo/strategies/output/xlsform_cht_hf.py,sha256=xm6SKirV3nMZvM2w54_zJcXAeAgAkq-EEqGEjnOWv6c,988
44
44
  tricc_oo/visitors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
- tricc_oo/visitors/tricc.py,sha256=8fULbAxKSeEtPyD5TYTaIG8-20Bjg2iZmzxcJM70n18,110295
45
+ tricc_oo/visitors/tricc.py,sha256=_UzAylSHuAhfXXPUBV_PJIhta5ihbJhLDe3GJAwxQzU,110598
46
46
  tricc_oo/visitors/utils.py,sha256=j83aAq5s5atXi3OC0jc_uJd54a8XrHHmizeeEbWZQJg,421
47
47
  tricc_oo/visitors/xform_pd.py,sha256=ryAnI3V9x3eTmJ2LNsUZfvl0_yfCqo6oBgeSu-WPqaE,9613
48
- tricc_oo-1.6.8.dist-info/licenses/LICENSE,sha256=Pz2eACSxkhsGfW9_iN60pgy-enjnbGTj8df8O3ebnQQ,16726
49
- tricc_oo-1.6.8.dist-info/METADATA,sha256=g9u0_NdvNT8FFgNB3LQzsUADmGqL1PBB1DPdnCBKVuI,8599
50
- tricc_oo-1.6.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
51
- tricc_oo-1.6.8.dist-info/top_level.txt,sha256=NvbfMNAiy9m4b1unBsqpeOQWh4IgA1Xa33BtKA4abxk,15
52
- tricc_oo-1.6.8.dist-info/RECORD,,
48
+ tricc_oo-1.6.9.dist-info/licenses/LICENSE,sha256=Pz2eACSxkhsGfW9_iN60pgy-enjnbGTj8df8O3ebnQQ,16726
49
+ tricc_oo-1.6.9.dist-info/METADATA,sha256=qbIz4okrC-eeMx-8mZMt_--MUWzPcZmht_dhO11qovY,8599
50
+ tricc_oo-1.6.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
51
+ tricc_oo-1.6.9.dist-info/top_level.txt,sha256=NvbfMNAiy9m4b1unBsqpeOQWh4IgA1Xa33BtKA4abxk,15
52
+ tricc_oo-1.6.9.dist-info/RECORD,,