From 612362b34a4b438657e5e90e86f35cf869af75af Mon Sep 17 00:00:00 2001 From: TiaTuinstra Date: Tue, 4 Nov 2025 18:36:07 +0000 Subject: [PATCH 1/4] Brought in changes from single-client pdf refactor; some testing added; added pretty-json to pre-commit linting --- .pre-commit-config.yaml | 8 +- config/disease_normalization.json | 2 +- config/map_school.json | 33 + config/parameters.yaml | 6 + config/translations/en_diseases_chart.json | 4 +- config/translations/fr_diseases_chart.json | 16 +- config/translations/fr_diseases_overdue.json | 14 +- config/vaccine_reference.json | 738 +++++++++--------- docs/email_package/convert_docs_to_pdf.py | 2 +- pipeline/config_loader.py | 27 + pipeline/generate_notices.py | 54 +- pipeline/orchestrator.py | 24 +- ...2025_mock_generate_template_english_map.sh | 178 +++++ scripts/generate_notices_map.sh | 22 + scripts/run_pipeline_map.sh | 167 ++++ templates/en_template.py | 21 +- templates/fr_template.py | 21 +- tests/unit/test_en_template.py | 40 + tests/unit/test_fr_template.py | 45 ++ 19 files changed, 1010 insertions(+), 412 deletions(-) create mode 100644 config/map_school.json create mode 100755 scripts/2025_mock_generate_template_english_map.sh create mode 100755 scripts/generate_notices_map.sh create mode 100755 scripts/run_pipeline_map.sh diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 39f5579..2cb2ddd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,4 +4,10 @@ repos: hooks: - id: ruff-check args: [--fix] # Lint and auto-fix - - id: ruff-format # Format code like black \ No newline at end of file + - id: ruff-format # Format code like black + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: pretty-format-json + args: [--autofix] + files: \.json$ \ No newline at end of file diff --git a/config/disease_normalization.json b/config/disease_normalization.json index 135db27..53653a7 100644 --- a/config/disease_normalization.json +++ b/config/disease_normalization.json @@ -1,8 +1,8 @@ { "Haemophilus influenzae infection, invasive": "Hib", "Haemophilus influenzae infection,invasive": "Hib", - "Poliomyelitis": "Polio", "Human papilloma virus infection": "HPV", "Human papillomavirus infection": "HPV", + "Poliomyelitis": "Polio", "Varicella": "Varicella" } diff --git a/config/map_school.json b/config/map_school.json new file mode 100644 index 0000000..2dee235 --- /dev/null +++ b/config/map_school.json @@ -0,0 +1,33 @@ +{ + "BURROW_PUBLIC_SCHOOL": { + "phu_address": "Burrow Health, 123 Placeholder Street, Sample City, ON A1A 1A1", + "phu_email": "mainpublichealth@rodenthealth.ca", + "phu_phone": "555-555-5555 ext. 1234", + "phu_website": "https://www.test-immunization.ca" + }, + "CHEESE_WHEEL_ACADEMY": { + "phu_address": "Cheese Wheel Health, 123 Placeholder Street, Sample City, ON A1A 1A1", + "phu_email": "mainpublichealth@rodenthealth.ca", + "phu_phone": "555-555-5555 ext. 1234" + }, + "DEFAULT": { + "phu_address": "Tunnel Health, 123 Placeholder Street, Sample City, ON A1A 1A1", + "phu_email": "mainpublichealth@rodenthealth.ca", + "phu_phone": "555-555-5555 ext. 1234" + }, + "DOWNTOWN_COLLEGIATE": { + "phu_address": "Tunnel Health, 123 Placeholder Street, Sample City, ON A1A 1A1", + "phu_email": "mainpublichealth@rodenthealth.ca", + "phu_phone": "555-555-5555 ext. 1234" + }, + "MOUNTAIN_HEIGHTS_PUBLIC_SCHOOL": { + "phu_address": "Mountain Heights Health, 123 Placeholder Street, Sample City, ON A1A 1A1", + "phu_email": "mainpublichealth@rodenthealth.ca", + "phu_phone": "555-555-5555 ext. 1234" + }, + "RIVER_VALLEY_ELEMENTARY": { + "phu_address": "Tunnel Health, 123 Placeholder Street, Sample City, ON A1A 1A1", + "phu_email": "mainpublichealth@rodenthealth.ca", + "phu_phone": "555-555-5555 ext. 1234" + } +} diff --git a/config/parameters.yaml b/config/parameters.yaml index c071261..f706f95 100644 --- a/config/parameters.yaml +++ b/config/parameters.yaml @@ -37,9 +37,15 @@ ignore_agents: - HBIg - RabIg - Ig +phu_data: + phu_address: Tunnel Health, 123 Placeholder Street, Sample City, ON A1A 1A1 + phu_email: mainpublichealth@rodenthealth.ca + phu_phone: 555-555-5555 ext. 1234 + phu_website: https://www.test-immunization.ca pipeline: auto_remove_output: true keep_intermediate_files: true + map_school: true qr: enabled: true payload_template: https://www.test-immunization.ca/update?client_id={client_id}&dob={date_of_birth_iso}&lang={language_code} diff --git a/config/translations/en_diseases_chart.json b/config/translations/en_diseases_chart.json index 9663d4b..05d4fd1 100644 --- a/config/translations/en_diseases_chart.json +++ b/config/translations/en_diseases_chart.json @@ -6,12 +6,12 @@ "Measles": "Measles", "Meningococcal": "Meningococcal", "Mumps": "Mumps", + "Other": "Other", "Pertussis": "Pertussis", "Pneumococcal": "Pneumococcal", "Polio": "Polio", "Rotavirus": "Rotavirus", "Rubella": "Rubella", "Tetanus": "Tetanus", - "Varicella": "Varicella", - "Other": "Other" + "Varicella": "Varicella" } diff --git a/config/translations/fr_diseases_chart.json b/config/translations/fr_diseases_chart.json index e09a1c1..4c71be1 100644 --- a/config/translations/fr_diseases_chart.json +++ b/config/translations/fr_diseases_chart.json @@ -1,17 +1,17 @@ { - "Diphtheria": "Diphtérie", + "Diphtheria": "Dipht\u00e9rie", "HPV": "VPH", - "Hepatitis B": "Hépatite B", + "Hepatitis B": "H\u00e9patite B", "Hib": "Hib", "Measles": "Rougeole", - "Meningococcal": "Méningocoque", + "Meningococcal": "M\u00e9ningocoque", "Mumps": "Oreillons", + "Other": "Autre", "Pertussis": "Coqueluche", "Pneumococcal": "Pneumocoque", - "Polio": "Poliomyélite", + "Polio": "Poliomy\u00e9lite", "Rotavirus": "Rotavirus", - "Rubella": "Rubéole", - "Tetanus": "Tétanos", - "Varicella": "Varicelle", - "Other": "Autre" + "Rubella": "Rub\u00e9ole", + "Tetanus": "T\u00e9tanos", + "Varicella": "Varicelle" } diff --git a/config/translations/fr_diseases_overdue.json b/config/translations/fr_diseases_overdue.json index 1cbeee8..70507ba 100644 --- a/config/translations/fr_diseases_overdue.json +++ b/config/translations/fr_diseases_overdue.json @@ -1,16 +1,16 @@ { - "Diphtheria": "Diphtérie", + "Diphtheria": "Dipht\u00e9rie", "HPV": "VPH", - "Hepatitis B": "Hépatite B", + "Hepatitis B": "H\u00e9patite B", "Hib": "Hib", "Measles": "Rougeole", - "Meningococcal": "Méningocoque", + "Meningococcal": "M\u00e9ningocoque", "Mumps": "Oreillons", "Pertussis": "Coqueluche", "Pneumococcal": "Pneumocoque", - "Polio": "Poliomyélite", + "Polio": "Poliomy\u00e9lite", "Rotavirus": "Rotavirus", - "Rubella": "Rubéole", - "Tetanus": "Tétanos", + "Rubella": "Rub\u00e9ole", + "Tetanus": "T\u00e9tanos", "Varicella": "Varicelle" -} \ No newline at end of file +} diff --git a/config/vaccine_reference.json b/config/vaccine_reference.json index 35864de..3727f83 100644 --- a/config/vaccine_reference.json +++ b/config/vaccine_reference.json @@ -1,370 +1,370 @@ { - "aP": [ - "Pertussis" - ], - "ap-unspecified": [ - "Pertussis" - ], - "BAT": [ - "Other" - ], - "BCG vaccine": [ - "Other" - ], - "Chol": [ - "Other" - ], - "Chol-Ecol-O": [ - "Other" - ], - "D": [ - "Diphtheria" - ], - "DPT": [ - "Diphtheria", - "Tetanus", - "Pertussis" - ], - "DPT-HB": [ - "Diphtheria", - "Tetanus", - "Pertussis", - "Hepatitis B" - ], - "DPT-HB-Hib": [ - "Diphtheria", - "Tetanus", - "Pertussis", - "Hepatitis B", - "Hib" - ], - "DPT-Hib": [ - "Diphtheria", - "Tetanus", - "Pertussis", - "Hib" - ], - "DPT-IPV": [ - "Diphtheria", - "Tetanus", - "Pertussis", - "Polio" - ], - "DPTP": [ - "Diphtheria", - "Tetanus", - "Pertussis", - "Polio" - ], - "DPTP-Hib": [ - "Diphtheria", - "Tetanus", - "Pertussis", - "Polio", - "Hib" - ], - "DT": [ - "Diphtheria", - "Tetanus" - ], - "DTaP": [ - "Diphtheria", - "Tetanus", - "Pertussis" - ], - "DTaP-HB-IPV": [ - "Diphtheria", - "Tetanus", - "Pertussis", - "Polio", - "Hepatitis B" - ], - "DTaP-HB-IPV-Hib": [ - "Diphtheria", - "Tetanus", - "Pertussis", - "Polio", - "Hepatitis B", - "Hib" - ], - "DTaP-Hib": [ - "Diphtheria", - "Tetanus", - "Pertussis", - "Hib" - ], - "DTaP-IPV": [ - "Diphtheria", - "Tetanus", - "Pertussis", - "Polio" - ], - "DTaP-IPV-Hib": [ - "Diphtheria", - "Tetanus", - "Pertussis", - "Polio", - "Hib" - ], - "DT-IPV": [ - "Diphtheria", - "Tetanus", - "Polio" - ], - "DTwP-HB": [ - "Diphtheria", - "Tetanus", - "Pertussis", - "Hepatitis B" - ], - "d-unspecified": [ - "Diphtheria" - ], - "H1N1": [ - "Other" - ], - "HA": [ - "Other" - ], - "HAHB": [ - "Hepatitis B", - "Other" - ], - "HAHB-pediatric": [ - "Hepatitis B", - "Other" - ], - "HAHB-unspecified": [ - "Hepatitis B", - "Other" - ], - "HA-pediatric": [ - "Other" - ], - "HA-Typh-I": [ - "Other" - ], - "HA-unspecified": [ - "Other" - ], - "HB": [ - "Hepatitis B" - ], - "HB-dialysis": [ - "Hepatitis B" - ], - "HB-pediatric": [ - "Hepatitis B" - ], - "HB-unspecified": [ - "Hepatitis B" - ], - "Hib": [ - "Hib" - ], - "Hib-HB": [ - "Hepatitis B", - "Hib" - ], - "HPV-2": [ - "HPV" - ], - "HPV-4": [ - "HPV" - ], - "HPV-9": [ - "HPV" - ], - "hpv-unspecified": [ - "HPV" - ], - "Inf (QIV)": [ - "Other" - ], - "Inf (TIV)": [ - "Other" - ], - "Inf High Dose (TIV)": [ - "Other" - ], - "inf-unspecified": [ - "Other" - ], - "IPV": [ - "Polio" - ], - "JE": [ - "Other" - ], - "LAIV": [ - "Other" - ], - "M": [ - "Measles" - ], - "men-AC unspecified": [ - "Meningococcal" - ], - "Men-ACYW-135-unspecified": [ - "Meningococcal" - ], - "Men-B": [ - "Meningococcal" - ], - "Men-C-A": [ - "Meningococcal" - ], - "Men-C-AC": [ - "Meningococcal" - ], - "Men-C-ACYW-135": [ - "Meningococcal" - ], - "Men-C-C": [ - "Meningococcal" - ], - "Men-C-CY-Hib": [ - "Meningococcal", - "Hib" - ], - "men-c-unspecified": [ - "Meningococcal" - ], - "men-p-AC unspecified": [ - "Meningococcal" - ], - "Men-P-ACYW-135": [ - "Meningococcal" - ], - "men-p-A unspecified": [ - "Meningococcal" - ], - "men-p-unspecified": [ - "Meningococcal" - ], - "men-unspecified": [ - "Meningococcal" - ], - "MMR": [ - "Measles", - "Mumps", - "Rubella" - ], - "MMR-Var": [ - "Measles", - "Mumps", - "Rubella", - "Varicella" - ], - "MR": [ - "Measles", - "Rubella" - ], - "Mu": [ - "Mumps" - ], - "OPV": [ - "Polio" - ], - "pertussis-unspecified": [ - "Pertussis" - ], - "Pneu-C-10": [ - "Pneumococcal" - ], - "Pneu-C-13": [ - "Pneumococcal" - ], - "Pneu-C-15": [ - "Pneumococcal" - ], - "Pneu-C-20": [ - "Pneumococcal" - ], - "Pneu-C-7": [ - "Pneumococcal" - ], - "pneu-c-unspecified": [ - "Pneumococcal" - ], - "Pneu-P-23": [ - "Pneumococcal" - ], - "pneu-p-unspecified": [ - "Pneumococcal" - ], - "pneu-unspecified": [ - "Pneumococcal" - ], - "p-unspecified": [ - "Polio" - ], - "R": [ - "Rubella" - ], - "Rab": [ - "Other" - ], - "Rota-1": [ - "Rotavirus" - ], - "Rota-5": [ - "Rotavirus" - ], - "rota-unspecified": [ - "Rotavirus" - ], - "RSV": [ - "Other" - ], - "Sma": [ - "Other" - ], - "T": [ - "Tetanus" - ], - "TBE": [ - "Other" - ], - "Td": [ - "Diphtheria", - "Tetanus" - ], - "Tdap": [ - "Diphtheria", - "Tetanus", - "Pertussis" - ], - "Tdap-IPV": [ - "Diphtheria", - "Tetanus", - "Pertussis", - "Polio" - ], - "Td-IPV": [ - "Diphtheria", - "Tetanus", - "Polio" - ], - "Typh-I": [ - "Other" - ], - "Typh-O": [ - "Other" - ], - "typh-unspecified": [ - "Other" - ], - "Var": [ - "Varicella" - ], - "YF": [ - "Other" - ], - "Zos": [ - "Other" - ], - "Zos-unspecified": [ - "Other" - ] -} \ No newline at end of file + "BAT": [ + "Other" + ], + "BCG vaccine": [ + "Other" + ], + "Chol": [ + "Other" + ], + "Chol-Ecol-O": [ + "Other" + ], + "D": [ + "Diphtheria" + ], + "DPT": [ + "Diphtheria", + "Tetanus", + "Pertussis" + ], + "DPT-HB": [ + "Diphtheria", + "Tetanus", + "Pertussis", + "Hepatitis B" + ], + "DPT-HB-Hib": [ + "Diphtheria", + "Tetanus", + "Pertussis", + "Hepatitis B", + "Hib" + ], + "DPT-Hib": [ + "Diphtheria", + "Tetanus", + "Pertussis", + "Hib" + ], + "DPT-IPV": [ + "Diphtheria", + "Tetanus", + "Pertussis", + "Polio" + ], + "DPTP": [ + "Diphtheria", + "Tetanus", + "Pertussis", + "Polio" + ], + "DPTP-Hib": [ + "Diphtheria", + "Tetanus", + "Pertussis", + "Polio", + "Hib" + ], + "DT": [ + "Diphtheria", + "Tetanus" + ], + "DT-IPV": [ + "Diphtheria", + "Tetanus", + "Polio" + ], + "DTaP": [ + "Diphtheria", + "Tetanus", + "Pertussis" + ], + "DTaP-HB-IPV": [ + "Diphtheria", + "Tetanus", + "Pertussis", + "Polio", + "Hepatitis B" + ], + "DTaP-HB-IPV-Hib": [ + "Diphtheria", + "Tetanus", + "Pertussis", + "Polio", + "Hepatitis B", + "Hib" + ], + "DTaP-Hib": [ + "Diphtheria", + "Tetanus", + "Pertussis", + "Hib" + ], + "DTaP-IPV": [ + "Diphtheria", + "Tetanus", + "Pertussis", + "Polio" + ], + "DTaP-IPV-Hib": [ + "Diphtheria", + "Tetanus", + "Pertussis", + "Polio", + "Hib" + ], + "DTwP-HB": [ + "Diphtheria", + "Tetanus", + "Pertussis", + "Hepatitis B" + ], + "H1N1": [ + "Other" + ], + "HA": [ + "Other" + ], + "HA-Typh-I": [ + "Other" + ], + "HA-pediatric": [ + "Other" + ], + "HA-unspecified": [ + "Other" + ], + "HAHB": [ + "Hepatitis B", + "Other" + ], + "HAHB-pediatric": [ + "Hepatitis B", + "Other" + ], + "HAHB-unspecified": [ + "Hepatitis B", + "Other" + ], + "HB": [ + "Hepatitis B" + ], + "HB-dialysis": [ + "Hepatitis B" + ], + "HB-pediatric": [ + "Hepatitis B" + ], + "HB-unspecified": [ + "Hepatitis B" + ], + "HPV-2": [ + "HPV" + ], + "HPV-4": [ + "HPV" + ], + "HPV-9": [ + "HPV" + ], + "Hib": [ + "Hib" + ], + "Hib-HB": [ + "Hepatitis B", + "Hib" + ], + "IPV": [ + "Polio" + ], + "Inf (QIV)": [ + "Other" + ], + "Inf (TIV)": [ + "Other" + ], + "Inf High Dose (TIV)": [ + "Other" + ], + "JE": [ + "Other" + ], + "LAIV": [ + "Other" + ], + "M": [ + "Measles" + ], + "MMR": [ + "Measles", + "Mumps", + "Rubella" + ], + "MMR-Var": [ + "Measles", + "Mumps", + "Rubella", + "Varicella" + ], + "MR": [ + "Measles", + "Rubella" + ], + "Men-ACYW-135-unspecified": [ + "Meningococcal" + ], + "Men-B": [ + "Meningococcal" + ], + "Men-C-A": [ + "Meningococcal" + ], + "Men-C-AC": [ + "Meningococcal" + ], + "Men-C-ACYW-135": [ + "Meningococcal" + ], + "Men-C-C": [ + "Meningococcal" + ], + "Men-C-CY-Hib": [ + "Meningococcal", + "Hib" + ], + "Men-P-ACYW-135": [ + "Meningococcal" + ], + "Mu": [ + "Mumps" + ], + "OPV": [ + "Polio" + ], + "Pneu-C-10": [ + "Pneumococcal" + ], + "Pneu-C-13": [ + "Pneumococcal" + ], + "Pneu-C-15": [ + "Pneumococcal" + ], + "Pneu-C-20": [ + "Pneumococcal" + ], + "Pneu-C-7": [ + "Pneumococcal" + ], + "Pneu-P-23": [ + "Pneumococcal" + ], + "R": [ + "Rubella" + ], + "RSV": [ + "Other" + ], + "Rab": [ + "Other" + ], + "Rota-1": [ + "Rotavirus" + ], + "Rota-5": [ + "Rotavirus" + ], + "Sma": [ + "Other" + ], + "T": [ + "Tetanus" + ], + "TBE": [ + "Other" + ], + "Td": [ + "Diphtheria", + "Tetanus" + ], + "Td-IPV": [ + "Diphtheria", + "Tetanus", + "Polio" + ], + "Tdap": [ + "Diphtheria", + "Tetanus", + "Pertussis" + ], + "Tdap-IPV": [ + "Diphtheria", + "Tetanus", + "Pertussis", + "Polio" + ], + "Typh-I": [ + "Other" + ], + "Typh-O": [ + "Other" + ], + "Var": [ + "Varicella" + ], + "YF": [ + "Other" + ], + "Zos": [ + "Other" + ], + "Zos-unspecified": [ + "Other" + ], + "aP": [ + "Pertussis" + ], + "ap-unspecified": [ + "Pertussis" + ], + "d-unspecified": [ + "Diphtheria" + ], + "hpv-unspecified": [ + "HPV" + ], + "inf-unspecified": [ + "Other" + ], + "men-AC unspecified": [ + "Meningococcal" + ], + "men-c-unspecified": [ + "Meningococcal" + ], + "men-p-A unspecified": [ + "Meningococcal" + ], + "men-p-AC unspecified": [ + "Meningococcal" + ], + "men-p-unspecified": [ + "Meningococcal" + ], + "men-unspecified": [ + "Meningococcal" + ], + "p-unspecified": [ + "Polio" + ], + "pertussis-unspecified": [ + "Pertussis" + ], + "pneu-c-unspecified": [ + "Pneumococcal" + ], + "pneu-p-unspecified": [ + "Pneumococcal" + ], + "pneu-unspecified": [ + "Pneumococcal" + ], + "rota-unspecified": [ + "Rotavirus" + ], + "typh-unspecified": [ + "Other" + ] +} diff --git a/docs/email_package/convert_docs_to_pdf.py b/docs/email_package/convert_docs_to_pdf.py index fd44f37..2867255 100644 --- a/docs/email_package/convert_docs_to_pdf.py +++ b/docs/email_package/convert_docs_to_pdf.py @@ -11,4 +11,4 @@ if file.endswith(".md"): md_path = os.path.join(input_dir, file) output_path = os.path.join(output_dir, os.path.splitext(file)[0] + ".html") - pypandoc.convert_file(input_dir, "html", outputfile=output_path) \ No newline at end of file + pypandoc.convert_file(input_dir, "html", outputfile=output_path) diff --git a/pipeline/config_loader.py b/pipeline/config_loader.py index af37c46..fe6c47d 100644 --- a/pipeline/config_loader.py +++ b/pipeline/config_loader.py @@ -86,6 +86,33 @@ def validate_config(config: Dict[str, Any]) -> None: - All error messages are clear and actionable - Config is validated once at load time, not per-step """ + # Validate phu_data config + phu_data = config.get("phu_data", {}) + + if not phu_data: + raise ValueError( + "Default values for PHU info not provided. " + "Please define phu_data.[phu_addres, phu_phone, phu_email, phu_website] in config/parameters.yaml" + ) + else: + required_keys = {"phu_address", "phu_phone", "phu_email", "phu_website"} + + missing = [key for key in required_keys if key not in phu_data] + if missing: + missing_keys = ", ".join(missing) + raise KeyError(f"Missing phu_data keys in config: {missing_keys}") + + # Validate school map config + map_enabled = config.get("pipeline", {})["map_school"] + + if map_enabled: + map_filepath = DEFAULT_CONFIG_PATH.parent / "map_school.json" + if not map_filepath.exists(): + raise FileNotFoundError( + "Mapping to school-specific info enabled, but expected mapping file not present." + "Please provide mapping file at config/map_school.json." + ) + # Validate QR config qr_config = config.get("qr", {}) qr_enabled = qr_config.get("enabled", True) diff --git a/pipeline/generate_notices.py b/pipeline/generate_notices.py index d7353b1..d1993be 100644 --- a/pipeline/generate_notices.py +++ b/pipeline/generate_notices.py @@ -40,6 +40,7 @@ from __future__ import annotations import json +import re import logging from pathlib import Path from typing import Dict, List, Mapping, Sequence @@ -255,7 +256,9 @@ def load_and_translate_chart_diseases(language: str) -> List[str]: def build_template_context( - client: ClientRecord, qr_output_dir: Path | None = None + client: ClientRecord, + qr_output_dir: Path | None = None, + map_file: Path | None = None, ) -> Dict[str, str]: """Build template context from client data. @@ -270,7 +273,8 @@ def build_template_context( Client record with all required fields. qr_output_dir : Path, optional Directory containing QR code PNG files. - + map_file: Filepath, optional + File containing mapping of schools to specific info (e.g. satellite PHU office info) to populate template. Returns ------- Dict[str, str] @@ -306,6 +310,41 @@ def build_template_context( if qr_path.exists(): client_data["qr_code"] = to_root_relative(qr_path) + # Attempt to load default PHU data values from config; this should also contain all required keys + try: + phu_data = config.get("phu_data", {}) + except KeyError as err: + raise ( + f"Loading default PHU info, error when attempting to access 'phu_data' from config: {err}" + ) + + # Check if supposed to map to satellite office + if map_file: + # Load mapping file data + with open(map_file, "r") as f: + map_data = json.load(f) + + # Clean school name + client_school_key = re.sub(r"\s+", "_", client_data["school"]).upper() + + # Check if school has a mapping associated with it - otherwise, use default config values + if client_school_key in map_data.keys(): + map_client_data = map_data[client_school_key] + + # Replace default values with values in map file. If any are missing, keep default values. + for key in phu_data.keys(): + if key in map_client_data.keys(): + phu_data[key] = map_client_data[key] + else: + print( + f"Mapping file for school {client_school_key} missing {key}. Using default value." + ) + + else: + print( + f"School {client_school_key} not in mapping file. Using default values." + ) + # Load and translate chart disease header chart_diseases_translated = load_and_translate_chart_diseases(client.language) @@ -346,6 +385,7 @@ def build_template_context( return { "client_row": to_typ_value([client.client_id]), "client_data": to_typ_value(client_data), + "phu_data": phu_data, "vaccines_due_str": to_typ_value(vaccines_due_str_translated), "vaccines_due_array": to_typ_value(vaccines_due_array_translated), "received": to_typ_value(received_translated), @@ -393,10 +433,12 @@ def render_notice( logo: Path, signature: Path, qr_output_dir: Path | None = None, + map_file: Path | None = None, ) -> str: language = Language.from_string(client.language) renderer = get_language_renderer(language) - context = build_template_context(client, qr_output_dir) + context = build_template_context(client, qr_output_dir, map_file) + print(context) return renderer( context, logo_path=to_root_relative(logo), @@ -409,6 +451,7 @@ def generate_typst_files( output_dir: Path, logo_path: Path, signature_path: Path, + map_file: Path | None = None, ) -> List[Path]: output_dir.mkdir(parents=True, exist_ok=True) qr_output_dir = output_dir / "qr_codes" @@ -427,6 +470,7 @@ def generate_typst_files( logo=logo_path, signature=signature_path, qr_output_dir=qr_output_dir, + map_file=map_file, ) filename = f"{language}_notice_{client.sequence}_{client.client_id}.typ" file_path = typst_output_dir / filename @@ -441,6 +485,7 @@ def main( output_dir: Path, logo_path: Path, signature_path: Path, + map_file: Path | None = None, ) -> List[Path]: """Main entry point for Typst notice generation. @@ -454,6 +499,8 @@ def main( Path to the logo image. signature_path : Path Path to the signature image. + map_file : Path + (Optional) Path to file mapping schools to template details (e.g. PHU satellite offices). Returns ------- @@ -466,6 +513,7 @@ def main( output_dir, logo_path, signature_path, + map_file, ) print( f"Generated {len(generated)} Typst files in {output_dir} for language {payload.language}" diff --git a/pipeline/orchestrator.py b/pipeline/orchestrator.py index f42e19c..f4630af 100755 --- a/pipeline/orchestrator.py +++ b/pipeline/orchestrator.py @@ -246,6 +246,7 @@ def run_step_4_generate_notices( run_id: str, assets_dir: Path, config_dir: Path, + map_file: Path | None = None, ) -> None: """Step 4: Generating Typst templates.""" print_step(4, "Generating Typst templates") @@ -257,10 +258,7 @@ def run_step_4_generate_notices( # Generate Typst files using main function generated = generate_notices.main( - artifact_path, - artifacts_dir, - logo_path, - signature_path, + artifact_path, artifacts_dir, logo_path, signature_path, map_file ) print(f"Generated {len(generated)} Typst files in {artifacts_dir}") @@ -433,6 +431,19 @@ def main() -> int: encryption_enabled = config.get("encryption", {}).get("enabled", False) auto_remove_output = pipeline_config.get("auto_remove_output", False) keep_intermediate = pipeline_config.get("keep_intermediate_files", False) + map_school = pipeline_config.get("map_school", False) + + if map_school: + map_file = config_dir / "map_school.json" + if map_file.exists(): + "School mapping file provided." + else: + print( + "Expected school mapping file at '{map_file}', but file does not exist." + ) + return 1 + else: + map_file = None print_header(args.input_file) @@ -479,10 +490,7 @@ def main() -> int: # Step 4: Generating Notices step_start = time.time() run_step_4_generate_notices( - output_dir, - run_id, - DEFAULT_TEMPLATES_ASSETS_DIR, - config_dir, + output_dir, run_id, DEFAULT_TEMPLATES_ASSETS_DIR, config_dir, map_file ) step_duration = time.time() - step_start step_times.append(("Template Generation", step_duration)) diff --git a/scripts/2025_mock_generate_template_english_map.sh b/scripts/2025_mock_generate_template_english_map.sh new file mode 100755 index 0000000..476ca52 --- /dev/null +++ b/scripts/2025_mock_generate_template_english_map.sh @@ -0,0 +1,178 @@ +#!/bin/bash + +INDIR=${1} +FILENAME=${2} +LOGO=${3} +SIGNATURE=${4} +PARAMETERS=${5} +MAP_SCHOOL=${6} + +CLIENTIDFILE=${FILENAME}_client_ids.csv +JSONFILE=${FILENAME}.json +OUTFILE=${INDIR}/${FILENAME}_immunization_notice.typ + + +echo " +// --- CCEYA NOTICE TEMPLATE (TEST VERSION) --- // +// Description: A typst template that dynamically generates 2025 cceya templates for phsd. +// NOTE: All contact details are placeholders for testing purposes only. +// Author: Kassy Raymond +// Date Created: 2025-06-25 +// Date Last Updated: 2025-09-16 +// ----------------------------------------- // + +#import \"conf.typ\" + +// General document formatting +#set text(fill: black) +#set par(justify: false) +#set page(\"us-letter\") + +// Formatting links +#show link: underline + +// Font formatting +#set text( + font: \"FreeSans\", + size: 10pt +) + +// Read current date from yaml file +#let date(contents) = { + contents.date_today +} + +// Read diseases from yaml file +#let diseases_yaml(contents) = { + contents.chart_diseases_header +} + +#let diseases = diseases_yaml(yaml(\"${PARAMETERS}\")) +#let date = date(yaml(\"${PARAMETERS}\")) + +// Immunization Notice Section +#let immunization_notice(client, client_id, immunizations_due, date, font_size, school_address, school_phone) = block[ + +#v(0.2cm) + +#conf.header_info_cim(\"${LOGO}\") + +#v(0.2cm) + +#conf.client_info_tbl_en(equal_split: false, vline: false, client, client_id, font_size) + +#v(0.3cm) + +// Notice for immunizations +As of *#date* our files show that your child has not received the following immunization(s): + +#conf.client_immunization_list(immunizations_due) + +Please review the Immunization Record on page 2 and update your child's record by using one of the following options: + +1. By visiting #text(fill:conf.linkcolor)[#link(\"https://www.test-immunization.ca\")] +2. By emailing #text(fill:conf.linkcolor)[#link(\"records@test-immunization.ca\")] +3. By mailing a photocopy of your child’s immunization record to #school_address +4. By Phone: #school_phone + +Please update Public Health and your childcare centre every time your child receives a vaccine. By keeping your child's vaccinations up to date, you are not only protecting their health but also the health of other children and staff at the childcare centre. + +*If you are choosing not to immunize your child*, a valid medical exemption or statement of conscience or religious belief must be completed and submitted to Public Health. Links to these forms can be located at #text(fill:conf.wdgteal)[#link(\"https://www.test-immunization.ca/exemptions\")]. Please note this exemption is for childcare only and a new exemption will be required upon enrollment in elementary school. + +If there is an outbreak of a vaccine-preventable disease, Public Health may require that children who are not adequately immunized (including those with exemptions) be excluded from the childcare centre until the outbreak is over. + +If you have any questions about your child’s vaccines, please call 555-555-5555 ext. 1234 to speak with a Public Health Nurse. + + Sincerely, + +#conf.signature(\"${SIGNATURE}\", \"Dr. Jane Smith, MPH\", \"Associate Medical Officer of Health\") + +] + +#let vaccine_table_page(client_id) = block[ + + #v(0.5cm) + + #grid( + + columns: (50%,50%), + gutter: 5%, + [#image(\"${LOGO}\", width: 6cm)], + [#set align(center + bottom) + #text(size: 20.5pt, fill: black)[*Immunization Record*]] + +) + + #v(0.5cm) + + For your reference, the immunization(s) on file with Public Health are as follows: + +] + +#let end_of_immunization_notice() = [ + #set align(center) + End of immunization record ] + +#let client_ids = csv(\"${CLIENTIDFILE}\", delimiter: \",\", row-type: array) + +#for row in client_ids { + + let reset = <__reset> + let subtotal() = { + let loc = here() + let list = query(selector(reset).after(loc)) + if list.len() > 0 { + counter(page).at(list.first().location()).first() - 1 + } else { + counter(page).final().first() + } +} + + let page-numbers = context numbering( + \"1 / 1\", + ..counter(page).get(), + subtotal(), + ) + + set page(margin: (top: 1cm, bottom: 2cm, left: 1.75cm, right: 2cm), + footer: align(center, page-numbers)) + + let value = row.at(0) // Access the first (and only) element of the row + let data = json(\"${JSONFILE}\").at(value) + let received = data.received + + let school_address = \"Test Health, 123 Placeholder Street, Sample City, ON A1A 1A1\" + let school_phone = \"555-555-5555 ext. 1234\" + + if \"${MAP_SCHOOL}\" != \"false\" { + let school = upper(data.school.replace(regex(\"\\s+\"), \"_\")) + let school_data = json(\"${MAP_SCHOOL}\").at(school) + school_address = school_data.phu_address + school_phone = school_data.phu_phone + } + + let num_rows = received.len() + + // get vaccines due, split string into an array of sub strings + let vaccines_due = data.vaccines_due + + let vaccines_due_array = vaccines_due.split(\", \") + + let section(it) = { + [#metadata(none)#reset] + pagebreak(weak: true) + counter(page).update(1) // Reset page counter for this section + pagebreak(weak: true) + immunization_notice(data, row, vaccines_due_array, date, 11pt, school_address, school_phone) + pagebreak() + vaccine_table_page(value) + conf.immunization-table(5, num_rows, received, diseases, 11pt) + end_of_immunization_notice() + } + + section([] + page-numbers) + +} + + +" > "${OUTFILE}" \ No newline at end of file diff --git a/scripts/generate_notices_map.sh b/scripts/generate_notices_map.sh new file mode 100755 index 0000000..5240be9 --- /dev/null +++ b/scripts/generate_notices_map.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +OUTDIR="../output" +LANG=$1 +MAP_SCHOOL=$2 + +if [ "$MAP_SCHOOL" != "false" ]; then + MAP_SCHOOL="../../config/map_school.json" + echo "Using school mapping file: $MAP_SCHOOL" +fi + +echo "Generating templates..." + +for jsonfile in ${OUTDIR}/json_${LANG}/*.json; do + filename=$(basename "$jsonfile" .json) + echo "Processing $filename" + ./2025_mock_generate_template_${LANG}.sh "${OUTDIR}/json_${LANG}" "$filename" \ + "../../assets/logo.png" \ + "../../assets/signature.png" \ + "../../config/parameters.yaml" \ + $MAP_SCHOOL +done diff --git a/scripts/run_pipeline_map.sh b/scripts/run_pipeline_map.sh new file mode 100755 index 0000000..e96c8f6 --- /dev/null +++ b/scripts/run_pipeline_map.sh @@ -0,0 +1,167 @@ +#!/bin/bash +set -e + +if [ $# -lt 2 ]; then + echo "Usage: $0 [--no-cleanup]" + exit 1 +fi + +INFILE=$1 +LANG=$2 +SKIP_CLEANUP=false +MAP_SCHOOL=false + + +# Shift past the first two arguments +shift 2 + +# Define allowed flags +allowed_flags=("--no-cleanup" "--map-school") + +# Loop through remaining arguments +while [[ $# -gt 0 ]]; do + flag="$1" + shift + + # Check if flag is allowed + if [[ " ${allowed_flags[@]} " =~ " ${flag} " ]]; then + case $flag in + --no-cleanup) + SKIP_CLEANUP=true + ;; + --map-school) + MAP_SCHOOL=true + ;; + *) + echo "Unknown option: $flag" + echo "Usage: $0 [--no-cleanup] [--map-school]" + exit 1 + ;; + esac + + else + echo "Error: Unknown flag $flag" + exit 1 + fi +done + +INDIR="../input" +OUTDIR="../output" +BATCH_SIZE=100 + +if [ "$LANG" != "english" ] && [ "$LANG" != "french" ]; then + echo "Error: Language must be 'english' or 'french'" + exit 1 +fi + +echo "" +echo "🚀 Starting VIPER Pipeline" +echo "🗂️ Input File: ${INFILE}" +echo "" + +TOTAL_START=$(date +%s) + + +########################################## +# Step 1: Preprocessing +########################################## +STEP1_START=$(date +%s) +echo "" +echo "🔍 Step 1: Preprocessing started..." +python preprocess.py ${INDIR} ${INFILE} ${OUTDIR} ${LANG} ${BATCH_SIZE} +STEP1_END=$(date +%s) +STEP1_DURATION=$((STEP1_END - STEP1_START)) +echo "✅ Step 1: Preprocessing complete in ${STEP1_DURATION} seconds." + +########################################## +# Record count +########################################## +CSV_PATH="${INDIR}/${CSVFILE}" +if [ -f "$CSV_PATH" ]; then + TOTAL_RECORDS=$(tail -n +2 "$CSV_PATH" | wc -l) + echo "📊 Total records (excluding header): $TOTAL_RECORDS" +else + echo "⚠️ CSV not found for record count: $CSV_PATH" +fi + +########################################## +# Step 2: Generating Notices +########################################## +STEP2_START=$(date +%s) +echo "" +echo "📝 Step 2: Generating Typst templates..." +bash ./generate_notices.sh ${LANG} ${MAP_SCHOOL} +STEP2_END=$(date +%s) +STEP2_DURATION=$((STEP2_END - STEP2_START)) +echo "✅ Step 2: Template generation complete in ${STEP2_DURATION} seconds." + +########################################## +# Step 3: Compiling Notices +########################################## +STEP3_START=$(date +%s) + +# Check to see if the conf.typ file is in the json_ directory +if [ -e "${OUTDIR}/json_${LANG}/conf.typ" ]; then + echo "Found conf.typ in ${OUTDIR}/json_${LANG}/" +else + # Move conf.typ to the json_ directory + echo "Moving conf.typ to ${OUTDIR}/json_${LANG}/" + cp ./conf.typ "${OUTDIR}/json_${LANG}/conf.typ" +fi + +echo "" +echo "📄 Step 3: Compiling Typst templates..." +bash ./compile_notices.sh ${LANG} +STEP3_END=$(date +%s) +STEP3_DURATION=$((STEP3_END - STEP3_START)) +echo "✅ Step 3: Compilation complete in ${STEP3_DURATION} seconds." + +########################################## +# Step 4: Checking length of compiled files against expected length +########################################## + +echo "" +echo "📏 Step 4: Checking length of compiled files..." + +# Remove conf.pdf if it exists +if [ -e "${OUTDIR}/json_${LANG}/conf.pdf" ]; then + echo "Removing existing conf.pdf..." + rm "${OUTDIR}/json_${LANG}/conf.pdf" +fi + +for file in "${OUTDIR}/json_${LANG}/"*.pdf; do + python count_pdfs.py ${file} +done + +########################################## +# Step 5: Cleanup +########################################## + +echo "" +if [ "$SKIP_CLEANUP" = true ]; then + echo "🧹 Step 5: Cleanup skipped (--no-cleanup flag)." +else + echo "🧹 Step 5: Cleanup started..." + python cleanup.py ${OUTDIR} ${LANG} +fi + +########################################## +# Summary +########################################## +TOTAL_END=$(date +%s) +TOTAL_DURATION=$((TOTAL_END - TOTAL_START)) + +echo "" +echo "🎉 Pipeline completed successfully!" +echo "🕒 Time Summary:" +echo " - Preprocessing: ${STEP1_DURATION}s" +echo " - Template Generation: ${STEP2_DURATION}s" +echo " - Template Compilation: ${STEP3_DURATION}s" +echo " - -----------------------------" +echo " - Total Time: ${TOTAL_DURATION}s" +echo "" +echo "📦 Batch size: ${BATCH_SIZE}" +echo "📊 Total records: ${TOTAL_RECORDS}" +if [ "$SKIP_CLEANUP" = true ]; then + echo "🧹 Cleanup: Skipped" +fi \ No newline at end of file diff --git a/templates/en_template.py b/templates/en_template.py index 93b1e88..8b5ea70 100644 --- a/templates/en_template.py +++ b/templates/en_template.py @@ -45,7 +45,7 @@ ) // Immunization Notice Section -#let immunization_notice(client, client_id, immunizations_due, date, font_size) = block[ +#let immunization_notice(client, client_id, immunizations_due, date, font_size, phu_address, phu_phone, phu_email, phu_website) = block[ #v(0.2cm) @@ -64,10 +64,10 @@ Please review the Immunization Record on page 2 and update your child's record by using one of the following options: -1. By visiting #text(fill:conf.linkcolor)[#link("https://www.test-immunization.ca")] -2. By emailing #text(fill:conf.linkcolor)[#link("records@test-immunization.ca")] -3. By mailing a photocopy of your child's immunization record to Test Health, 123 Placeholder Street, Sample City, ON A1A 1A1 -4. By Phone: 555-555-5555 ext. 1234 +1. By visiting #text(fill:conf.linkcolor)[#link(phu_website)] +2. By emailing #text(fill:conf.linkcolor)[#link(phu_email)] +3. By mailing a photocopy of your child's immunization record to #phu_address +4. By Phone: #phu_phone Please update Public Health and your childcare centre every time your child receives a vaccine. @@ -124,13 +124,17 @@ #let num_rows = __NUM_ROWS__ #let diseases = __CHART_DISEASES_TRANSLATED__ #let date = data.date_data_cutoff +#let phu_address = "__PHU_ADDRESS__" +#let phu_phone = "__PHU_PHONE__" +#let phu_email = "__PHU_EMAIL__" +#let phu_website = "__PHU_WEBSITE__" #set page( margin: (top: 1cm, bottom: 2cm, left: 1.75cm, right: 2cm), footer: align(center, context numbering("1 / " + str(counter(page).final().first()), counter(page).get().first())) ) -#immunization_notice(data, client_row, vaccines_due_array, date, 11pt) +#immunization_notice(data, client_row, vaccines_due_array, date, 11pt, phu_address, phu_phone, phu_email, phu_website) #pagebreak() #vaccine_table_page(client_row.at(0)) #conf.immunization-table(5, num_rows, received, diseases, 11pt) @@ -181,6 +185,7 @@ def render_notice( "received", "num_rows", "chart_diseases_translated", + "phu_data", ) missing = [key for key in required_keys if key not in context] if missing: @@ -199,5 +204,9 @@ def render_notice( .replace("__RECEIVED__", context["received"]) .replace("__NUM_ROWS__", context["num_rows"]) .replace("__CHART_DISEASES_TRANSLATED__", context["chart_diseases_translated"]) + .replace("__PHU_ADDRESS__", context["phu_data"]["phu_address"]) + .replace("__PHU_EMAIL__", context["phu_data"]["phu_email"]) + .replace("__PHU_PHONE__", context["phu_data"]["phu_phone"]) + .replace("__PHU_WEBSITE__", context["phu_data"]["phu_website"]) ) return prefix + dynamic diff --git a/templates/fr_template.py b/templates/fr_template.py index e4471e6..df8b6b8 100644 --- a/templates/fr_template.py +++ b/templates/fr_template.py @@ -46,7 +46,7 @@ ) // Immunization Notice Section -#let immunization_notice(client, client_id, immunizations_due, date, font_size) = block[ +#let immunization_notice(client, client_id, immunizations_due, date, font_size, phu_address, phu_phone, phu_email, phu_website) = block[ #v(0.2cm) @@ -65,10 +65,10 @@ Veuillez examiner le dossier d'immunisation à la page 2 et mettre à jour le dossier de votre enfant en utilisant l'une des options suivantes : -1. En visitant #text(fill:conf.linkcolor)[#link("https://www.test-immunization.ca")] -2. En envoyant un courriel à #text(fill:conf.linkcolor)[#link("records@test-immunization.ca")] -3. En envoyant par la poste une photocopie du dossier d'immunisation de votre enfant à Test Health, 123 Placeholder Street, Sample City, ON A1A 1A1 -4. Par téléphone : 555-555-5555 poste 1234 +1. En visitant #text(fill:conf.linkcolor)[#link(phu_website)] +2. En envoyant un courriel à #text(fill:conf.linkcolor)[#link(phu_email)] +3. En envoyant par la poste une photocopie du dossier d'immunisation de votre enfant à #phu_address +4. Par téléphone : #phu_phone Veuillez informer la Santé publique et votre centre de garde d'enfants chaque fois que votre enfant reçoit un vaccin. En gardant les vaccinations de votre enfant à jour, vous protégez non seulement sa santé, mais aussi la santé des autres enfants et du personnel du centre de garde d'enfants. @@ -125,13 +125,17 @@ #let num_rows = __NUM_ROWS__ #let diseases = __CHART_DISEASES_TRANSLATED__ #let date = data.date_data_cutoff +#let phu_address = "__PHU_ADDRESS__" +#let phu_phone = "__PHU_PHONE__" +#let phu_email = "__PHU_EMAIL__" +#let phu_website = "__PHU_WEBSITE__" #set page( margin: (top: 1cm, bottom: 2cm, left: 1.75cm, right: 2cm), footer: align(center, context numbering("1 / " + str(counter(page).final().first()), counter(page).get().first())) ) -#immunization_notice(data, client_row, vaccines_due_array, date, 11pt) +#immunization_notice(data, client_row, vaccines_due_array, date, 11pt, phu_address, phu_phone, phu_email, phu_website) #pagebreak() #vaccine_table_page(client_row.at(0)) #conf.immunization-table(5, num_rows, received, diseases, 11pt) @@ -182,6 +186,7 @@ def render_notice( "received", "num_rows", "chart_diseases_translated", + "phu_data", ) missing = [key for key in required_keys if key not in context] if missing: @@ -200,5 +205,9 @@ def render_notice( .replace("__RECEIVED__", context["received"]) .replace("__NUM_ROWS__", context["num_rows"]) .replace("__CHART_DISEASES_TRANSLATED__", context["chart_diseases_translated"]) + .replace("__PHU_ADDRESS__", context["phu_data"]["phu_address"]) + .replace("__PHU_EMAIL__", context["phu_data"]["phu_email"]) + .replace("__PHU_PHONE__", context["phu_data"]["phu_phone"]) + .replace("__PHU_WEBSITE__", context["phu_data"]["phu_website"]) ) return prefix + dynamic diff --git a/tests/unit/test_en_template.py b/tests/unit/test_en_template.py index 1c737d3..8611475 100644 --- a/tests/unit/test_en_template.py +++ b/tests/unit/test_en_template.py @@ -45,6 +45,11 @@ def test_render_notice_with_valid_context(self) -> None: "received": '(("MMR", "2020-05-15"), ("DPT", "2019-03-15"))', "num_rows": "2", "chart_diseases_translated": '("Diphtheria", "Tetanus", "Pertussis")', + "phu_data": { + "phu_address": "Tunnel Health, 123 Placeholder Street, Sample City, ON A1A 1A1", + "phu_email": "mainpublichealth#rodenthealth.ca", + "phu_phone": "555-555-5555 ext. 1234", + }, } result = render_notice( @@ -73,6 +78,11 @@ def test_render_notice_missing_client_row_raises_error(self) -> None: "received": "()", "num_rows": "0", "chart_diseases_translated": '("Diphtheria", "Tetanus", "Pertussis")', + "phu_data": { + "phu_address": "Tunnel Health, 123 Placeholder Street, Sample City, ON A1A 1A1", + "phu_email": "mainpublichealth#rodenthealth.ca", + "phu_phone": "555-555-5555 ext. 1234", + }, } with pytest.raises(KeyError, match="Missing context keys"): @@ -116,6 +126,11 @@ def test_render_notice_substitutes_logo_path(self) -> None: "received": "()", "num_rows": "0", "chart_diseases_translated": '("Diphtheria", "Tetanus", "Pertussis")', + "phu_data": { + "phu_address": "Tunnel Health, 123 Placeholder Street, Sample City, ON A1A 1A1", + "phu_email": "mainpublichealth#rodenthealth.ca", + "phu_phone": "555-555-5555 ext. 1234", + }, } logo_path = "/custom/logo/path.png" @@ -142,6 +157,11 @@ def test_render_notice_substitutes_signature_path(self) -> None: "received": "()", "num_rows": "0", "chart_diseases_translated": '("Diphtheria", "Tetanus", "Pertussis")', + "phu_data": { + "phu_address": "Tunnel Health, 123 Placeholder Street, Sample City, ON A1A 1A1", + "phu_email": "mainpublichealth#rodenthealth.ca", + "phu_phone": "555-555-5555 ext. 1234", + }, } signature_path = "/custom/signature.png" @@ -168,6 +188,11 @@ def test_render_notice_includes_template_prefix(self) -> None: "received": "()", "num_rows": "0", "chart_diseases_translated": '("Diphtheria", "Tetanus", "Pertussis")', + "phu_data": { + "phu_address": "Tunnel Health, 123 Placeholder Street, Sample City, ON A1A 1A1", + "phu_email": "mainpublichealth#rodenthealth.ca", + "phu_phone": "555-555-5555 ext. 1234", + }, } result = render_notice( @@ -194,6 +219,11 @@ def test_render_notice_includes_dynamic_block(self) -> None: "received": "()", "num_rows": "1", "chart_diseases_translated": '("Diphtheria", "Tetanus", "Pertussis")', + "phu_data": { + "phu_address": "Tunnel Health, 123 Placeholder Street, Sample City, ON A1A 1A1", + "phu_email": "mainpublichealth#rodenthealth.ca", + "phu_phone": "555-555-5555 ext. 1234", + }, } result = render_notice( @@ -222,6 +252,11 @@ def test_render_notice_with_complex_client_data(self) -> None: "received": '(("Measles", "2020-05-01"), ("Mumps", "2020-05-01"))', "num_rows": "5", "chart_diseases_translated": '("Diphtheria", "Tetanus", "Pertussis")', + "phu_data": { + "phu_address": "Tunnel Health, 123 Placeholder Street, Sample City, ON A1A 1A1", + "phu_email": "mainpublichealth#rodenthealth.ca", + "phu_phone": "555-555-5555 ext. 1234", + }, } result = render_notice( @@ -250,6 +285,11 @@ def test_render_notice_empty_vaccines_handled(self) -> None: "received": "()", "num_rows": "0", "chart_diseases_translated": '("Diphtheria", "Tetanus", "Pertussis")', + "phu_data": { + "phu_address": "Tunnel Health, 123 Placeholder Street, Sample City, ON A1A 1A1", + "phu_email": "mainpublichealth#rodenthealth.ca", + "phu_phone": "555-555-5555 ext. 1234", + }, } result = render_notice( diff --git a/tests/unit/test_fr_template.py b/tests/unit/test_fr_template.py index 64aa7c0..4e072f8 100644 --- a/tests/unit/test_fr_template.py +++ b/tests/unit/test_fr_template.py @@ -63,6 +63,11 @@ def test_render_notice_with_valid_context(self) -> None: "received": '(("RRO", "2020-05-15"), ("DPT", "2019-03-15"))', "num_rows": "2", "chart_diseases_translated": '("Diphtérie", "Tétanos", "Coqueluche")', + "phu_data": { + "phu_address": "Tunnel Health, 123 Placeholder Street, Sample City, ON A1A 1A1", + "phu_email": "mainpublichealth#rodenthealth.ca", + "phu_phone": "555-555-5555 ext. 1234", + }, } result = render_notice( @@ -91,6 +96,11 @@ def test_render_notice_missing_client_row_raises_error(self) -> None: "received": "()", "num_rows": "0", "chart_diseases_translated": '("Diphtérie", "Tétanos", "Coqueluche")', + "phu_data": { + "phu_address": "Tunnel Health, 123 Placeholder Street, Sample City, ON A1A 1A1", + "phu_email": "mainpublichealth#rodenthealth.ca", + "phu_phone": "555-555-5555 ext. 1234", + }, } with pytest.raises(KeyError, match="Missing context keys"): @@ -134,6 +144,11 @@ def test_render_notice_substitutes_logo_path(self) -> None: "received": "()", "num_rows": "0", "chart_diseases_translated": '("Diphtérie", "Tétanos", "Coqueluche")', + "phu_data": { + "phu_address": "Tunnel Health, 123 Placeholder Street, Sample City, ON A1A 1A1", + "phu_email": "mainpublichealth#rodenthealth.ca", + "phu_phone": "555-555-5555 ext. 1234", + }, } logo_path = "/custom/logo/path.png" @@ -160,6 +175,11 @@ def test_render_notice_substitutes_signature_path(self) -> None: "received": "()", "num_rows": "0", "chart_diseases_translated": '("Diphtérie", "Tétanos", "Coqueluche")', + "phu_data": { + "phu_address": "Tunnel Health, 123 Placeholder Street, Sample City, ON A1A 1A1", + "phu_email": "mainpublichealth#rodenthealth.ca", + "phu_phone": "555-555-5555 ext. 1234", + }, } signature_path = "/custom/signature.png" @@ -186,6 +206,11 @@ def test_render_notice_includes_template_prefix(self) -> None: "received": "()", "num_rows": "0", "chart_diseases_translated": '("Diphtérie", "Tétanos", "Coqueluche")', + "phu_data": { + "phu_address": "Tunnel Health, 123 Placeholder Street, Sample City, ON A1A 1A1", + "phu_email": "mainpublichealth#rodenthealth.ca", + "phu_phone": "555-555-5555 ext. 1234", + }, } result = render_notice( @@ -212,6 +237,11 @@ def test_render_notice_includes_dynamic_block(self) -> None: "received": "()", "num_rows": "1", "chart_diseases_translated": '("Diphtérie", "Tétanos", "Coqueluche")', + "phu_data": { + "phu_address": "Tunnel Health, 123 Placeholder Street, Sample City, ON A1A 1A1", + "phu_email": "mainpublichealth#rodenthealth.ca", + "phu_phone": "555-555-5555 ext. 1234", + }, } result = render_notice( @@ -240,6 +270,11 @@ def test_render_notice_with_complex_client_data(self) -> None: "received": '(("Rougeole", "2020-05-01"), ("Oreillons", "2020-05-01"))', "num_rows": "5", "chart_diseases_translated": '("Diphtérie", "Tétanos", "Coqueluche")', + "phu_data": { + "phu_address": "Tunnel Health, 123 Placeholder Street, Sample City, ON A1A 1A1", + "phu_email": "mainpublichealth#rodenthealth.ca", + "phu_phone": "555-555-5555 ext. 1234", + }, } result = render_notice( @@ -268,6 +303,11 @@ def test_render_notice_empty_vaccines_handled(self) -> None: "received": "()", "num_rows": "0", "chart_diseases_translated": '("Diphtérie", "Tétanos", "Coqueluche")', + "phu_data": { + "phu_address": "Tunnel Health, 123 Placeholder Street, Sample City, ON A1A 1A1", + "phu_email": "mainpublichealth#rodenthealth.ca", + "phu_phone": "555-555-5555 ext. 1234", + }, } result = render_notice( @@ -295,6 +335,11 @@ def test_render_notice_french_content(self) -> None: "received": "()", "num_rows": "0", "chart_diseases_translated": '("Diphtérie", "Tétanos", "Coqueluche")', + "phu_data": { + "phu_address": "Tunnel Health, 123 Placeholder Street, Sample City, ON A1A 1A1", + "phu_email": "mainpublichealth#rodenthealth.ca", + "phu_phone": "555-555-5555 ext. 1234", + }, } result = render_notice( From 8945b2bc9c7302083e635e08e712038ecd67a86e Mon Sep 17 00:00:00 2001 From: TiaTuinstra Date: Thu, 6 Nov 2025 15:09:34 +0000 Subject: [PATCH 2/4] Update: now no mapping file is expected in config/parameters.yaml. By default, build_template_context function in generate_notices.py will expect and read from config/map_school.json. --- config/parameters.yaml | 1 - pipeline/config_loader.py | 14 ------------ pipeline/generate_notices.py | 39 +++++++++++++-------------------- pipeline/orchestrator.py | 13 ++--------- tests/unit/test_en_template.py | 8 +++++++ tests/unit/test_fr_template.py | 9 ++++++++ tests/unit/test_run_pipeline.py | 1 + 7 files changed, 35 insertions(+), 50 deletions(-) diff --git a/config/parameters.yaml b/config/parameters.yaml index 8581c44..5a76b33 100644 --- a/config/parameters.yaml +++ b/config/parameters.yaml @@ -39,7 +39,6 @@ pipeline: remove_unencrypted_pdfs: false before_run: clear_output_directory: true -map_file: map_school.json qr: enabled: false payload_template: https://www.test-immunization.ca/update?client_id={client_id}&dob={date_of_birth_iso}&lang={language_code} diff --git a/pipeline/config_loader.py b/pipeline/config_loader.py index a697a13..5a1347f 100644 --- a/pipeline/config_loader.py +++ b/pipeline/config_loader.py @@ -81,26 +81,12 @@ def validate_config(config: Dict[str, Any]) -> None: - **PDF Bundling:** If bundle_size > 0, must be positive integer; group_by must be valid enum - **Encryption:** If encryption.enabled=true, requires password.template - **Cleanup:** If delete_unencrypted_pdfs is set, must be boolean - - **Template Arguments:** Make sure map_file is provided and exists **Validation philosophy:** - Infrastructure errors (missing config) raise immediately (fail-fast) - All error messages are clear and actionable - Config is validated once at load time, not per-step """ - # Validate mapping of PHU data to school from config - try: - map_filepath = DEFAULT_CONFIG_PATH.parent / config.get("map_file") - except KeyError as err: - raise ( - f'Attempting to load map_file from config, make sure "map_file" provided: {err}' - ) - - if not map_filepath.exists(): - raise FileNotFoundError( - "Mapping to school-specific info enabled, but expected mapping file not present." - "Please provide mapping file at config/map_school.json." - ) # Validate QR config qr_config = config.get("qr", {}) diff --git a/pipeline/generate_notices.py b/pipeline/generate_notices.py index 11aa772..7d6acbe 100644 --- a/pipeline/generate_notices.py +++ b/pipeline/generate_notices.py @@ -258,7 +258,8 @@ def load_and_translate_chart_diseases(language: str) -> List[str]: def build_template_context( client: ClientRecord, qr_output_dir: Path | None = None, - map_file: Path | None = None, + map_file: Path = ROOT_DIR / "config/map_school.json", + required_keys: Dict = {"phu_address", "phu_phone", "phu_email", "phu_website"}, ) -> Dict[str, str]: """Build template context from client data. @@ -275,6 +276,10 @@ def build_template_context( Directory containing QR code PNG files. map_file: Filepath, optional File containing mapping of schools to specific info (e.g. satellite PHU office info) to populate template. + By default, will use config/map_school.json. + required_keys: Dict, optional + Dictionary containing the keys that should come from the mapping file. + Each of these keys should be present in the "DEFAULT" section of the mapping file. Returns ------- Dict[str, str] @@ -312,7 +317,11 @@ def build_template_context( # Check if mapping file provided if map_file: - required_keys = {"phu_address", "phu_phone", "phu_email", "phu_website"} + # Check if mapping file exists + if not map_file.exists(): + raise FileNotFoundError( + f"Expected school mapping file at {map_file}, but file does not exist. Please provide mapping file at {map_file}." + ) # Load mapping file data with open(map_file, "r") as f: @@ -458,11 +467,10 @@ def render_notice( logo: Path, signature: Path, qr_output_dir: Path | None = None, - map_file: Path | None = None, ) -> str: language = Language.from_string(client.language) renderer = get_language_renderer(language) - context = build_template_context(client, qr_output_dir, map_file) + context = build_template_context(client, qr_output_dir) return renderer( context, logo_path=to_root_relative(logo), @@ -471,11 +479,7 @@ def render_notice( def generate_typst_files( - payload: ArtifactPayload, - output_dir: Path, - logo_path: Path, - signature_path: Path, - map_file: Path | None = None, + payload: ArtifactPayload, output_dir: Path, logo_path: Path, signature_path: Path ) -> List[Path]: output_dir.mkdir(parents=True, exist_ok=True) qr_output_dir = output_dir / "qr_codes" @@ -494,7 +498,6 @@ def generate_typst_files( logo=logo_path, signature=signature_path, qr_output_dir=qr_output_dir, - map_file=map_file, ) filename = f"{language}_notice_{client.sequence}_{client.client_id}.typ" file_path = typst_output_dir / filename @@ -505,11 +508,7 @@ def generate_typst_files( def main( - artifact_path: Path, - output_dir: Path, - logo_path: Path, - signature_path: Path, - map_file: Path | None = None, + artifact_path: Path, output_dir: Path, logo_path: Path, signature_path: Path ) -> List[Path]: """Main entry point for Typst notice generation. @@ -523,8 +522,6 @@ def main( Path to the logo image. signature_path : Path Path to the signature image. - map_file : Path - (Optional) Path to file mapping schools to template details (e.g. PHU satellite offices). Returns ------- @@ -532,13 +529,7 @@ def main( List of generated Typst file paths. """ payload = read_artifact(artifact_path) - generated = generate_typst_files( - payload, - output_dir, - logo_path, - signature_path, - map_file, - ) + generated = generate_typst_files(payload, output_dir, logo_path, signature_path) print( f"Generated {len(generated)} Typst files in {output_dir} for language {payload.language}" ) diff --git a/pipeline/orchestrator.py b/pipeline/orchestrator.py index 45ff94e..a59480a 100755 --- a/pipeline/orchestrator.py +++ b/pipeline/orchestrator.py @@ -250,7 +250,6 @@ def run_step_4_generate_notices( run_id: str, assets_dir: Path, config_dir: Path, - map_file: Path | None = None, ) -> None: """Step 4: Generating Typst templates.""" print_step(4, "Generating Typst templates") @@ -262,7 +261,7 @@ def run_step_4_generate_notices( # Generate Typst files using main function generated = generate_notices.main( - artifact_path, artifacts_dir, logo_path, signature_path, map_file + artifact_path, artifacts_dir, logo_path, signature_path ) print(f"Generated {len(generated)} Typst files in {artifacts_dir}") @@ -462,14 +461,6 @@ def main() -> int: # Extract config settings encryption_enabled = config.get("encryption", {}).get("enabled", False) - map_file = config_dir / config.get("map_file") - - if map_file.exists(): - "School mapping file provided." - else: - print("Expected school mapping file at '{map_file}', but file does not exist.") - return 1 - print_header(args.input_file) total_start = time.time() @@ -515,7 +506,7 @@ def main() -> int: # Step 4: Generating Notices step_start = time.time() run_step_4_generate_notices( - output_dir, run_id, DEFAULT_TEMPLATES_ASSETS_DIR, config_dir, map_file + output_dir, run_id, DEFAULT_TEMPLATES_ASSETS_DIR, config_dir ) step_duration = time.time() - step_start step_times.append(("Template Generation", step_duration)) diff --git a/tests/unit/test_en_template.py b/tests/unit/test_en_template.py index 8611475..bc685a9 100644 --- a/tests/unit/test_en_template.py +++ b/tests/unit/test_en_template.py @@ -49,6 +49,7 @@ def test_render_notice_with_valid_context(self) -> None: "phu_address": "Tunnel Health, 123 Placeholder Street, Sample City, ON A1A 1A1", "phu_email": "mainpublichealth#rodenthealth.ca", "phu_phone": "555-555-5555 ext. 1234", + "phu_website": "https://www.test-immunization.ca", }, } @@ -82,6 +83,7 @@ def test_render_notice_missing_client_row_raises_error(self) -> None: "phu_address": "Tunnel Health, 123 Placeholder Street, Sample City, ON A1A 1A1", "phu_email": "mainpublichealth#rodenthealth.ca", "phu_phone": "555-555-5555 ext. 1234", + "phu_website": "https://www.test-immunization.ca", }, } @@ -130,6 +132,7 @@ def test_render_notice_substitutes_logo_path(self) -> None: "phu_address": "Tunnel Health, 123 Placeholder Street, Sample City, ON A1A 1A1", "phu_email": "mainpublichealth#rodenthealth.ca", "phu_phone": "555-555-5555 ext. 1234", + "phu_website": "https://www.test-immunization.ca", }, } @@ -161,6 +164,7 @@ def test_render_notice_substitutes_signature_path(self) -> None: "phu_address": "Tunnel Health, 123 Placeholder Street, Sample City, ON A1A 1A1", "phu_email": "mainpublichealth#rodenthealth.ca", "phu_phone": "555-555-5555 ext. 1234", + "phu_website": "https://www.test-immunization.ca", }, } @@ -192,6 +196,7 @@ def test_render_notice_includes_template_prefix(self) -> None: "phu_address": "Tunnel Health, 123 Placeholder Street, Sample City, ON A1A 1A1", "phu_email": "mainpublichealth#rodenthealth.ca", "phu_phone": "555-555-5555 ext. 1234", + "phu_website": "https://www.test-immunization.ca", }, } @@ -223,6 +228,7 @@ def test_render_notice_includes_dynamic_block(self) -> None: "phu_address": "Tunnel Health, 123 Placeholder Street, Sample City, ON A1A 1A1", "phu_email": "mainpublichealth#rodenthealth.ca", "phu_phone": "555-555-5555 ext. 1234", + "phu_website": "https://www.test-immunization.ca", }, } @@ -256,6 +262,7 @@ def test_render_notice_with_complex_client_data(self) -> None: "phu_address": "Tunnel Health, 123 Placeholder Street, Sample City, ON A1A 1A1", "phu_email": "mainpublichealth#rodenthealth.ca", "phu_phone": "555-555-5555 ext. 1234", + "phu_website": "https://www.test-immunization.ca", }, } @@ -289,6 +296,7 @@ def test_render_notice_empty_vaccines_handled(self) -> None: "phu_address": "Tunnel Health, 123 Placeholder Street, Sample City, ON A1A 1A1", "phu_email": "mainpublichealth#rodenthealth.ca", "phu_phone": "555-555-5555 ext. 1234", + "phu_website": "https://www.test-immunization.ca", }, } diff --git a/tests/unit/test_fr_template.py b/tests/unit/test_fr_template.py index 4e072f8..5234f0d 100644 --- a/tests/unit/test_fr_template.py +++ b/tests/unit/test_fr_template.py @@ -67,6 +67,7 @@ def test_render_notice_with_valid_context(self) -> None: "phu_address": "Tunnel Health, 123 Placeholder Street, Sample City, ON A1A 1A1", "phu_email": "mainpublichealth#rodenthealth.ca", "phu_phone": "555-555-5555 ext. 1234", + "phu_website": "https://www.test-immunization.ca", }, } @@ -100,6 +101,7 @@ def test_render_notice_missing_client_row_raises_error(self) -> None: "phu_address": "Tunnel Health, 123 Placeholder Street, Sample City, ON A1A 1A1", "phu_email": "mainpublichealth#rodenthealth.ca", "phu_phone": "555-555-5555 ext. 1234", + "phu_website": "https://www.test-immunization.ca", }, } @@ -148,6 +150,7 @@ def test_render_notice_substitutes_logo_path(self) -> None: "phu_address": "Tunnel Health, 123 Placeholder Street, Sample City, ON A1A 1A1", "phu_email": "mainpublichealth#rodenthealth.ca", "phu_phone": "555-555-5555 ext. 1234", + "phu_website": "https://www.test-immunization.ca", }, } @@ -179,6 +182,7 @@ def test_render_notice_substitutes_signature_path(self) -> None: "phu_address": "Tunnel Health, 123 Placeholder Street, Sample City, ON A1A 1A1", "phu_email": "mainpublichealth#rodenthealth.ca", "phu_phone": "555-555-5555 ext. 1234", + "phu_website": "https://www.test-immunization.ca", }, } @@ -210,6 +214,7 @@ def test_render_notice_includes_template_prefix(self) -> None: "phu_address": "Tunnel Health, 123 Placeholder Street, Sample City, ON A1A 1A1", "phu_email": "mainpublichealth#rodenthealth.ca", "phu_phone": "555-555-5555 ext. 1234", + "phu_website": "https://www.test-immunization.ca", }, } @@ -241,6 +246,7 @@ def test_render_notice_includes_dynamic_block(self) -> None: "phu_address": "Tunnel Health, 123 Placeholder Street, Sample City, ON A1A 1A1", "phu_email": "mainpublichealth#rodenthealth.ca", "phu_phone": "555-555-5555 ext. 1234", + "phu_website": "https://www.test-immunization.ca", }, } @@ -274,6 +280,7 @@ def test_render_notice_with_complex_client_data(self) -> None: "phu_address": "Tunnel Health, 123 Placeholder Street, Sample City, ON A1A 1A1", "phu_email": "mainpublichealth#rodenthealth.ca", "phu_phone": "555-555-5555 ext. 1234", + "phu_website": "https://www.test-immunization.ca", }, } @@ -307,6 +314,7 @@ def test_render_notice_empty_vaccines_handled(self) -> None: "phu_address": "Tunnel Health, 123 Placeholder Street, Sample City, ON A1A 1A1", "phu_email": "mainpublichealth#rodenthealth.ca", "phu_phone": "555-555-5555 ext. 1234", + "phu_website": "https://www.test-immunization.ca", }, } @@ -339,6 +347,7 @@ def test_render_notice_french_content(self) -> None: "phu_address": "Tunnel Health, 123 Placeholder Street, Sample City, ON A1A 1A1", "phu_email": "mainpublichealth#rodenthealth.ca", "phu_phone": "555-555-5555 ext. 1234", + "phu_website": "https://www.test-immunization.ca", }, } diff --git a/tests/unit/test_run_pipeline.py b/tests/unit/test_run_pipeline.py index 9e4ab6b..75b6044 100644 --- a/tests/unit/test_run_pipeline.py +++ b/tests/unit/test_run_pipeline.py @@ -311,6 +311,7 @@ def test_pipeline_loads_parameters_yaml(self, config_file: Path) -> None: mock_load.return_value = { "pipeline": {"auto_remove_output": False}, "qr": {"enabled": True}, + "map_file": "map_school.json", } from pipeline.config_loader import load_config From e3b570494581903ae32f1e34bc6e1b929de71d49 Mon Sep 17 00:00:00 2001 From: TiaTuinstra Date: Thu, 6 Nov 2025 16:23:05 +0000 Subject: [PATCH 3/4] added testing for build_template_context (increase codecoverage score) --- tests/unit/test_generate_notices.py | 54 +++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/tests/unit/test_generate_notices.py b/tests/unit/test_generate_notices.py index c3a2a3f..51dced8 100644 --- a/tests/unit/test_generate_notices.py +++ b/tests/unit/test_generate_notices.py @@ -293,6 +293,7 @@ def test_build_template_context_from_client(self) -> None: assert "vaccines_due_array" in context assert "received" in context assert "num_rows" in context + assert "phu_data" in context def test_build_template_context_includes_client_id(self) -> None: """Verify client_id is in context. @@ -372,6 +373,59 @@ def test_build_template_context_includes_formatted_date(self) -> None: or "date_data_cutoff" in client_data_str ) + def test_build_template_context_map_file(self, tmp_test_dir: Path) -> None: + """Verify handling of map file. + + Real-world significance: + - Map file must be present with expected values + - Must avoid missing or mixing up template values + """ + # Test + map_path = tmp_test_dir / "map.json" + map_path.write_text("not valid json {{{") + + client = sample_input.create_test_client_record( + school_name="Test School", + ) + + with pytest.raises(Exception): # json.JSONDecodeError or similar + generate_notices.build_template_context(client, map_file=map_path) + + # Missing "DEFAULT" in .json file + test_json = {"SCHOOL_1": {}} + json_string = json.dumps(test_json) + map_path.write_text(json_string) + with pytest.raises(Exception): # json.JSONDecodeError or similar + generate_notices.build_template_context(client, map_file=map_path) + + # Missing required keys in "DEFAULT" + required_keys = {"phu_number"} + test_json = {"DEFAULT": {}} + json_string = json.dumps(test_json) + map_path.write_text(json_string) + with pytest.raises(Exception): # json.JSONDecodeError or similar + generate_notices.build_template_context( + client, map_file=map_path, required_keys=required_keys + ) + + # Check that finds school and substitutes provided value, and uses defaults for keys not provided for school + required_keys = {"phu_phone", "phu_email"} + test_json = { + "DEFAULT": { + "phu_phone": "555-555-5555 ext. 1234", + "phu_email": "mainpublichealth@rodenthealth.ca", + }, + "TEST_SCHOOL": {"phu_phone": "555-555-5555 ext. 4321"}, + } + json_string = json.dumps(test_json) + map_path.write_text(json_string) + context = generate_notices.build_template_context( + client, map_file=map_path, required_keys=required_keys + ) + assert context["phu_data"] + assert context["phu_data"]["phu_email"] == "mainpublichealth@rodenthealth.ca" + assert context["phu_data"]["phu_phone"] == "555-555-5555 ext. 4321" + @pytest.mark.unit class TestLanguageSupport: From 8906fb8725fc7920a83f6cf9a1a10143e34dcb9b Mon Sep 17 00:00:00 2001 From: TiaTuinstra Date: Thu, 6 Nov 2025 16:42:18 +0000 Subject: [PATCH 4/4] more tests for codecov --- pipeline/generate_notices.py | 103 +++++++++++++--------------- tests/unit/test_generate_notices.py | 22 ++++-- 2 files changed, 62 insertions(+), 63 deletions(-) diff --git a/pipeline/generate_notices.py b/pipeline/generate_notices.py index 7d6acbe..70e402c 100644 --- a/pipeline/generate_notices.py +++ b/pipeline/generate_notices.py @@ -315,69 +315,58 @@ def build_template_context( if qr_path.exists(): client_data["qr_code"] = to_root_relative(qr_path) - # Check if mapping file provided - if map_file: - # Check if mapping file exists - if not map_file.exists(): - raise FileNotFoundError( - f"Expected school mapping file at {map_file}, but file does not exist. Please provide mapping file at {map_file}." - ) - - # Load mapping file data - with open(map_file, "r") as f: - map_data = json.load(f) + # Check if mapping file exists + if not map_file.exists(): + raise FileNotFoundError( + f"Expected school mapping file at {map_file}, but file does not exist. Please provide mapping file at {map_file}." + ) - # Attempt to load default PHU data values from mapping file; this should also contain all required keys - try: - phu_data = map_data["DEFAULT"] - except KeyError as err: - raise ( - f"Loading default PHU info, error when attempting to access 'DEFAULT' from {map_file}: {err}" - ) + # Load mapping file data + with open(map_file, "r") as f: + map_data = json.load(f) - if not phu_data: - raise ValueError( - "Default values for PHU info not provided. " - f'Please define DEFAULT values {required_keys} in under "DEFAULT" key in {map_file}.' - ) - else: - missing = [key for key in required_keys if key not in phu_data] - if missing: - missing_keys = ", ".join(missing) - raise KeyError(f"Missing phu_data keys in config: {missing_keys}") - - # Clean school name - client_school_key = re.sub(r"\s+", "_", client_data["school"]).upper() - - # Check if school has a mapping associated with it - otherwise, use default config values - if client_school_key in map_data.keys(): - if client_school_key != "DEFAULT": - LOG.info( - f"School-specific information provided for: {client_school_key}" - ) - - map_client_data = map_data[client_school_key] - - # Replace default values with values in map file. If any are missing, keep default values. - for key in phu_data.keys(): - if key in map_client_data.keys(): - phu_data[key] = map_client_data[key] - else: - LOG.info( - f"Mapping file for school {client_school_key} missing {key}. Using default value." - ) + # Attempt to load default PHU data values from mapping file; this should also contain all required keys + try: + phu_data = map_data["DEFAULT"] + except KeyError as err: + raise ( + f"Loading default PHU info, error when attempting to access 'DEFAULT' from {map_file}: {err}" + ) - else: - LOG.info( - f"School {client_school_key} not in mapping file. Using default values." - ) + if not phu_data: + raise ValueError( + "Default values for PHU info not provided. " + f'Please define DEFAULT values {required_keys} in under "DEFAULT" key in {map_file}.' + ) + else: + missing = [key for key in required_keys if key not in phu_data] + if missing: + missing_keys = ", ".join(missing) + raise KeyError(f"Missing phu_data keys in config: {missing_keys}") + + # Clean school name + client_school_key = re.sub(r"\s+", "_", client_data["school"]).upper() + + # Check if school has a mapping associated with it - otherwise, use default config values + if client_school_key in map_data.keys(): + if client_school_key != "DEFAULT": + LOG.info(f"School-specific information provided for: {client_school_key}") + + map_client_data = map_data[client_school_key] + + # Replace default values with values in map file. If any are missing, keep default values. + for key in phu_data.keys(): + if key in map_client_data.keys(): + phu_data[key] = map_client_data[key] + else: + LOG.info( + f"Mapping file for school {client_school_key} missing {key}. Using default value." + ) else: - print( - "Missing mapping filename. Please provide within config/parameters.yaml." - "File should reside within config directory." + LOG.info( + f"School {client_school_key} not in mapping file. Using default values." ) - return 1 # Load and translate chart disease header chart_diseases_translated = load_and_translate_chart_diseases(client.language) diff --git a/tests/unit/test_generate_notices.py b/tests/unit/test_generate_notices.py index 51dced8..98e248d 100644 --- a/tests/unit/test_generate_notices.py +++ b/tests/unit/test_generate_notices.py @@ -388,6 +388,13 @@ def test_build_template_context_map_file(self, tmp_test_dir: Path) -> None: school_name="Test School", ) + # Map file does not exist + with pytest.raises(Exception): + generate_notices.build_template_context( + client, map_file=tmp_test_dir / "fake_map.json" + ) + + # Map file exists, but can't read json with pytest.raises(Exception): # json.JSONDecodeError or similar generate_notices.build_template_context(client, map_file=map_path) @@ -395,21 +402,24 @@ def test_build_template_context_map_file(self, tmp_test_dir: Path) -> None: test_json = {"SCHOOL_1": {}} json_string = json.dumps(test_json) map_path.write_text(json_string) - with pytest.raises(Exception): # json.JSONDecodeError or similar + with pytest.raises(Exception): generate_notices.build_template_context(client, map_file=map_path) # Missing required keys in "DEFAULT" - required_keys = {"phu_number"} - test_json = {"DEFAULT": {}} + required_keys = {"phu_phone", "phu_email"} + test_json = { + "DEFAULT": { + "phu_phone": "555-555-5555 ext. 1234", + } + } json_string = json.dumps(test_json) map_path.write_text(json_string) - with pytest.raises(Exception): # json.JSONDecodeError or similar + with pytest.raises(Exception): generate_notices.build_template_context( client, map_file=map_path, required_keys=required_keys ) - # Check that finds school and substitutes provided value, and uses defaults for keys not provided for school - required_keys = {"phu_phone", "phu_email"} + # Check that finds school name and substitutes provided value and uses default values for keys not provided for school name test_json = { "DEFAULT": { "phu_phone": "555-555-5555 ext. 1234",