tricc-oo 1.5.26__py3-none-any.whl → 1.6.8__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.
- tests/build.py +1 -0
- tests/test_build.py +260 -0
- tricc_oo/converters/tricc_to_xls_form.py +15 -6
- tricc_oo/converters/xml_to_tricc.py +9 -8
- tricc_oo/models/base.py +4 -2
- tricc_oo/serializers/xls_form.py +34 -18
- tricc_oo/strategies/output/base_output_strategy.py +7 -0
- tricc_oo/strategies/output/dhis2_form.py +908 -0
- tricc_oo/strategies/output/openmrs_form.py +52 -5
- tricc_oo/strategies/output/xls_form.py +62 -32
- tricc_oo/strategies/output/xlsform_cht.py +107 -0
- tricc_oo/visitors/tricc.py +145 -71
- {tricc_oo-1.5.26.dist-info → tricc_oo-1.6.8.dist-info}/METADATA +2 -1
- {tricc_oo-1.5.26.dist-info → tricc_oo-1.6.8.dist-info}/RECORD +17 -15
- {tricc_oo-1.5.26.dist-info → tricc_oo-1.6.8.dist-info}/WHEEL +0 -0
- {tricc_oo-1.5.26.dist-info → tricc_oo-1.6.8.dist-info}/licenses/LICENSE +0 -0
- {tricc_oo-1.5.26.dist-info → tricc_oo-1.6.8.dist-info}/top_level.txt +0 -0
|
@@ -21,6 +21,7 @@ from tricc_oo.models.tricc import (
|
|
|
21
21
|
TriccNodeSelectOption,
|
|
22
22
|
TriccNodeInputModel,
|
|
23
23
|
TriccNodeBaseModel,
|
|
24
|
+
TriccNodeSelect,
|
|
24
25
|
TriccNodeDisplayModel,
|
|
25
26
|
)
|
|
26
27
|
|
|
@@ -69,6 +70,9 @@ class OpenMRSStrategy(BaseOutPutStrategy):
|
|
|
69
70
|
self.current_segment = None
|
|
70
71
|
self.current_activity = None
|
|
71
72
|
self.concept_map = {}
|
|
73
|
+
self.calculated_fields = [] # Store calculated fields to add to first section of each page
|
|
74
|
+
self.calculated_fields_added = set() # Track which pages have had calculated fields added
|
|
75
|
+
self.inject_version()
|
|
72
76
|
|
|
73
77
|
def get_export_name(self, r):
|
|
74
78
|
if isinstance(r, TriccNodeSelectOption):
|
|
@@ -146,6 +150,9 @@ class OpenMRSStrategy(BaseOutPutStrategy):
|
|
|
146
150
|
logger.info("generate the export format")
|
|
147
151
|
self.process_export(self.project.start_pages, pages=self.project.pages)
|
|
148
152
|
|
|
153
|
+
logger.info("create calculation page")
|
|
154
|
+
self.create_calculation_page()
|
|
155
|
+
|
|
149
156
|
logger.info("print the export")
|
|
150
157
|
self.export(self.project.start_pages, version=version)
|
|
151
158
|
|
|
@@ -160,7 +167,7 @@ class OpenMRSStrategy(BaseOutPutStrategy):
|
|
|
160
167
|
'select_multiple': 'multiCheckbox',
|
|
161
168
|
'select_yesno': 'select',
|
|
162
169
|
'not_available': 'checkbox',
|
|
163
|
-
'note': '
|
|
170
|
+
'note': 'markdown'
|
|
164
171
|
}
|
|
165
172
|
|
|
166
173
|
# if issubclass(node.__class__, TriccNodeSelectYesNo):
|
|
@@ -250,7 +257,9 @@ class OpenMRSStrategy(BaseOutPutStrategy):
|
|
|
250
257
|
}
|
|
251
258
|
}
|
|
252
259
|
}
|
|
253
|
-
|
|
260
|
+
# Collect calculated fields to add to first section of each page
|
|
261
|
+
self.calculated_fields.append(question)
|
|
262
|
+
return None # Don't return the question, it will be added to first section
|
|
254
263
|
return None
|
|
255
264
|
|
|
256
265
|
def generate_calculate(self, node, processed_nodes, **kwargs):
|
|
@@ -400,6 +409,8 @@ class OpenMRSStrategy(BaseOutPutStrategy):
|
|
|
400
409
|
return f"'{option}'"
|
|
401
410
|
elif issubclass(r.__class__, TriccNodeInputModel):
|
|
402
411
|
return self.get_export_name(r)
|
|
412
|
+
elif issubclass(r.__class__, TriccNodeSelect):
|
|
413
|
+
return "(" + self.get_export_name(r) + " ?? [])"
|
|
403
414
|
elif issubclass(r.__class__, TriccNodeBaseModel):
|
|
404
415
|
return self.get_export_name(r)
|
|
405
416
|
else:
|
|
@@ -439,7 +450,7 @@ class OpenMRSStrategy(BaseOutPutStrategy):
|
|
|
439
450
|
if len(ref_expressions) == 1:
|
|
440
451
|
return ref_expressions[0]
|
|
441
452
|
if len(ref_expressions) > 1:
|
|
442
|
-
return " || ".join(ref_expressions)
|
|
453
|
+
return "(" + " || ".join(ref_expressions) + ")"
|
|
443
454
|
else:
|
|
444
455
|
return "true"
|
|
445
456
|
|
|
@@ -447,7 +458,7 @@ class OpenMRSStrategy(BaseOutPutStrategy):
|
|
|
447
458
|
return f"!({ref_expressions[0]})"
|
|
448
459
|
|
|
449
460
|
def tricc_operation_plus(self, ref_expressions):
|
|
450
|
-
return " + ".join(ref_expressions)
|
|
461
|
+
return "(" + " + ".join(ref_expressions) + ")"
|
|
451
462
|
|
|
452
463
|
def tricc_operation_minus(self, ref_expressions):
|
|
453
464
|
if len(ref_expressions) > 1:
|
|
@@ -472,7 +483,7 @@ class OpenMRSStrategy(BaseOutPutStrategy):
|
|
|
472
483
|
return f"({ref_expressions[0]}.includes({ref_expressions[1]}))"
|
|
473
484
|
|
|
474
485
|
def tricc_operation_count(self, ref_expressions):
|
|
475
|
-
return f"
|
|
486
|
+
return f"{ref_expressions[0]}.length"
|
|
476
487
|
|
|
477
488
|
def tricc_operation_multiplied(self, ref_expressions):
|
|
478
489
|
return "*".join(ref_expressions)
|
|
@@ -638,6 +649,7 @@ class OpenMRSStrategy(BaseOutPutStrategy):
|
|
|
638
649
|
"label": section_label,
|
|
639
650
|
"questions": []
|
|
640
651
|
})
|
|
652
|
+
|
|
641
653
|
if group_node.relevance:
|
|
642
654
|
relevance_str = self.convert_expression_to_string(not_clean(group_node.relevance))
|
|
643
655
|
if relevance_str and relevance_str != 'false':
|
|
@@ -645,3 +657,38 @@ class OpenMRSStrategy(BaseOutPutStrategy):
|
|
|
645
657
|
"hideWhenExpression": f"{relevance_str}"
|
|
646
658
|
}
|
|
647
659
|
logger.debug(f"Started section: {section_label}")
|
|
660
|
+
|
|
661
|
+
def create_calculation_page(self):
|
|
662
|
+
"""Create a dedicated page for all calculated fields"""
|
|
663
|
+
if self.calculated_fields:
|
|
664
|
+
self.clean_sections()
|
|
665
|
+
self.clean_pages()
|
|
666
|
+
page = {
|
|
667
|
+
"label": "Calculations",
|
|
668
|
+
"sections": [
|
|
669
|
+
{
|
|
670
|
+
"label": "Calculations",
|
|
671
|
+
"questions": self.calculated_fields
|
|
672
|
+
}
|
|
673
|
+
]
|
|
674
|
+
}
|
|
675
|
+
self.form_data["pages"].append(page)
|
|
676
|
+
logger.debug("Created calculation page")
|
|
677
|
+
|
|
678
|
+
def inject_version(self):
|
|
679
|
+
# Add hidden version field using version() function
|
|
680
|
+
question = {
|
|
681
|
+
"id": "version",
|
|
682
|
+
"type": "control",
|
|
683
|
+
"label": "",
|
|
684
|
+
"hide": {
|
|
685
|
+
"hideWhenExpression": "true"
|
|
686
|
+
},
|
|
687
|
+
"questionOptions": {
|
|
688
|
+
"calculate": {
|
|
689
|
+
"calculateExpression": "version()"
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
# Collect calculated fields to add to first section of each page
|
|
694
|
+
self.calculated_fields.append(question)
|
|
@@ -8,6 +8,7 @@ import logging
|
|
|
8
8
|
import os
|
|
9
9
|
import re
|
|
10
10
|
import pandas as pd
|
|
11
|
+
from pyxform import create_survey_from_xls
|
|
11
12
|
|
|
12
13
|
from tricc_oo.converters.utils import clean_name
|
|
13
14
|
from tricc_oo.models.base import (
|
|
@@ -131,7 +132,7 @@ class XLSFormStrategy(BaseOutPutStrategy):
|
|
|
131
132
|
return generate_xls_form_export(self, node, **kwargs)
|
|
132
133
|
|
|
133
134
|
def inject_version(self):
|
|
134
|
-
# Add hidden version field using ODK's
|
|
135
|
+
# Add hidden version field using ODK's version()
|
|
135
136
|
empty = langs.get_trads("", force_dict=True)
|
|
136
137
|
self.df_survey.loc[len(self.df_survey)] = [
|
|
137
138
|
"hidden",
|
|
@@ -148,7 +149,7 @@ class XLSFormStrategy(BaseOutPutStrategy):
|
|
|
148
149
|
"", # required
|
|
149
150
|
*list(empty.values()), # required_message
|
|
150
151
|
"", # read only
|
|
151
|
-
"
|
|
152
|
+
"version()", # calculation
|
|
152
153
|
"", # trigger
|
|
153
154
|
"", # repeat_count
|
|
154
155
|
"", # image
|
|
@@ -409,7 +410,7 @@ class XLSFormStrategy(BaseOutPutStrategy):
|
|
|
409
410
|
return f"count-selected({self.clean_coalesce(ref_expressions[0])})"
|
|
410
411
|
|
|
411
412
|
def tricc_operation_multiplied(self, ref_expressions):
|
|
412
|
-
return "*".join(ref_expressions)
|
|
413
|
+
return "*".join(map(str, ref_expressions))
|
|
413
414
|
|
|
414
415
|
def tricc_operation_divided(self, ref_expressions):
|
|
415
416
|
return f"{ref_expressions[0]} div {ref_expressions[1]}"
|
|
@@ -430,7 +431,7 @@ class XLSFormStrategy(BaseOutPutStrategy):
|
|
|
430
431
|
return f"-{ref_expressions[0]}"
|
|
431
432
|
|
|
432
433
|
def tricc_operation_plus(self, ref_expressions):
|
|
433
|
-
return " + ".join(ref_expressions)
|
|
434
|
+
return " + ".join(map(str, ref_expressions))
|
|
434
435
|
|
|
435
436
|
def tricc_operation_not(self, ref_expressions):
|
|
436
437
|
return f"not({ref_expressions[0]})"
|
|
@@ -460,9 +461,9 @@ class XLSFormStrategy(BaseOutPutStrategy):
|
|
|
460
461
|
return "1"
|
|
461
462
|
|
|
462
463
|
def tricc_operation_native(self, ref_expressions):
|
|
463
|
-
|
|
464
|
+
|
|
464
465
|
if len(ref_expressions) > 0:
|
|
465
|
-
if ref_expressions[0].startswith(("'","`",)):
|
|
466
|
+
if ref_expressions[0].startswith(("'", "`",)):
|
|
466
467
|
ref_expressions[0] = ref_expressions[0][1:-1]
|
|
467
468
|
if ref_expressions[0] == "GetChoiceName":
|
|
468
469
|
return f"""jr:choice-name({
|
|
@@ -474,7 +475,7 @@ class XLSFormStrategy(BaseOutPutStrategy):
|
|
|
474
475
|
return "0"
|
|
475
476
|
# return f"jr:choice-name({','.join(ref_expressions[1:])})"
|
|
476
477
|
else:
|
|
477
|
-
return f"{ref_expressions[0]}({','.join(ref_expressions[1:])})"
|
|
478
|
+
return f"{ref_expressions[0]}({','.join(map(str, ref_expressions[1:]))})"
|
|
478
479
|
|
|
479
480
|
def tricc_operation_istrue(self, ref_expressions):
|
|
480
481
|
if str(BOOLEAN_MAP[str(TRICC_TRUE_VALUE)]).isnumeric():
|
|
@@ -495,7 +496,12 @@ class XLSFormStrategy(BaseOutPutStrategy):
|
|
|
495
496
|
parts = []
|
|
496
497
|
for s in ref_expressions[1:]:
|
|
497
498
|
# for option with numeric value
|
|
498
|
-
|
|
499
|
+
if isinstance(s, str):
|
|
500
|
+
cleaned_s = s
|
|
501
|
+
elif isinstance(s, TriccNodeSelectOption):
|
|
502
|
+
cleaned_s = s.name
|
|
503
|
+
else:
|
|
504
|
+
cleaned_s = "'" + str(s) + "'"
|
|
499
505
|
parts.append(f"selected({self.clean_coalesce(ref_expressions[0])}, {cleaned_s})")
|
|
500
506
|
if len(parts) == 1:
|
|
501
507
|
return parts[0]
|
|
@@ -548,15 +554,15 @@ class XLSFormStrategy(BaseOutPutStrategy):
|
|
|
548
554
|
ifs = 0
|
|
549
555
|
parts = []
|
|
550
556
|
else_found = False
|
|
551
|
-
if
|
|
557
|
+
if isinstance(ref_expressions[0], list):
|
|
552
558
|
return self.tricc_operation_ifs(ref_expressions)
|
|
553
|
-
for i in range(int(len(ref_expressions))):
|
|
559
|
+
for i in range(int(len(ref_expressions[1:]))):
|
|
554
560
|
if isinstance(ref_expressions[i], list):
|
|
555
|
-
parts.append(f"if({ref_expressions[i][0]},{ref_expressions[i][1]}")
|
|
561
|
+
parts.append(f"if({ref_expressions[0]}={ref_expressions[i+1][0]},{ref_expressions[i+1][1]}")
|
|
556
562
|
ifs += 1
|
|
557
563
|
else:
|
|
558
564
|
else_found = True
|
|
559
|
-
parts.append(ref_expressions[i])
|
|
565
|
+
parts.append(ref_expressions[i+1])
|
|
560
566
|
# join the if
|
|
561
567
|
exp = ",".join(map(str, parts))
|
|
562
568
|
# in case there is no default put ''
|
|
@@ -571,15 +577,16 @@ class XLSFormStrategy(BaseOutPutStrategy):
|
|
|
571
577
|
ifs = 0
|
|
572
578
|
parts = []
|
|
573
579
|
else_found = False
|
|
574
|
-
for i in range(int(len(ref_expressions
|
|
575
|
-
if isinstance(ref_expressions[i
|
|
576
|
-
parts.append(f"if({ref_expressions[
|
|
580
|
+
for i in range(int(len(ref_expressions))):
|
|
581
|
+
if isinstance(ref_expressions[i], list):
|
|
582
|
+
parts.append(f"if({ref_expressions[i][0]},{ref_expressions[i][1]}")
|
|
577
583
|
ifs += 1
|
|
578
584
|
else:
|
|
579
585
|
else_found = True
|
|
580
|
-
parts.append(ref_expressions[i
|
|
586
|
+
parts.append(ref_expressions[i])
|
|
587
|
+
break
|
|
581
588
|
# join the if
|
|
582
|
-
exp = ",".join(parts)
|
|
589
|
+
exp = ",".join(map(str, parts))
|
|
583
590
|
# in case there is no default put ''
|
|
584
591
|
if not else_found:
|
|
585
592
|
exp += ",''"
|
|
@@ -588,11 +595,14 @@ class XLSFormStrategy(BaseOutPutStrategy):
|
|
|
588
595
|
exp += ")"
|
|
589
596
|
return exp
|
|
590
597
|
|
|
598
|
+
def get_empty_label(self):
|
|
599
|
+
return "."
|
|
600
|
+
|
|
591
601
|
def tricc_operation_if(self, ref_expressions):
|
|
592
602
|
return f"if({ref_expressions[0]},{ref_expressions[1]},{ref_expressions[2]})"
|
|
593
603
|
|
|
594
604
|
def tricc_operation_contains(self, ref_expressions):
|
|
595
|
-
return f"contains(
|
|
605
|
+
return f"contains({self.clean_coalesce(ref_expressions[0])}, {self.clean_coalesce(ref_expressions[1])})"
|
|
596
606
|
|
|
597
607
|
def tricc_operation_exists(self, ref_expressions):
|
|
598
608
|
parts = []
|
|
@@ -720,22 +730,12 @@ class XLSFormStrategy(BaseOutPutStrategy):
|
|
|
720
730
|
# @param r reference to be translated
|
|
721
731
|
if isinstance(r, TriccOperation):
|
|
722
732
|
return self.get_tricc_operation_expression(r)
|
|
733
|
+
elif isinstance(r, (TriccStatic, str, int, float)):
|
|
734
|
+
return get_export_name(r)
|
|
723
735
|
elif isinstance(r, TriccReference):
|
|
724
736
|
logger.warning(f"reference `{r.value}` still used in a calculate")
|
|
725
737
|
return f"${{{get_export_name(r.value)}}}"
|
|
726
|
-
|
|
727
|
-
return get_export_name(r)
|
|
728
|
-
elif isinstance(r, str):
|
|
729
|
-
if r == TRICC_TRUE_VALUE:
|
|
730
|
-
return BOOLEAN_MAP[str(TRICC_TRUE_VALUE)]
|
|
731
|
-
elif r == TRICC_FALSE_VALUE:
|
|
732
|
-
return BOOLEAN_MAP[str(TRICC_FALSE_VALUE)]
|
|
733
|
-
elif isinstance(r, str):
|
|
734
|
-
return f"'{r}'"
|
|
735
|
-
else:
|
|
736
|
-
return str(r)
|
|
737
|
-
elif isinstance(r, (int, float)):
|
|
738
|
-
return str(r)
|
|
738
|
+
|
|
739
739
|
elif isinstance(r, TriccNodeSelectOption):
|
|
740
740
|
logger.debug(f"select option {r.get_name()} from {r.select.get_name()} was used as a reference")
|
|
741
741
|
return get_export_name(r)
|
|
@@ -747,4 +747,34 @@ class XLSFormStrategy(BaseOutPutStrategy):
|
|
|
747
747
|
raise NotImplementedError(f"This type of node {r.__class__} is not supported within an operation")
|
|
748
748
|
|
|
749
749
|
def tricc_operation_concatenate(self, ref_expressions):
|
|
750
|
-
return f"concat({','.join(ref_expressions)})"
|
|
750
|
+
return f"concat({','.join(map(str, ref_expressions))})"
|
|
751
|
+
|
|
752
|
+
def validate(self):
|
|
753
|
+
"""Validate the generated XLS form using pyxform."""
|
|
754
|
+
try:
|
|
755
|
+
# Determine the XLS file path
|
|
756
|
+
if self.project.start_pages["main"].root.form_id is not None:
|
|
757
|
+
form_id = str(self.project.start_pages["main"].root.form_id)
|
|
758
|
+
xls_path = os.path.join(self.output_path, form_id + ".xlsx")
|
|
759
|
+
|
|
760
|
+
if not os.path.exists(xls_path):
|
|
761
|
+
logger.error(f"XLS file not found: {xls_path}")
|
|
762
|
+
return False
|
|
763
|
+
|
|
764
|
+
# Validate using pyxform
|
|
765
|
+
survey = create_survey_from_xls(xls_path)
|
|
766
|
+
xml_output = survey.to_xml()
|
|
767
|
+
|
|
768
|
+
# Check if XML was generated successfully
|
|
769
|
+
if xml_output and len(xml_output.strip()) > 0:
|
|
770
|
+
logger.info("XLSForm validation successful")
|
|
771
|
+
return True
|
|
772
|
+
else:
|
|
773
|
+
logger.error("XLSForm validation failed: Empty XML output")
|
|
774
|
+
return False
|
|
775
|
+
else:
|
|
776
|
+
logger.error("Form ID not found for validation")
|
|
777
|
+
return False
|
|
778
|
+
except Exception as e:
|
|
779
|
+
logger.error(f"XLSForm validation failed: {str(e)}")
|
|
780
|
+
return False
|
|
@@ -2,6 +2,7 @@ import datetime
|
|
|
2
2
|
import logging
|
|
3
3
|
import os
|
|
4
4
|
import shutil
|
|
5
|
+
import subprocess
|
|
5
6
|
import pandas as pd
|
|
6
7
|
|
|
7
8
|
from tricc_oo.models.lang import SingletonLangClass
|
|
@@ -33,6 +34,9 @@ class XLSFormCHTStrategy(XLSFormCDSSStrategy):
|
|
|
33
34
|
|
|
34
35
|
self.inject_version()
|
|
35
36
|
|
|
37
|
+
def get_empty_label(self):
|
|
38
|
+
return "NO_LABEL"
|
|
39
|
+
|
|
36
40
|
def get_cht_input(self, start_pages, **kwargs):
|
|
37
41
|
empty = langs.get_trads("", force_dict=True)
|
|
38
42
|
df_input = pd.DataFrame(columns=SURVEY_MAP.keys())
|
|
@@ -613,6 +617,9 @@ class XLSFormCHTStrategy(XLSFormCDSSStrategy):
|
|
|
613
617
|
newpath = os.path.join(self.output_path, newfilename)
|
|
614
618
|
media_path = os.path.join(self.output_path, form_id + "-media")
|
|
615
619
|
|
|
620
|
+
# Track all generated XLS files for validation
|
|
621
|
+
generated_files = [newpath]
|
|
622
|
+
|
|
616
623
|
settings = {
|
|
617
624
|
"form_title": title,
|
|
618
625
|
"form_id": form_id,
|
|
@@ -657,6 +664,7 @@ class XLSFormCHTStrategy(XLSFormCDSSStrategy):
|
|
|
657
664
|
new_form_id = f"{form_id}_{clean_name(e.name)}"
|
|
658
665
|
newfilename = f"{new_form_id}.xlsx"
|
|
659
666
|
newpath = os.path.join(self.output_path, newfilename)
|
|
667
|
+
generated_files.append(newpath) # Track additional XLS files
|
|
660
668
|
settings = {
|
|
661
669
|
"form_title": title,
|
|
662
670
|
"form_id": f"{new_form_id}",
|
|
@@ -705,6 +713,105 @@ class XLSFormCHTStrategy(XLSFormCDSSStrategy):
|
|
|
705
713
|
shutil.move(os.path.join(media_path_tmp, file_name), media_path)
|
|
706
714
|
shutil.rmtree(media_path_tmp)
|
|
707
715
|
|
|
716
|
+
return generated_files
|
|
717
|
+
|
|
718
|
+
def execute(self):
|
|
719
|
+
"""Override execute to handle multiple output files from CHT strategy."""
|
|
720
|
+
version = datetime.datetime.now().strftime("%Y%m%d%H%M")
|
|
721
|
+
logger.info(f"build version: {version}")
|
|
722
|
+
if "main" in self.project.start_pages:
|
|
723
|
+
self.process_base(self.project.start_pages, pages=self.project.pages, version=version)
|
|
724
|
+
else:
|
|
725
|
+
logger.critical("Main process required")
|
|
726
|
+
|
|
727
|
+
logger.info("generate the relevance based on edges")
|
|
728
|
+
|
|
729
|
+
# create relevance Expression
|
|
730
|
+
|
|
731
|
+
# create calculate Expression
|
|
732
|
+
self.process_calculate(self.project.start_pages, pages=self.project.pages)
|
|
733
|
+
logger.info("generate the export format")
|
|
734
|
+
# create calculate Expression
|
|
735
|
+
self.process_export(self.project.start_pages, pages=self.project.pages)
|
|
736
|
+
|
|
737
|
+
logger.info("print the export")
|
|
738
|
+
|
|
739
|
+
# Export returns list of generated files for CHT strategy
|
|
740
|
+
generated_files = self.export(self.project.start_pages, version=version)
|
|
741
|
+
|
|
742
|
+
logger.info("validate the output")
|
|
743
|
+
self.validate(generated_files)
|
|
744
|
+
|
|
745
|
+
def validate(self, generated_files=None):
|
|
746
|
+
"""Validate the generated XLS form(s) using xls2xform-medic."""
|
|
747
|
+
if generated_files is None:
|
|
748
|
+
# Fallback for single file validation
|
|
749
|
+
if self.project.start_pages["main"].root.form_id is not None:
|
|
750
|
+
form_id = str(self.project.start_pages["main"].root.form_id)
|
|
751
|
+
generated_files = [os.path.join(self.output_path, form_id + ".xlsx")]
|
|
752
|
+
else:
|
|
753
|
+
logger.error("Form ID not found for validation")
|
|
754
|
+
return False
|
|
755
|
+
|
|
756
|
+
# Ensure xls2xform-medic is available
|
|
757
|
+
medic_tool = self._ensure_xls2xform_medic()
|
|
758
|
+
if not medic_tool:
|
|
759
|
+
logger.error("xls2xform-medic tool not available, skipping CHT validation")
|
|
760
|
+
return False
|
|
761
|
+
|
|
762
|
+
all_valid = True
|
|
763
|
+
for xls_file in generated_files:
|
|
764
|
+
if not os.path.exists(xls_file):
|
|
765
|
+
logger.error(f"XLS file not found: {xls_file}")
|
|
766
|
+
all_valid = False
|
|
767
|
+
continue
|
|
768
|
+
|
|
769
|
+
try:
|
|
770
|
+
# Run xls2xform-medic validation
|
|
771
|
+
result = subprocess.run(
|
|
772
|
+
[medic_tool, xls_file],
|
|
773
|
+
capture_output=True,
|
|
774
|
+
text=True,
|
|
775
|
+
cwd=self.output_path
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
if result.returncode == 0:
|
|
779
|
+
logger.info(f"CHT XLSForm validation successful: {os.path.basename(xls_file)}")
|
|
780
|
+
else:
|
|
781
|
+
logger.error(f"CHT XLSForm validation failed for {os.path.basename(xls_file)}: {result.stderr}")
|
|
782
|
+
all_valid = False
|
|
783
|
+
|
|
784
|
+
except Exception as e:
|
|
785
|
+
logger.error(f"CHT XLSForm validation error for {os.path.basename(xls_file)}: {str(e)}")
|
|
786
|
+
all_valid = False
|
|
787
|
+
|
|
788
|
+
return all_valid
|
|
789
|
+
|
|
790
|
+
def _ensure_xls2xform_medic(self):
|
|
791
|
+
"""Ensure xls2xform-medic tool is available."""
|
|
792
|
+
# Check if it's in PATH
|
|
793
|
+
medic_tool = shutil.which("xls2xform-medic")
|
|
794
|
+
if medic_tool:
|
|
795
|
+
return medic_tool
|
|
796
|
+
|
|
797
|
+
# Check if we need to download it
|
|
798
|
+
medic_path = os.path.join(os.path.dirname(__file__), "xls2xform-medic")
|
|
799
|
+
if os.path.exists(medic_path):
|
|
800
|
+
return medic_path
|
|
801
|
+
|
|
802
|
+
# Try to download from the provided URL
|
|
803
|
+
try:
|
|
804
|
+
import urllib.request
|
|
805
|
+
medic_url = "https://github.com/medic/pyxform/releases/download/v4.0.0-medic/xls2xform-medic"
|
|
806
|
+
logger.info(f"Downloading xls2xform-medic from {medic_url}")
|
|
807
|
+
urllib.request.urlretrieve(medic_url, medic_path)
|
|
808
|
+
# Make executable
|
|
809
|
+
os.chmod(medic_path, 0o755)
|
|
810
|
+
return medic_path
|
|
811
|
+
except Exception as e:
|
|
812
|
+
logger.error(f"Failed to download xls2xform-medic: {str(e)}")
|
|
813
|
+
return None
|
|
814
|
+
|
|
708
815
|
def tricc_operation_zscore(self, ref_expressions):
|
|
709
816
|
y, ll, m, s = self.get_zscore_params(ref_expressions)
|
|
710
817
|
# return ((Math.pow((y / m), l) - 1) / (s * l));
|