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.
@@ -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': 'text'
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
- return question
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"({ref_expressions[0]}.length)"
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 jr:version()
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
- "jr:version()", # calculation
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
- cleaned_s = s if isinstance(s, str) else "'" + str(s) + "'"
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 not isinstance(ref_expressions[0], list):
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[1:]))):
575
- if isinstance(ref_expressions[i + 1], list):
576
- parts.append(f"if({ref_expressions[0]}={ref_expressions[i+1][0]},{ref_expressions[i+1][1]}")
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 + 1])
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('{self.clean_coalesce(ref_expressions[0])}', '{self.clean_coalesce(ref_expressions[1])}')"
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
- elif isinstance(r, TriccStatic):
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));