"""Advisory export sanity checks for Scrub Legal. This helper prepares Dutch, user-facing warnings for the export step. It is purely advisory: it does not block downloads, mutate review rows or change which replacement rows are included in export. """ from __future__ import annotations from collections.abc import Mapping from typing import Any from review_summary import ( NEEDS_REVIEW, _is_checked, _lower, _normalise_rows, _row_status, build_review_summary, ) CONTROL_NEEDED_WARNING = ( "Let op: {count} regel(s) met 'Controle nodig' staan niet aangevinkt. " "Controleer deze waarden handmatig voordat u het bestand gebruikt." ) CANDIDATE_NOT_INCLUDED_WARNING = ( "Let op: {count} mogelijke kandidaatwaarde(n) worden niet meegenomen in de export. " "Niet-aangevinkte kandidaten blijven mogelijk zichtbaar in de uitvoer." ) NO_REPLACEMENTS_WARNING = ( "Er zijn geen vervangingen geselecteerd. De export kan daardoor gelijk blijven " "aan de invoer." ) USER_REVIEW_REQUIRED_WARNING = ( "Gebruikerscontrole blijft nodig: controleer het opgeschoonde document altijd zelf " "vóór gebruik of delen." ) ADVISORY_EXPORT_WARNING = ( "De export is een hulpmiddel en geen garantie op volledige anonimisering. " "Downloaden blijft mogelijk, maar de gebruiker blijft verantwoordelijk voor de eindcontrole." ) def _to_int(value: Any) -> int: try: return int(value or 0) except (TypeError, ValueError): return 0 def _is_export_summary(value: Any) -> bool: return isinstance(value, Mapping) and "total_rows" in value and "include" not in value def _is_candidate_row(row: Mapping[str, Any]) -> bool: source = _lower(row.get("source")) candidate_marker = _lower(row.get("candidate")) return source == "candidate" or candidate_marker in {"1", "true", "yes", "ja"} def _normalise_export_summary(summary: Mapping[str, Any]) -> dict[str, int | bool | str | dict[str, Any]]: review_summary = dict(summary.get("review_summary", summary)) checked_rows = _to_int(summary.get("checked_rows_included_in_export", review_summary.get("checked_rows_included_in_export"))) total_rows = _to_int(summary.get("total_rows", review_summary.get("total_rows"))) unchecked_control_needed_rows = _to_int( summary.get("unchecked_control_needed_rows", review_summary.get("open_candidate_rows")) ) candidate_rows_not_included = _to_int( summary.get("candidate_rows_not_included", review_summary.get("open_candidate_rows")) ) normalised: dict[str, int | bool | str | dict[str, Any]] = { "total_rows": total_rows, "checked_rows_included_in_export": checked_rows, "unchecked_rows_excluded_from_export": _to_int( summary.get( "unchecked_rows_excluded_from_export", review_summary.get("unchecked_rows_excluded_from_export"), ) ), "unchecked_control_needed_rows": unchecked_control_needed_rows, "candidate_rows_not_included": candidate_rows_not_included, "no_replacements_selected": bool(summary.get("no_replacements_selected", checked_rows == 0)), "user_review_required": True, "export_is_advisory": True, "blocks_export": False, "changes_export_semantics": False, "review_summary": review_summary, } normalised["ready_label"] = export_sanity_ready_label(normalised) return normalised def build_export_sanity_checks(rows: Any) -> dict[str, int | bool | str | dict[str, Any]]: """Build advisory export sanity flags from replacement review rows. The returned flags are intended for warning text only. They must not be used to block downloads or to change export inclusion behavior. """ records = _normalise_rows(rows) review_summary = build_review_summary(records) unchecked_control_needed_rows = sum( 1 for row in records if _row_status(row) == NEEDS_REVIEW and not _is_checked(row.get("include")) ) candidate_rows_not_included = sum( 1 for row in records if _is_candidate_row(row) and not _is_checked(row.get("include")) ) checked_rows = _to_int(review_summary.get("checked_rows_included_in_export")) summary: dict[str, int | bool | str | dict[str, Any]] = { "total_rows": _to_int(review_summary.get("total_rows")), "checked_rows_included_in_export": checked_rows, "unchecked_rows_excluded_from_export": _to_int( review_summary.get("unchecked_rows_excluded_from_export") ), "unchecked_control_needed_rows": unchecked_control_needed_rows, "candidate_rows_not_included": candidate_rows_not_included, "no_replacements_selected": checked_rows == 0, "user_review_required": True, "export_is_advisory": True, "blocks_export": False, "changes_export_semantics": False, "review_summary": review_summary, } summary["ready_label"] = export_sanity_ready_label(summary) return summary def _as_export_sanity_summary(summary_or_rows: Any) -> dict[str, int | bool | str | dict[str, Any]]: if _is_export_summary(summary_or_rows): return _normalise_export_summary(summary_or_rows) return build_export_sanity_checks(summary_or_rows) def export_sanity_warnings(summary_or_rows: Any) -> list[str]: """Return Dutch user-facing warnings for the export step.""" summary = _as_export_sanity_summary(summary_or_rows) warnings: list[str] = [] unchecked_control_needed_rows = _to_int(summary.get("unchecked_control_needed_rows")) candidate_rows_not_included = _to_int(summary.get("candidate_rows_not_included")) if unchecked_control_needed_rows > 0: warnings.append(CONTROL_NEEDED_WARNING.format(count=unchecked_control_needed_rows)) if candidate_rows_not_included > 0: warnings.append(CANDIDATE_NOT_INCLUDED_WARNING.format(count=candidate_rows_not_included)) if bool(summary.get("no_replacements_selected")): warnings.append(NO_REPLACEMENTS_WARNING) warnings.append(USER_REVIEW_REQUIRED_WARNING) warnings.append(ADVISORY_EXPORT_WARNING) return warnings def export_sanity_ready_label(summary_or_rows: Any) -> str: """Return a compact Dutch readiness label for advisory display.""" if _is_export_summary(summary_or_rows): total_rows = _to_int(summary_or_rows.get("total_rows")) checked_rows = _to_int(summary_or_rows.get("checked_rows_included_in_export")) unchecked_control_needed_rows = _to_int(summary_or_rows.get("unchecked_control_needed_rows")) candidate_rows_not_included = _to_int(summary_or_rows.get("candidate_rows_not_included")) no_replacements_selected = bool(summary_or_rows.get("no_replacements_selected", checked_rows == 0)) else: summary = build_export_sanity_checks(summary_or_rows) total_rows = _to_int(summary.get("total_rows")) checked_rows = _to_int(summary.get("checked_rows_included_in_export")) unchecked_control_needed_rows = _to_int(summary.get("unchecked_control_needed_rows")) candidate_rows_not_included = _to_int(summary.get("candidate_rows_not_included")) no_replacements_selected = bool(summary.get("no_replacements_selected")) if total_rows == 0: return "Geen vervangregels gevonden — controleer handmatig" if no_replacements_selected or checked_rows == 0: return "Geen vervangingen geselecteerd" if unchecked_control_needed_rows > 0 or candidate_rows_not_included > 0: return "Controle nodig vóór export" return "Klaar voor export na gebruikerscontrole"