diff --git a/eb_dashboard.py b/eb_dashboard.py index cf03856..06ea788 100644 --- a/eb_dashboard.py +++ b/eb_dashboard.py @@ -88,6 +88,8 @@ from eb_dashboard_utils import ( 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 ) @@ -105,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 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') # ============================================================================ @@ -132,6 +149,7 @@ _user_interaction_lock = threading.Lock() # Global variables (mutable, set at runtime - not constants) inclusions_mapping_config = [] organizations_mapping_config = [] +_exclude_drafts = False # Set by --exclude-drafts CLI arg if provided excel_export_config = None excel_export_enabled = False @@ -158,8 +176,7 @@ 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): @@ -167,6 +184,11 @@ if "--config" in sys.argv: else: set_dashboard_config_path_override(os.path.join(get_config_path(), _raw_config_path)) +# Handle --exclude-drafts flag (remove from sys.argv to preserve positional args) +_exclude_drafts = "--exclude-drafts" in sys.argv +if _exclude_drafts: + sys.argv.remove("--exclude-drafts") + # --- Progress Bar Configuration --- # NOTE: BAR_N_FMT_WIDTH, BAR_TOTAL_FMT_WIDTH, BAR_TIME_WIDTH, BAR_RATE_WIDTH # are imported from eb_dashboard_constants.py (SINGLE SOURCE OF TRUTH) @@ -1233,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. + # Activated via --exclude-drafts CLI flag. + if _exclude_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: @@ -1497,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 @@ -1642,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 === @@ -1667,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]") diff --git a/eb_dashboard_excel_export.py b/eb_dashboard_excel_export.py index 7db4d7e..b28c528 100644 --- a/eb_dashboard_excel_export.py +++ b/eb_dashboard_excel_export.py @@ -34,7 +34,7 @@ try: except ImportError: xw = None -from eb_dashboard_utils import get_nested_value, get_config_path, get_dashboard_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, @@ -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" diff --git a/eb_dashboard_quality_checks.py b/eb_dashboard_quality_checks.py index 1941383..4e0d624 100644 --- a/eb_dashboard_quality_checks.py +++ b/eb_dashboard_quality_checks.py @@ -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, get_dashboard_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, @@ -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)) # ============================================================================ diff --git a/eb_dashboard_utils.py b/eb_dashboard_utils.py index 5745a0e..3f6a163 100644 --- a/eb_dashboard_utils.py +++ b/eb_dashboard_utils.py @@ -220,6 +220,27 @@ def get_dashboard_config_path(config_file_name): 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.