Compare commits

..

11 Commits

20 changed files with 132 additions and 37 deletions

1
.gitignore vendored
View File

@@ -203,3 +203,4 @@ nul
*.csv *.csv
/*.xlsx /*.xlsx
!eb_org_center_mapping.xlsx !eb_org_center_mapping.xlsx
/pyproject.toml

View File

@@ -86,6 +86,10 @@ from eb_dashboard_utils import (
clear_httpx_client, clear_httpx_client,
get_thread_position, get_thread_position,
get_config_path, get_config_path,
set_dashboard_config_path_override,
get_dashboard_config_path,
set_output_file_suffix,
get_output_filename,
thread_local_storage, thread_local_storage,
run_with_context run_with_context
) )
@@ -103,8 +107,23 @@ from eb_dashboard_excel_export import (
set_dependencies as excel_set_dependencies set_dependencies as excel_set_dependencies
) )
logging.basicConfig(level=logging.WARNING, format='%(asctime)s - %(levelname)s - %(message)s', filename=LOG_FILE_NAME, def _fatal_cli_error(message):
filemode='w') """Print a CLI error then wait for Enter before exiting (keeps console open on Windows Explorer launch)."""
print(message)
input("Press Enter to exit...")
sys.exit(1)
# Handle --add-suffix <value> early: must precede logging.basicConfig (affects log filename)
if "--add-suffix" in sys.argv:
_idx = sys.argv.index("--add-suffix")
if _idx + 1 >= len(sys.argv):
_fatal_cli_error("Error: --add-suffix requires a value")
set_output_file_suffix(sys.argv[_idx + 1])
del sys.argv[_idx:_idx + 2]
logging.basicConfig(level=logging.WARNING, format='%(asctime)s - %(levelname)s - %(message)s',
filename=get_output_filename(LOG_FILE_NAME), filemode='w')
# ============================================================================ # ============================================================================
@@ -130,7 +149,7 @@ _user_interaction_lock = threading.Lock()
# Global variables (mutable, set at runtime - not constants) # Global variables (mutable, set at runtime - not constants)
inclusions_mapping_config = [] inclusions_mapping_config = []
organizations_mapping_config = [] organizations_mapping_config = []
_dashboard_config_path_override = None # Set by --config CLI arg if provided _include_drafts = False # Set by --include-drafts CLI arg if provided
excel_export_config = None excel_export_config = None
excel_export_enabled = False excel_export_enabled = False
@@ -157,14 +176,18 @@ if "--debug" in sys.argv:
if "--config" in sys.argv: if "--config" in sys.argv:
_idx = sys.argv.index("--config") _idx = sys.argv.index("--config")
if _idx + 1 >= len(sys.argv): if _idx + 1 >= len(sys.argv):
print("Error: --config requires a file path argument") _fatal_cli_error("Error: --config requires a file path argument")
sys.exit(1)
_raw_config_path = sys.argv[_idx + 1] _raw_config_path = sys.argv[_idx + 1]
del sys.argv[_idx:_idx + 2] del sys.argv[_idx:_idx + 2]
if os.path.isabs(_raw_config_path): if os.path.isabs(_raw_config_path):
_dashboard_config_path_override = _raw_config_path set_dashboard_config_path_override(_raw_config_path)
else: else:
_dashboard_config_path_override = os.path.join(get_config_path(), _raw_config_path) set_dashboard_config_path_override(os.path.join(get_config_path(), _raw_config_path))
# Handle --include-drafts flag (remove from sys.argv to preserve positional args)
_include_drafts = "--include-drafts" in sys.argv
if _include_drafts:
sys.argv.remove("--include-drafts")
# --- Progress Bar Configuration --- # --- Progress Bar Configuration ---
# NOTE: BAR_N_FMT_WIDTH, BAR_TOTAL_FMT_WIDTH, BAR_TIME_WIDTH, BAR_RATE_WIDTH # NOTE: BAR_N_FMT_WIDTH, BAR_TOTAL_FMT_WIDTH, BAR_TIME_WIDTH, BAR_RATE_WIDTH
@@ -471,7 +494,7 @@ def load_json_file(filename):
def load_inclusions_mapping_config(): def load_inclusions_mapping_config():
"""Loads and validates the inclusions mapping configuration from the Excel file.""" """Loads and validates the inclusions mapping configuration from the Excel file."""
global inclusions_mapping_config global inclusions_mapping_config
config_path = _dashboard_config_path_override or os.path.join(get_config_path(), DASHBOARD_CONFIG_FILE_NAME) config_path = get_dashboard_config_path(DASHBOARD_CONFIG_FILE_NAME)
try: try:
# Load with data_only=True to read calculated values instead of formulas # Load with data_only=True to read calculated values instead of formulas
@@ -573,7 +596,7 @@ def load_inclusions_mapping_config():
def load_organizations_mapping_config(): def load_organizations_mapping_config():
"""Loads and validates the organizations mapping configuration from the Excel file.""" """Loads and validates the organizations mapping configuration from the Excel file."""
global organizations_mapping_config global organizations_mapping_config
config_path = _dashboard_config_path_override or os.path.join(get_config_path(), DASHBOARD_CONFIG_FILE_NAME) config_path = get_dashboard_config_path(DASHBOARD_CONFIG_FILE_NAME)
try: try:
# Load with data_only=True to read calculated values instead of formulas # Load with data_only=True to read calculated values instead of formulas
@@ -1232,14 +1255,14 @@ def get_all_questionnaires_by_patient(patient_id, record_data):
q_category = get_nested_value(item, path=["questionnaire", "category"]) q_category = get_nested_value(item, path=["questionnaire", "category"])
answers = get_nested_value(item, path=["answers"], default={}) answers = get_nested_value(item, path=["answers"], default={})
# ── UNLINKED PATIENT FILTER ───────────────────────────────────────────── # ── DRAFT ANSWERS FILTER ────────────────────────────────────────────────
# If answers.patient is missing, null, or does not match the requested # "Draft" answers have a missing or mismatched subject (patient id):
# patient_id, the questionnaire is considered unlinked → clear answers # they are not yet submitted and should not be treated as valid patient data.
# to prevent use of orphaned data. # Active by default; use --include-drafts CLI flag to disable.
# (disable: comment out the block below) if not _include_drafts:
answers_patient = answers.get("subject") if isinstance(answers, dict) else None answers_patient = answers.get("subject") if isinstance(answers, dict) else None
if not answers_patient or answers_patient != patient_id: if not answers_patient or answers_patient != patient_id:
answers = {} answers = {}
# ──────────────────────────────────────────────────────────────────────── # ────────────────────────────────────────────────────────────────────────
if q_id: if q_id:
@@ -1496,7 +1519,7 @@ def main():
load_organizations_mapping_config() load_organizations_mapping_config()
# Completely externalized Excel-only workflow # Completely externalized Excel-only workflow
export_excel_only(sys.argv, INCLUSIONS_FILE_NAME, ORGANIZATIONS_FILE_NAME, export_excel_only(sys.argv, get_output_filename(INCLUSIONS_FILE_NAME), get_output_filename(ORGANIZATIONS_FILE_NAME),
inclusions_mapping_config, organizations_mapping_config) inclusions_mapping_config, organizations_mapping_config)
return return
@@ -1641,7 +1664,7 @@ def main():
has_coherence_critical, has_regression_critical = run_quality_checks( has_coherence_critical, has_regression_critical = run_quality_checks(
current_inclusions=output_inclusions, # list: données en mémoire (nouvellement collectées) current_inclusions=output_inclusions, # list: données en mémoire (nouvellement collectées)
organizations_list=organizations_list, # list: données en mémoire avec compteurs organizations_list=organizations_list, # list: données en mémoire avec compteurs
old_inclusions_filename=INCLUSIONS_FILE_NAME # str: "endobest_inclusions.json" (version courante sur disque) old_inclusions_filename=get_output_filename(INCLUSIONS_FILE_NAME) # str: current file on disk (with suffix if any)
) )
# === CHECK FOR CRITICAL ISSUES AND ASK USER CONFIRMATION === # === CHECK FOR CRITICAL ISSUES AND ASK USER CONFIRMATION ===
@@ -1666,9 +1689,9 @@ def main():
# === WRITE NEW FILES === # === WRITE NEW FILES ===
print("Writing files...") print("Writing files...")
with open(INCLUSIONS_FILE_NAME, 'w', encoding='utf-8') as f_json: with open(get_output_filename(INCLUSIONS_FILE_NAME), 'w', encoding='utf-8') as f_json:
json.dump(output_inclusions, f_json, indent=4, ensure_ascii=False) json.dump(output_inclusions, f_json, indent=4, ensure_ascii=False)
with open(ORGANIZATIONS_FILE_NAME, 'w', encoding='utf-8') as f_json: with open(get_output_filename(ORGANIZATIONS_FILE_NAME), 'w', encoding='utf-8') as f_json:
json.dump(organizations_list, f_json, indent=4, ensure_ascii=False) json.dump(organizations_list, f_json, indent=4, ensure_ascii=False)
console.print("[green]✓ Data saved to JSON files[/green]") console.print("[green]✓ Data saved to JSON files[/green]")

View File

@@ -0,0 +1,2 @@
@echo off
eb_dashboard.exe --config "Endobest_Dashboard_Config - Debug.xlsx" --add-suffix %*

View File

@@ -0,0 +1,3 @@
@echo off
call C:\PythonProjects\.rcvenv\Scripts\activate.bat
python eb_dashboard.py --config "Endobest_Dashboard_Config - Debug.xlsx" --add-suffix %*

View File

@@ -34,7 +34,7 @@ try:
except ImportError: except ImportError:
xw = None xw = None
from eb_dashboard_utils import get_nested_value, get_config_path from eb_dashboard_utils import get_nested_value, get_config_path, get_dashboard_config_path, get_output_filename
from eb_dashboard_constants import ( from eb_dashboard_constants import (
INCLUSIONS_FILE_NAME, INCLUSIONS_FILE_NAME,
ORGANIZATIONS_FILE_NAME, ORGANIZATIONS_FILE_NAME,
@@ -117,7 +117,7 @@ def load_excel_export_config(console_instance=None):
if console_instance: if console_instance:
console = console_instance console = console_instance
config_path = os.path.join(get_config_path(), DASHBOARD_CONFIG_FILE_NAME) config_path = get_dashboard_config_path(DASHBOARD_CONFIG_FILE_NAME)
error_messages = [] error_messages = []
try: try:
@@ -399,7 +399,7 @@ def export_to_excel(inclusions_data, organizations_data, excel_config,
output_template = workbook_config.get("output_file_name_template") output_template = workbook_config.get("output_file_name_template")
if_output_exists = workbook_config.get("if_output_exists", OUTPUT_ACTION_OVERWRITE) if_output_exists = workbook_config.get("if_output_exists", OUTPUT_ACTION_OVERWRITE)
# Resolve output filename # Resolve output filename, then apply --add-suffix if provided
try: try:
output_filename = output_template.format(**template_vars) output_filename = output_template.format(**template_vars)
except KeyError as e: except KeyError as e:
@@ -407,6 +407,7 @@ def export_to_excel(inclusions_data, organizations_data, excel_config,
error_count += 1 error_count += 1
continue continue
output_filename = get_output_filename(output_filename)
output_path = os.path.join(EXCEL_OUTPUT_FOLDER, output_filename) output_path = os.path.join(EXCEL_OUTPUT_FOLDER, output_filename)
# Log workbook processing start # Log workbook processing start
@@ -556,7 +557,7 @@ def _prepare_template_variables():
""" """
# Get UTC timestamp from inclusions file # Get UTC timestamp from inclusions file
# Use constant from eb_dashboard_constants (SINGLE SOURCE OF TRUTH) # Use constant from eb_dashboard_constants (SINGLE SOURCE OF TRUTH)
inclusions_file = INCLUSIONS_FILE_NAME inclusions_file = get_output_filename(INCLUSIONS_FILE_NAME)
if os.path.exists(inclusions_file): if os.path.exists(inclusions_file):
file_mtime = os.path.getmtime(inclusions_file) file_mtime = os.path.getmtime(inclusions_file)
extract_date_time_utc = datetime.fromtimestamp(file_mtime, tz=timezone.utc) extract_date_time_utc = datetime.fromtimestamp(file_mtime, tz=timezone.utc)
@@ -1921,9 +1922,9 @@ def export_excel_only(sys_argv,
global console global console
if not inclusions_filename: if not inclusions_filename:
inclusions_filename = INCLUSIONS_FILE_NAME inclusions_filename = get_output_filename(INCLUSIONS_FILE_NAME)
if not organizations_filename: if not organizations_filename:
organizations_filename = ORGANIZATIONS_FILE_NAME organizations_filename = get_output_filename(ORGANIZATIONS_FILE_NAME)
print() print()
console.print("[bold cyan]═══ EXCEL ONLY MODE ═══[/bold cyan]\n") console.print("[bold cyan]═══ EXCEL ONLY MODE ═══[/bold cyan]\n")
@@ -2038,8 +2039,8 @@ def run_normal_mode_export(excel_enabled, excel_config,
try: try:
# Load JSONs from filesystem to ensure data consistency with what was written # Load JSONs from filesystem to ensure data consistency with what was written
# Use constants imported from eb_dashboard_constants.py (SINGLE SOURCE OF TRUTH) # Use constants imported from eb_dashboard_constants.py (SINGLE SOURCE OF TRUTH)
inclusions_from_fs = _load_json_file_internal(INCLUSIONS_FILE_NAME) inclusions_from_fs = _load_json_file_internal(get_output_filename(INCLUSIONS_FILE_NAME))
organizations_from_fs = _load_json_file_internal(ORGANIZATIONS_FILE_NAME) organizations_from_fs = _load_json_file_internal(get_output_filename(ORGANIZATIONS_FILE_NAME))
if inclusions_from_fs is None or organizations_from_fs is None: if inclusions_from_fs is None or organizations_from_fs is None:
error_msg = "Could not load data files for Excel export" error_msg = "Could not load data files for Excel export"

View File

@@ -0,0 +1,3 @@
@echo off
eb_dashboard.exe --excel-only --config "Endobest_Dashboard_Config - Debug.xlsx" --add-suffix debug %*

View File

@@ -0,0 +1,4 @@
@echo off
call C:\PythonProjects\.rcvenv\Scripts\activate.bat
python eb_dashboard.py --excel-only --config "Endobest_Dashboard_Config - Debug.xlsx" --add-suffix debug %*

View File

@@ -17,7 +17,7 @@ import shutil
import openpyxl import openpyxl
from rich.console import Console from rich.console import Console
from eb_dashboard_utils import get_nested_value, get_old_filename as _get_old_filename, get_config_path from eb_dashboard_utils import get_nested_value, get_old_filename as _get_old_filename, get_config_path, get_dashboard_config_path, get_output_filename
from eb_dashboard_constants import ( from eb_dashboard_constants import (
INCLUSIONS_FILE_NAME, INCLUSIONS_FILE_NAME,
ORGANIZATIONS_FILE_NAME, ORGANIZATIONS_FILE_NAME,
@@ -93,7 +93,7 @@ def load_regression_check_config(console_instance=None):
if console_instance: if console_instance:
console = console_instance console = console_instance
config_path = os.path.join(get_config_path(), DASHBOARD_CONFIG_FILE_NAME) config_path = get_dashboard_config_path(DASHBOARD_CONFIG_FILE_NAME)
try: try:
workbook = openpyxl.load_workbook(config_path) workbook = openpyxl.load_workbook(config_path)
@@ -318,10 +318,10 @@ def run_check_only_mode(sys_argv):
# Run quality checks (will load all files internally) # Run quality checks (will load all files internally)
print() print()
old_inclusions_file = _get_old_filename(INCLUSIONS_FILE_NAME, OLD_FILE_SUFFIX) old_inclusions_file = _get_old_filename(get_output_filename(INCLUSIONS_FILE_NAME), OLD_FILE_SUFFIX)
has_coherence_critical, has_regression_critical = run_quality_checks( has_coherence_critical, has_regression_critical = run_quality_checks(
current_inclusions=INCLUSIONS_FILE_NAME, current_inclusions=get_output_filename(INCLUSIONS_FILE_NAME),
organizations_list=ORGANIZATIONS_FILE_NAME, organizations_list=get_output_filename(ORGANIZATIONS_FILE_NAME),
old_inclusions_filename=old_inclusions_file old_inclusions_filename=old_inclusions_file
) )
@@ -370,8 +370,8 @@ def backup_output_files():
except Exception as e: except Exception as e:
logging.warning(f"Could not backup {source}: {e}") logging.warning(f"Could not backup {source}: {e}")
_backup_file_silent(INCLUSIONS_FILE_NAME, _get_old_filename(INCLUSIONS_FILE_NAME, OLD_FILE_SUFFIX)) _backup_file_silent(get_output_filename(INCLUSIONS_FILE_NAME), _get_old_filename(get_output_filename(INCLUSIONS_FILE_NAME), OLD_FILE_SUFFIX))
_backup_file_silent(ORGANIZATIONS_FILE_NAME, _get_old_filename(ORGANIZATIONS_FILE_NAME, OLD_FILE_SUFFIX)) _backup_file_silent(get_output_filename(ORGANIZATIONS_FILE_NAME), _get_old_filename(get_output_filename(ORGANIZATIONS_FILE_NAME), OLD_FILE_SUFFIX))
# ============================================================================ # ============================================================================

View File

@@ -204,6 +204,43 @@ def get_config_path():
return CONFIG_FOLDER_NAME return CONFIG_FOLDER_NAME
_dashboard_config_path_override = None
def set_dashboard_config_path_override(path):
"""Sets a global override for the dashboard config file path (used by --config CLI arg)."""
global _dashboard_config_path_override
_dashboard_config_path_override = path
def get_dashboard_config_path(config_file_name):
"""Returns the dashboard config file path, respecting any --config CLI override."""
if _dashboard_config_path_override:
return _dashboard_config_path_override
return os.path.join(get_config_path(), config_file_name)
_output_file_suffix = None
def set_output_file_suffix(suffix):
"""Sets the global output file suffix (used by --add-suffix CLI arg)."""
global _output_file_suffix
_output_file_suffix = suffix
def get_output_filename(base_name):
"""Returns the output filename with the suffix inserted before the extension.
Example: get_output_filename("endobest_inclusions.json") with suffix "A"
"endobest_inclusions_A.json"
"""
if not _output_file_suffix:
return base_name
name, ext = os.path.splitext(base_name)
return f"{name}_{_output_file_suffix}{ext}"
def get_old_filename(current_filename, old_suffix="_old"): def get_old_filename(current_filename, old_suffix="_old"):
"""Generate old backup filename from current filename. """Generate old backup filename from current filename.

View File

@@ -0,0 +1,3 @@
@echo off
eb_dashboard.exe --include-drafts --add-suffix with_drafts %*

View File

@@ -0,0 +1,4 @@
@echo off
call C:\PythonProjects\.rcvenv\Scripts\activate.bat
python eb_dashboard.py --include-drafts --add-suffix with_drafts %*

View File

@@ -0,0 +1,3 @@
@echo off
eb_dashboard.exe --include-drafts --excel-only --add-suffix with_drafts %*

View File

@@ -0,0 +1,4 @@
@echo off
call C:\PythonProjects\.rcvenv\Scripts\activate.bat
python eb_dashboard.py --include-drafts --excel-only --add-suffix with_drafts %*

View File

@@ -0,0 +1,3 @@
@echo off
eb_dashboard.exe --include-drafts --excel-only --config "Endobest_Dashboard_Config - Debug.xlsx" --add-suffix with_drafts_debug %*

View File

@@ -0,0 +1,4 @@
@echo off
call C:\PythonProjects\.rcvenv\Scripts\activate.bat
python eb_dashboard.py --include-drafts --excel-only --config "Endobest_Dashboard_Config - Debug.xlsx" --add-suffix with_drafts_debug %*

Binary file not shown.