File size: 7,691 Bytes
5342e0e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
"""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"