Compare commits

..

11 Commits

20 changed files with 132 additions and 37 deletions

3
.gitignore vendored
View File

@@ -202,4 +202,5 @@ nul
*.txt
*.csv
/*.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,
get_thread_position,
get_config_path,
set_dashboard_config_path_override,
get_dashboard_config_path,
set_output_file_suffix,
get_output_filename,
thread_local_storage,
run_with_context
)
@@ -103,8 +107,23 @@ from eb_dashboard_excel_export import (
set_dependencies as excel_set_dependencies
)
logging.basicConfig(level=logging.WARNING, format='%(asctime)s - %(levelname)s - %(message)s', filename=LOG_FILE_NAME,
filemode='w')
def _fatal_cli_error(message):
"""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)
inclusions_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_enabled = False
@@ -157,14 +176,18 @@ if "--debug" in sys.argv:
if "--config" in sys.argv:
_idx = sys.argv.index("--config")
if _idx + 1 >= len(sys.argv):
print("Error: --config requires a file path argument")
sys.exit(1)
_fatal_cli_error("Error: --config requires a file path argument")
_raw_config_path = sys.argv[_idx + 1]
del sys.argv[_idx:_idx + 2]
if os.path.isabs(_raw_config_path):
_dashboard_config_path_override = _raw_config_path
set_dashboard_config_path_override(_raw_config_path)
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 ---
# 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():
"""Loads and validates the inclusions mapping configuration from the Excel file."""
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:
# 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():
"""Loads and validates the organizations mapping configuration from the Excel file."""
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:
# 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"])
answers = get_nested_value(item, path=["answers"], default={})
# ── UNLINKED PATIENT FILTER ─────────────────────────────────────────────
# If answers.patient is missing, null, or does not match the requested
# patient_id, the questionnaire is considered unlinked → clear answers
# to prevent use of orphaned data.
# (disable: comment out the block below)
answers_patient = answers.get("subject") if isinstance(answers, dict) else None
if not answers_patient or answers_patient != patient_id:
answers = {}
# ── DRAFT ANSWERS FILTER ────────────────────────────────────────────────
# "Draft" answers have a missing or mismatched subject (patient id):
# they are not yet submitted and should not be treated as valid patient data.
# Active by default; use --include-drafts CLI flag to disable.
if not _include_drafts:
answers_patient = answers.get("subject") if isinstance(answers, dict) else None
if not answers_patient or answers_patient != patient_id:
answers = {}
# ────────────────────────────────────────────────────────────────────────
if q_id:
@@ -1496,7 +1519,7 @@ def main():
load_organizations_mapping_config()
# 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)
return
@@ -1641,7 +1664,7 @@ def main():
has_coherence_critical, has_regression_critical = run_quality_checks(
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
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 ===
@@ -1666,9 +1689,9 @@ def main():
# === WRITE NEW 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)
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)
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:
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 (
INCLUSIONS_FILE_NAME,
ORGANIZATIONS_FILE_NAME,
@@ -117,7 +117,7 @@ def load_excel_export_config(console_instance=None):
if 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 = []
try:
@@ -399,7 +399,7 @@ def export_to_excel(inclusions_data, organizations_data, excel_config,
output_template = workbook_config.get("output_file_name_template")
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:
output_filename = output_template.format(**template_vars)
except KeyError as e:
@@ -407,6 +407,7 @@ def export_to_excel(inclusions_data, organizations_data, excel_config,
error_count += 1
continue
output_filename = get_output_filename(output_filename)
output_path = os.path.join(EXCEL_OUTPUT_FOLDER, output_filename)
# Log workbook processing start
@@ -556,7 +557,7 @@ def _prepare_template_variables():
"""
# Get UTC timestamp from inclusions file
# 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):
file_mtime = os.path.getmtime(inclusions_file)
extract_date_time_utc = datetime.fromtimestamp(file_mtime, tz=timezone.utc)
@@ -1921,9 +1922,9 @@ def export_excel_only(sys_argv,
global console
if not inclusions_filename:
inclusions_filename = INCLUSIONS_FILE_NAME
inclusions_filename = get_output_filename(INCLUSIONS_FILE_NAME)
if not organizations_filename:
organizations_filename = ORGANIZATIONS_FILE_NAME
organizations_filename = get_output_filename(ORGANIZATIONS_FILE_NAME)
print()
console.print("[bold cyan]═══ EXCEL ONLY MODE ═══[/bold cyan]\n")
@@ -2038,8 +2039,8 @@ def run_normal_mode_export(excel_enabled, excel_config,
try:
# Load JSONs from filesystem to ensure data consistency with what was written
# Use constants imported from eb_dashboard_constants.py (SINGLE SOURCE OF TRUTH)
inclusions_from_fs = _load_json_file_internal(INCLUSIONS_FILE_NAME)
organizations_from_fs = _load_json_file_internal(ORGANIZATIONS_FILE_NAME)
inclusions_from_fs = _load_json_file_internal(get_output_filename(INCLUSIONS_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:
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
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 (
INCLUSIONS_FILE_NAME,
ORGANIZATIONS_FILE_NAME,
@@ -93,7 +93,7 @@ def load_regression_check_config(console_instance=None):
if 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:
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)
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(
current_inclusions=INCLUSIONS_FILE_NAME,
organizations_list=ORGANIZATIONS_FILE_NAME,
current_inclusions=get_output_filename(INCLUSIONS_FILE_NAME),
organizations_list=get_output_filename(ORGANIZATIONS_FILE_NAME),
old_inclusions_filename=old_inclusions_file
)
@@ -370,8 +370,8 @@ def backup_output_files():
except Exception as 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(ORGANIZATIONS_FILE_NAME, _get_old_filename(ORGANIZATIONS_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(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
_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"):
"""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.