solidprivacy-nl commited on
Commit
5342e0e
·
1 Parent(s): cf45c80

Add v12.6 export sanity helper

Browse files
Files changed (1) hide show
  1. export_sanity.py +181 -0
export_sanity.py ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Advisory export sanity checks for Scrub Legal.
2
+
3
+ This helper prepares Dutch, user-facing warnings for the export step. It is
4
+ purely advisory: it does not block downloads, mutate review rows or change which
5
+ replacement rows are included in export.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Mapping
11
+ from typing import Any
12
+
13
+ from review_summary import (
14
+ NEEDS_REVIEW,
15
+ _is_checked,
16
+ _lower,
17
+ _normalise_rows,
18
+ _row_status,
19
+ build_review_summary,
20
+ )
21
+
22
+
23
+ CONTROL_NEEDED_WARNING = (
24
+ "Let op: {count} regel(s) met 'Controle nodig' staan niet aangevinkt. "
25
+ "Controleer deze waarden handmatig voordat u het bestand gebruikt."
26
+ )
27
+ CANDIDATE_NOT_INCLUDED_WARNING = (
28
+ "Let op: {count} mogelijke kandidaatwaarde(n) worden niet meegenomen in de export. "
29
+ "Niet-aangevinkte kandidaten blijven mogelijk zichtbaar in de uitvoer."
30
+ )
31
+ NO_REPLACEMENTS_WARNING = (
32
+ "Er zijn geen vervangingen geselecteerd. De export kan daardoor gelijk blijven "
33
+ "aan de invoer."
34
+ )
35
+ USER_REVIEW_REQUIRED_WARNING = (
36
+ "Gebruikerscontrole blijft nodig: controleer het opgeschoonde document altijd zelf "
37
+ "vóór gebruik of delen."
38
+ )
39
+ ADVISORY_EXPORT_WARNING = (
40
+ "De export is een hulpmiddel en geen garantie op volledige anonimisering. "
41
+ "Downloaden blijft mogelijk, maar de gebruiker blijft verantwoordelijk voor de eindcontrole."
42
+ )
43
+
44
+
45
+ def _to_int(value: Any) -> int:
46
+ try:
47
+ return int(value or 0)
48
+ except (TypeError, ValueError):
49
+ return 0
50
+
51
+
52
+ def _is_export_summary(value: Any) -> bool:
53
+ return isinstance(value, Mapping) and "total_rows" in value and "include" not in value
54
+
55
+
56
+ def _is_candidate_row(row: Mapping[str, Any]) -> bool:
57
+ source = _lower(row.get("source"))
58
+ candidate_marker = _lower(row.get("candidate"))
59
+ return source == "candidate" or candidate_marker in {"1", "true", "yes", "ja"}
60
+
61
+
62
+ def _normalise_export_summary(summary: Mapping[str, Any]) -> dict[str, int | bool | str | dict[str, Any]]:
63
+ review_summary = dict(summary.get("review_summary", summary))
64
+ checked_rows = _to_int(summary.get("checked_rows_included_in_export", review_summary.get("checked_rows_included_in_export")))
65
+ total_rows = _to_int(summary.get("total_rows", review_summary.get("total_rows")))
66
+ unchecked_control_needed_rows = _to_int(
67
+ summary.get("unchecked_control_needed_rows", review_summary.get("open_candidate_rows"))
68
+ )
69
+ candidate_rows_not_included = _to_int(
70
+ summary.get("candidate_rows_not_included", review_summary.get("open_candidate_rows"))
71
+ )
72
+
73
+ normalised: dict[str, int | bool | str | dict[str, Any]] = {
74
+ "total_rows": total_rows,
75
+ "checked_rows_included_in_export": checked_rows,
76
+ "unchecked_rows_excluded_from_export": _to_int(
77
+ summary.get(
78
+ "unchecked_rows_excluded_from_export",
79
+ review_summary.get("unchecked_rows_excluded_from_export"),
80
+ )
81
+ ),
82
+ "unchecked_control_needed_rows": unchecked_control_needed_rows,
83
+ "candidate_rows_not_included": candidate_rows_not_included,
84
+ "no_replacements_selected": bool(summary.get("no_replacements_selected", checked_rows == 0)),
85
+ "user_review_required": True,
86
+ "export_is_advisory": True,
87
+ "blocks_export": False,
88
+ "changes_export_semantics": False,
89
+ "review_summary": review_summary,
90
+ }
91
+ normalised["ready_label"] = export_sanity_ready_label(normalised)
92
+ return normalised
93
+
94
+
95
+ def build_export_sanity_checks(rows: Any) -> dict[str, int | bool | str | dict[str, Any]]:
96
+ """Build advisory export sanity flags from replacement review rows.
97
+
98
+ The returned flags are intended for warning text only. They must not be used
99
+ to block downloads or to change export inclusion behavior.
100
+ """
101
+ records = _normalise_rows(rows)
102
+ review_summary = build_review_summary(records)
103
+
104
+ unchecked_control_needed_rows = sum(
105
+ 1
106
+ for row in records
107
+ if _row_status(row) == NEEDS_REVIEW and not _is_checked(row.get("include"))
108
+ )
109
+ candidate_rows_not_included = sum(
110
+ 1 for row in records if _is_candidate_row(row) and not _is_checked(row.get("include"))
111
+ )
112
+ checked_rows = _to_int(review_summary.get("checked_rows_included_in_export"))
113
+
114
+ summary: dict[str, int | bool | str | dict[str, Any]] = {
115
+ "total_rows": _to_int(review_summary.get("total_rows")),
116
+ "checked_rows_included_in_export": checked_rows,
117
+ "unchecked_rows_excluded_from_export": _to_int(
118
+ review_summary.get("unchecked_rows_excluded_from_export")
119
+ ),
120
+ "unchecked_control_needed_rows": unchecked_control_needed_rows,
121
+ "candidate_rows_not_included": candidate_rows_not_included,
122
+ "no_replacements_selected": checked_rows == 0,
123
+ "user_review_required": True,
124
+ "export_is_advisory": True,
125
+ "blocks_export": False,
126
+ "changes_export_semantics": False,
127
+ "review_summary": review_summary,
128
+ }
129
+ summary["ready_label"] = export_sanity_ready_label(summary)
130
+ return summary
131
+
132
+
133
+ def _as_export_sanity_summary(summary_or_rows: Any) -> dict[str, int | bool | str | dict[str, Any]]:
134
+ if _is_export_summary(summary_or_rows):
135
+ return _normalise_export_summary(summary_or_rows)
136
+ return build_export_sanity_checks(summary_or_rows)
137
+
138
+
139
+ def export_sanity_warnings(summary_or_rows: Any) -> list[str]:
140
+ """Return Dutch user-facing warnings for the export step."""
141
+ summary = _as_export_sanity_summary(summary_or_rows)
142
+ warnings: list[str] = []
143
+
144
+ unchecked_control_needed_rows = _to_int(summary.get("unchecked_control_needed_rows"))
145
+ candidate_rows_not_included = _to_int(summary.get("candidate_rows_not_included"))
146
+
147
+ if unchecked_control_needed_rows > 0:
148
+ warnings.append(CONTROL_NEEDED_WARNING.format(count=unchecked_control_needed_rows))
149
+ if candidate_rows_not_included > 0:
150
+ warnings.append(CANDIDATE_NOT_INCLUDED_WARNING.format(count=candidate_rows_not_included))
151
+ if bool(summary.get("no_replacements_selected")):
152
+ warnings.append(NO_REPLACEMENTS_WARNING)
153
+
154
+ warnings.append(USER_REVIEW_REQUIRED_WARNING)
155
+ warnings.append(ADVISORY_EXPORT_WARNING)
156
+ return warnings
157
+
158
+
159
+ def export_sanity_ready_label(summary_or_rows: Any) -> str:
160
+ """Return a compact Dutch readiness label for advisory display."""
161
+ if _is_export_summary(summary_or_rows):
162
+ total_rows = _to_int(summary_or_rows.get("total_rows"))
163
+ checked_rows = _to_int(summary_or_rows.get("checked_rows_included_in_export"))
164
+ unchecked_control_needed_rows = _to_int(summary_or_rows.get("unchecked_control_needed_rows"))
165
+ candidate_rows_not_included = _to_int(summary_or_rows.get("candidate_rows_not_included"))
166
+ no_replacements_selected = bool(summary_or_rows.get("no_replacements_selected", checked_rows == 0))
167
+ else:
168
+ summary = build_export_sanity_checks(summary_or_rows)
169
+ total_rows = _to_int(summary.get("total_rows"))
170
+ checked_rows = _to_int(summary.get("checked_rows_included_in_export"))
171
+ unchecked_control_needed_rows = _to_int(summary.get("unchecked_control_needed_rows"))
172
+ candidate_rows_not_included = _to_int(summary.get("candidate_rows_not_included"))
173
+ no_replacements_selected = bool(summary.get("no_replacements_selected"))
174
+
175
+ if total_rows == 0:
176
+ return "Geen vervangregels gevonden — controleer handmatig"
177
+ if no_replacements_selected or checked_rows == 0:
178
+ return "Geen vervangingen geselecteerd"
179
+ if unchecked_control_needed_rows > 0 or candidate_rows_not_included > 0:
180
+ return "Controle nodig vóór export"
181
+ return "Klaar voor export na gebruikerscontrole"