tricc-oo 1.6.8__py3-none-any.whl → 1.6.10__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.
- tricc_oo/converters/tricc_to_xls_form.py +1 -1
- tricc_oo/converters/xml_to_tricc.py +2 -0
- tricc_oo/serializers/xls_form.py +17 -6
- tricc_oo/strategies/output/dhis2_form.py +211 -15
- tricc_oo/strategies/output/fhir_form.py +1 -1
- tricc_oo/strategies/output/xlsform_cht.py +77 -38
- tricc_oo/visitors/tricc.py +5 -6
- {tricc_oo-1.6.8.dist-info → tricc_oo-1.6.10.dist-info}/METADATA +1 -1
- {tricc_oo-1.6.8.dist-info → tricc_oo-1.6.10.dist-info}/RECORD +12 -12
- {tricc_oo-1.6.8.dist-info → tricc_oo-1.6.10.dist-info}/WHEEL +0 -0
- {tricc_oo-1.6.8.dist-info → tricc_oo-1.6.10.dist-info}/licenses/LICENSE +0 -0
- {tricc_oo-1.6.8.dist-info → tricc_oo-1.6.10.dist-info}/top_level.txt +0 -0
|
@@ -36,7 +36,7 @@ def get_export_name(node, replace_dots=True):
|
|
|
36
36
|
elif isinstance(node, bool):
|
|
37
37
|
return BOOLEAN_MAP[str(TRICC_TRUE_VALUE)] if node else BOOLEAN_MAP[str(TRICC_FALSE_VALUE)]
|
|
38
38
|
elif isinstance(node, TriccReference):
|
|
39
|
-
logger.warning(f"Reference {node.value} use in export, bad
|
|
39
|
+
logger.warning(f"Reference {node.value} use in export, bad serialization probable")
|
|
40
40
|
return str(node.value)
|
|
41
41
|
elif isinstance(node, (str, TriccStatic, TriccNodeSelectOption)):
|
|
42
42
|
if isinstance(node, TriccNodeSelectOption):
|
tricc_oo/serializers/xls_form.py
CHANGED
|
@@ -64,19 +64,30 @@ def start_group(
|
|
|
64
64
|
groups[name] = 0
|
|
65
65
|
relevance = relevance and cur_group.relevance is not None and cur_group.relevance != ""
|
|
66
66
|
past_instances = len(getattr(cur_group.base_instance, "instances", []))
|
|
67
|
-
group_calc_required = relevance is not None and (len(str(relevance)) > 100 or past_instances > 1)
|
|
67
|
+
group_calc_required = False and relevance is not None and (len(str(relevance)) > 100 or past_instances > 1)
|
|
68
68
|
calc = None
|
|
69
69
|
if group_calc_required and getattr(cur_group.relevance, 'operator', None) != TriccOperator.ISTRUE:
|
|
70
70
|
|
|
71
71
|
calc = TriccNodeCalculate(
|
|
72
72
|
id=generate_id(get_export_group_name(name)),
|
|
73
|
-
group=cur_group,
|
|
73
|
+
group=cur_group.group,
|
|
74
74
|
activity=cur_group.activity,
|
|
75
75
|
name=get_export_group_name(name),
|
|
76
|
-
expression=cur_group.relevance
|
|
76
|
+
expression=cur_group.relevance.copy()
|
|
77
77
|
)
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
|
|
79
|
+
if calc not in cur_group.activity.calculates:
|
|
80
|
+
process_reference(
|
|
81
|
+
calc,
|
|
82
|
+
processed_nodes,
|
|
83
|
+
calculates=kwargs.get('calculates', None),
|
|
84
|
+
used_calculates=kwargs.get('used_calculates', None),
|
|
85
|
+
replace_reference=True,
|
|
86
|
+
warn=False,
|
|
87
|
+
codesystems=kwargs.get('codesystems', None)
|
|
88
|
+
)
|
|
89
|
+
cur_group.activity.calculates.append(calc)
|
|
90
|
+
cur_group.activity.nodes[calc.id] = calc
|
|
80
91
|
processed_nodes.add(calc)
|
|
81
92
|
|
|
82
93
|
cur_group.relevance = TriccOperation(
|
|
@@ -123,7 +134,7 @@ def start_group(
|
|
|
123
134
|
value = get_export_name(calc)
|
|
124
135
|
calc_values.append(value)
|
|
125
136
|
elif column == "calculation":
|
|
126
|
-
calc_values.append(f"number({strategy.get_tricc_operation_expression(calc.expression)}")
|
|
137
|
+
calc_values.append(f"number({strategy.get_tricc_operation_expression(calc.expression)})")
|
|
127
138
|
elif column == "relevance":
|
|
128
139
|
calc_values.append("")
|
|
129
140
|
else:
|
|
@@ -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,13 +206,17 @@ 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
|
|
210
216
|
if hasattr(node, 'expression') and node.expression:
|
|
211
217
|
relevance = node.expression
|
|
212
218
|
if relevance:
|
|
213
|
-
relevance_str = self.convert_expression_to_string(
|
|
219
|
+
relevance_str = self.convert_expression_to_string(relevance)
|
|
214
220
|
if relevance_str and relevance_str != 'false':
|
|
215
221
|
# Create program rule action for hiding/showing based on relevance
|
|
216
222
|
rule_id = self.generate_id(f"rule_{node.get_name()}_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
|
-
|
|
229
|
-
|
|
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
|
|
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
|
-
|
|
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"
|
|
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):
|
|
@@ -274,8 +309,8 @@ class DHIS2Strategy(BaseOutPutStrategy):
|
|
|
274
309
|
"id": de_id,
|
|
275
310
|
"name": self.get_export_name(node),
|
|
276
311
|
"shortName": node.name[:50],
|
|
277
|
-
"displayFormName": self.get_display(node),
|
|
278
|
-
"formName": self.get_display(node),
|
|
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),
|
|
279
314
|
"valueType": value_type,
|
|
280
315
|
"domainType": "TRACKER",
|
|
281
316
|
"aggregationType": "NONE"
|
|
@@ -512,8 +547,36 @@ 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
|
+
# 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)
|
|
515
578
|
# Skip creating data elements for calculate nodes - they should only be program rule variables
|
|
516
|
-
|
|
579
|
+
elif not issubclass(node.__class__, TriccNodeCalculateBase):
|
|
517
580
|
data_element = self.generate_data_element(node)
|
|
518
581
|
if data_element:
|
|
519
582
|
# Add to program stage
|
|
@@ -522,7 +585,7 @@ class DHIS2Strategy(BaseOutPutStrategy):
|
|
|
522
585
|
psde = {
|
|
523
586
|
"id": psde_id,
|
|
524
587
|
"dataElement": {"id": data_element["id"]},
|
|
525
|
-
"compulsory":
|
|
588
|
+
"compulsory": False
|
|
526
589
|
}
|
|
527
590
|
self.program_metadata["programStages"][-1]["programStageDataElements"].append(psde)
|
|
528
591
|
|
|
@@ -532,6 +595,136 @@ class DHIS2Strategy(BaseOutPutStrategy):
|
|
|
532
595
|
|
|
533
596
|
return True
|
|
534
597
|
|
|
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
|
+
|
|
535
728
|
def clean_section(self, program_stages_payload):
|
|
536
729
|
"""Clean sections by removing empty ones and merging sections with same activity_ref"""
|
|
537
730
|
sections_to_remove = set()
|
|
@@ -542,11 +735,11 @@ class DHIS2Strategy(BaseOutPutStrategy):
|
|
|
542
735
|
section_id = section["id"]
|
|
543
736
|
activity_ref = section.get("activity_ref")
|
|
544
737
|
# Remove empty sections
|
|
545
|
-
if not section.get("dataElements"):
|
|
738
|
+
if not section.get("dataElements") and not section.get("description"):
|
|
546
739
|
sections_to_remove.add(section_id)
|
|
547
740
|
|
|
548
741
|
# Check for sections with same activity_ref
|
|
549
|
-
elif activity_ref == prev_activity_ref:
|
|
742
|
+
elif activity_ref == prev_activity_ref and not section.get("description"):
|
|
550
743
|
# Merge this section into the existing one
|
|
551
744
|
existing_section = self.sections[prev_section_id]
|
|
552
745
|
|
|
@@ -645,6 +838,8 @@ class DHIS2Strategy(BaseOutPutStrategy):
|
|
|
645
838
|
# Non-activity actions (HIDEFIELD) can be added directly
|
|
646
839
|
program_rule_actions_payload.append(action)
|
|
647
840
|
|
|
841
|
+
# Filter out rules that reference non-existent actions
|
|
842
|
+
valid_action_ids = {action["id"] for action in program_rule_actions_payload}
|
|
648
843
|
if self.program_rules:
|
|
649
844
|
program_rules_payload = [
|
|
650
845
|
{
|
|
@@ -652,6 +847,7 @@ class DHIS2Strategy(BaseOutPutStrategy):
|
|
|
652
847
|
"program": {"id": self.program_metadata["id"]}
|
|
653
848
|
}
|
|
654
849
|
for rule in self.program_rules
|
|
850
|
+
if all(action_ref["id"] in valid_action_ids for action_ref in rule["programRuleActions"])
|
|
655
851
|
]
|
|
656
852
|
|
|
657
853
|
if self.program_rule_variables:
|
|
@@ -710,7 +906,7 @@ class DHIS2Strategy(BaseOutPutStrategy):
|
|
|
710
906
|
return self.get_tricc_operation_expression(r)
|
|
711
907
|
elif isinstance(r, TriccReference):
|
|
712
908
|
# Use variable name from concept_map
|
|
713
|
-
node_id = self.concept_map.get(r.value
|
|
909
|
+
node_id = self.concept_map.get(r.value, self.get_export_name(r.value))
|
|
714
910
|
return f"#{{{node_id}}}"
|
|
715
911
|
elif isinstance(r, TriccStatic):
|
|
716
912
|
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]
|
|
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
|
|
|
@@ -3,8 +3,12 @@ import logging
|
|
|
3
3
|
import os
|
|
4
4
|
import shutil
|
|
5
5
|
import subprocess
|
|
6
|
+
import tempfile
|
|
7
|
+
import zipfile
|
|
6
8
|
import pandas as pd
|
|
7
9
|
|
|
10
|
+
from pyxform.xls2xform import convert
|
|
11
|
+
|
|
8
12
|
from tricc_oo.models.lang import SingletonLangClass
|
|
9
13
|
from tricc_oo.models.calculate import TriccNodeEnd
|
|
10
14
|
from tricc_oo.models.tricc import TriccNodeDisplayModel
|
|
@@ -740,10 +744,12 @@ class XLSFormCHTStrategy(XLSFormCDSSStrategy):
|
|
|
740
744
|
generated_files = self.export(self.project.start_pages, version=version)
|
|
741
745
|
|
|
742
746
|
logger.info("validate the output")
|
|
743
|
-
self.validate(generated_files)
|
|
747
|
+
if not self.validate(generated_files):
|
|
748
|
+
logger.error("CHT validation failed - aborting build")
|
|
749
|
+
exit(1)
|
|
744
750
|
|
|
745
751
|
def validate(self, generated_files=None):
|
|
746
|
-
"""Validate the generated XLS form(s) using
|
|
752
|
+
"""Validate the generated XLS form(s) using pyxform conversion and ODK Validate JAR."""
|
|
747
753
|
if generated_files is None:
|
|
748
754
|
# Fallback for single file validation
|
|
749
755
|
if self.project.start_pages["main"].root.form_id is not None:
|
|
@@ -753,10 +759,10 @@ class XLSFormCHTStrategy(XLSFormCDSSStrategy):
|
|
|
753
759
|
logger.error("Form ID not found for validation")
|
|
754
760
|
return False
|
|
755
761
|
|
|
756
|
-
# Ensure
|
|
757
|
-
|
|
758
|
-
if not
|
|
759
|
-
logger.error("
|
|
762
|
+
# Ensure ODK Validate JAR is available
|
|
763
|
+
jar_path = self._ensure_odk_validate_jar()
|
|
764
|
+
if not jar_path:
|
|
765
|
+
logger.error("ODK Validate JAR not available, skipping CHT validation")
|
|
760
766
|
return False
|
|
761
767
|
|
|
762
768
|
all_valid = True
|
|
@@ -767,49 +773,82 @@ class XLSFormCHTStrategy(XLSFormCDSSStrategy):
|
|
|
767
773
|
continue
|
|
768
774
|
|
|
769
775
|
try:
|
|
770
|
-
#
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
+
# Convert XLS to XForm using pyxform (without validation)
|
|
777
|
+
xform_path = xls_file.replace('.xlsx', '.xml')
|
|
778
|
+
convert_result = convert(
|
|
779
|
+
xlsform=xls_file,
|
|
780
|
+
validate=False, # Don't validate during conversion
|
|
781
|
+
pretty_print=True
|
|
776
782
|
)
|
|
783
|
+
xform_content = convert_result.xform
|
|
784
|
+
|
|
785
|
+
# Write XForm to temporary file for validation
|
|
786
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.xml', delete=False) as temp_file:
|
|
787
|
+
temp_file.write(xform_content)
|
|
788
|
+
temp_xform_path = temp_file.name
|
|
789
|
+
|
|
790
|
+
try:
|
|
791
|
+
# Run ODK Validate JAR on the XForm
|
|
792
|
+
result = subprocess.run(
|
|
793
|
+
["java", "-Djava.awt.headless=true", "-jar", jar_path, temp_xform_path],
|
|
794
|
+
capture_output=True,
|
|
795
|
+
text=True,
|
|
796
|
+
cwd=self.output_path
|
|
797
|
+
)
|
|
777
798
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
799
|
+
if result.returncode == 0 or "Cycle detected" in result.stderr:
|
|
800
|
+
logger.info(f"CHT XLSForm validation successful: {os.path.basename(xls_file)}")
|
|
801
|
+
else:
|
|
802
|
+
logger.error(f"CHT XLSForm validation failed for {os.path.basename(xls_file)}: {result.stderr}")
|
|
803
|
+
all_valid = False
|
|
804
|
+
|
|
805
|
+
finally:
|
|
806
|
+
# Clean up temporary XForm file
|
|
807
|
+
os.unlink(temp_xform_path)
|
|
783
808
|
|
|
784
809
|
except Exception as e:
|
|
785
810
|
logger.error(f"CHT XLSForm validation error for {os.path.basename(xls_file)}: {str(e)}")
|
|
786
811
|
all_valid = False
|
|
787
812
|
|
|
788
|
-
|
|
813
|
+
jar_in_zip = "site-packages/pyxform/validators/odk_validate/bin/ODK_Validate.jar"
|
|
814
|
+
zip_ref.extract(jar_in_zip, os.path.dirname(__file__))
|
|
815
|
+
|
|
816
|
+
# Move to final location
|
|
817
|
+
extracted_jar = os.path.join(os.path.dirname(__file__), jar_in_zip)
|
|
818
|
+
shutil.move(extracted_jar, jar_path)
|
|
789
819
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
# Check if it's in PATH
|
|
793
|
-
medic_tool = shutil.which("xls2xform-medic")
|
|
794
|
-
if medic_tool:
|
|
795
|
-
return medic_tool
|
|
820
|
+
logger.info(f"Extracted ODK Validate JAR to {jar_path}")
|
|
821
|
+
return jar_path
|
|
796
822
|
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
823
|
+
def _ensure_odk_validate_jar(self):
|
|
824
|
+
"""Ensure ODK Validate JAR is available by extracting from medic zip."""
|
|
825
|
+
jar_path = os.path.join(os.path.dirname(__file__), "ODK_Validate.jar")
|
|
826
|
+
|
|
827
|
+
# Check if JAR already exists
|
|
828
|
+
if os.path.exists(jar_path):
|
|
829
|
+
return jar_path
|
|
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
|
|
801
836
|
|
|
802
|
-
# Try to download from the provided URL
|
|
803
837
|
try:
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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}")
|
|
848
|
+
return jar_path
|
|
849
|
+
|
|
811
850
|
except Exception as e:
|
|
812
|
-
logger.error(f"Failed to
|
|
851
|
+
logger.error(f"Failed to extract ODK Validate JAR: {str(e)}")
|
|
813
852
|
return None
|
|
814
853
|
|
|
815
854
|
def tricc_operation_zscore(self, ref_expressions):
|
|
@@ -836,7 +875,7 @@ class XLSFormCHTStrategy(XLSFormCDSSStrategy):
|
|
|
836
875
|
self.clean_coalesce(ref_expressions[2])
|
|
837
876
|
} ,{
|
|
838
877
|
self.clean_coalesce(ref_expressions[3])
|
|
839
|
-
}, true
|
|
878
|
+
}, true"""
|
|
840
879
|
|
|
841
880
|
def tricc_operation_drug_dosage(self, ref_expressions):
|
|
842
881
|
# drug name
|
tricc_oo/visitors/tricc.py
CHANGED
|
@@ -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
|
|
@@ -1767,12 +1772,6 @@ def reorder_node_list(node_list, group, processed_nodes):
|
|
|
1767
1772
|
# Check for non main activities
|
|
1768
1773
|
elif activity and isinstance(activity.root, TriccNodeActivityStart):
|
|
1769
1774
|
priority += NON_START_ACTIVITY_PRIORITY
|
|
1770
|
-
# Check for display calculate and end nodes with prev_nodes
|
|
1771
|
-
elif (
|
|
1772
|
-
issubclass(node.__class__, TriccNodeDisplayCalculateBase) or
|
|
1773
|
-
isinstance(node, TriccNodeEnd)
|
|
1774
|
-
) and not isinstance(node, TriccNodeActivityEnd) and hasattr(node, 'prev_nodes') and len(node.prev_nodes) > 0:
|
|
1775
|
-
priority += FLOW_CALCULATE_NODE_PRIORITY
|
|
1776
1775
|
# Check for active activities (lower priority)
|
|
1777
1776
|
elif activity and activity in active_activities:
|
|
1778
1777
|
priority += ACTIVE_ACTIVITY_LOWER_PRIORITY
|
|
@@ -8,9 +8,9 @@ tricc_oo/converters/codesystem_to_ocl.py,sha256=Fh7Vk73OsxljZKu1k6H9uzYwz334tpQT
|
|
|
8
8
|
tricc_oo/converters/cql_to_operation.py,sha256=PUyV_YpUY98Ox0H_F_CN3UUf_I-BhFZVOcWWKTtwecM,14492
|
|
9
9
|
tricc_oo/converters/datadictionnary.py,sha256=T2HLCBo4Am1p0kFqSH1r0PqbD8AC2IGuWkbvMvSCru0,3658
|
|
10
10
|
tricc_oo/converters/drawio_type_map.py,sha256=UCPiGs7Lw0bigKScmZUnmOhACBz-FiDq92jHkI7RTSQ,9113
|
|
11
|
-
tricc_oo/converters/tricc_to_xls_form.py,sha256=
|
|
11
|
+
tricc_oo/converters/tricc_to_xls_form.py,sha256=39hwWgYNitGE-AuKtjUwNLz39tEpwc7nd9gT_gw5wjc,3842
|
|
12
12
|
tricc_oo/converters/utils.py,sha256=JZrtrvvOfXwdkw49pKauzinOcauWwsy-CVcw36TjyLo,1684
|
|
13
|
-
tricc_oo/converters/xml_to_tricc.py,sha256=
|
|
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
|
|
@@ -26,27 +26,27 @@ tricc_oo/parsers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,
|
|
|
26
26
|
tricc_oo/parsers/xml.py,sha256=uzkb1y18MHfqVFmZqVh0sKT4cx6u0-NcAT_lV_gHBt8,4208
|
|
27
27
|
tricc_oo/serializers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
28
28
|
tricc_oo/serializers/planuml.py,sha256=t57587-6L3aDncpHh58lS77Zft8yxDE9DPtXx2BeUSU,132
|
|
29
|
-
tricc_oo/serializers/xls_form.py,sha256=
|
|
29
|
+
tricc_oo/serializers/xls_form.py,sha256=_sYYVmJkGJeeOr8l_FzRIOuCcGzrxeFc1APXBgNNs08,22585
|
|
30
30
|
tricc_oo/strategies/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
31
31
|
tricc_oo/strategies/input/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
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=
|
|
36
|
-
tricc_oo/strategies/output/fhir_form.py,sha256=
|
|
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
|
|
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
|
|
40
40
|
tricc_oo/strategies/output/xls_form.py,sha256=_pNTND7n-55EjRphJ1hSVtRYa-UkXlmwpam2OKQ8o_w,30860
|
|
41
41
|
tricc_oo/strategies/output/xlsform_cdss.py,sha256=X00Lt5MzV8TX14dR4dFI1MqllI5S1e13bKbeysWM9uA,17435
|
|
42
|
-
tricc_oo/strategies/output/xlsform_cht.py,sha256=
|
|
42
|
+
tricc_oo/strategies/output/xlsform_cht.py,sha256=RXacGiVAib5bF_0ea564YhuJ1ASs20MuMuIeZXiP5-M,29019
|
|
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=
|
|
45
|
+
tricc_oo/visitors/tricc.py,sha256=9jAVSxFfklJcLzwLvZAV1_sRuD8616HSgOSl8wdqPTE,110227
|
|
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.
|
|
49
|
-
tricc_oo-1.6.
|
|
50
|
-
tricc_oo-1.6.
|
|
51
|
-
tricc_oo-1.6.
|
|
52
|
-
tricc_oo-1.6.
|
|
48
|
+
tricc_oo-1.6.10.dist-info/licenses/LICENSE,sha256=Pz2eACSxkhsGfW9_iN60pgy-enjnbGTj8df8O3ebnQQ,16726
|
|
49
|
+
tricc_oo-1.6.10.dist-info/METADATA,sha256=kiJuIi8qGKOf5IP63NtQYJ7ReSJJHrReUl3fuWEJS18,8600
|
|
50
|
+
tricc_oo-1.6.10.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
51
|
+
tricc_oo-1.6.10.dist-info/top_level.txt,sha256=NvbfMNAiy9m4b1unBsqpeOQWh4IgA1Xa33BtKA4abxk,15
|
|
52
|
+
tricc_oo-1.6.10.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|