feat(gui): câbler les 7 toggles catégories au moteur (P1-2)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,10 +9,36 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable, List, Optional
|
from typing import Callable, FrozenSet, List, Optional
|
||||||
|
|
||||||
from gui_v6.engine_bridge import EngineSettings
|
from gui_v6.engine_bridge import EngineSettings
|
||||||
|
|
||||||
|
# Mapping centralisé champ ConfigState → CATÉGORIE moteur (Plan 1b / P1-2).
|
||||||
|
#
|
||||||
|
# Les 7 catégories doivent matcher EXACTEMENT le set accepté par
|
||||||
|
# ``anonymizer_core_refactored_onnx.process_pdf(disabled_kinds=...)`` :
|
||||||
|
# {"NOM", "DATE_NAISSANCE", "ETAB", "ADRESSE", "NIR", "TEL", "ADHERENT"}.
|
||||||
|
#
|
||||||
|
# Sémantique des booléens ``detect_*`` : True = « détecter cette catégorie »
|
||||||
|
# (= masquer, comportement par défaut). False = laisser en clair → la catégorie
|
||||||
|
# entre dans ``disabled_kinds``. Note : CODE_POSTAL suit le toggle ADRESSE côté
|
||||||
|
# moteur (décision Dom 2026-06-26), aucun toggle dédié n'est exposé.
|
||||||
|
#
|
||||||
|
# L'ordre suit les 7 lignes de ``tab_config._DETECTION_OPTIONS`` :
|
||||||
|
# Noms/prénoms · Dates de naissance · Établissements · Adresses/CP ·
|
||||||
|
# N° sécurité sociale · Téléphones/e-mails · N° adhérent mutuelle.
|
||||||
|
CATEGORY_FIELDS = {
|
||||||
|
"detect_nom": "NOM",
|
||||||
|
"detect_date_naissance": "DATE_NAISSANCE",
|
||||||
|
"detect_etab": "ETAB",
|
||||||
|
"detect_adresse": "ADRESSE",
|
||||||
|
"detect_nir": "NIR",
|
||||||
|
"detect_tel": "TEL",
|
||||||
|
"detect_adherent": "ADHERENT",
|
||||||
|
}
|
||||||
|
# Catégories canoniques (ordre = ordre des toggles UI).
|
||||||
|
DETECTION_CATEGORIES = tuple(CATEGORY_FIELDS.values())
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ConfigState:
|
class ConfigState:
|
||||||
@@ -33,6 +59,26 @@ class ConfigState:
|
|||||||
mask_margin_y: int = 1
|
mask_margin_y: int = 1
|
||||||
mask_rounded_corners: bool = False
|
mask_rounded_corners: bool = False
|
||||||
|
|
||||||
|
# 7 toggles « Données à détecter » — tous ON par défaut (zéro changement).
|
||||||
|
detect_nom: bool = True
|
||||||
|
detect_date_naissance: bool = True
|
||||||
|
detect_etab: bool = True
|
||||||
|
detect_adresse: bool = True
|
||||||
|
detect_nir: bool = True
|
||||||
|
detect_tel: bool = True
|
||||||
|
detect_adherent: bool = True
|
||||||
|
|
||||||
|
def disabled_kinds(self) -> FrozenSet[str]:
|
||||||
|
"""Set des CATÉGORIES décochées (laissées en clair).
|
||||||
|
|
||||||
|
Défaut (tous les toggles ON) ⇒ ``frozenset()`` (no-op moteur).
|
||||||
|
"""
|
||||||
|
return frozenset(
|
||||||
|
category
|
||||||
|
for field_name, category in CATEGORY_FIELDS.items()
|
||||||
|
if not getattr(self, field_name)
|
||||||
|
)
|
||||||
|
|
||||||
def to_engine_settings(self, config_path: Optional[Path] = None) -> EngineSettings:
|
def to_engine_settings(self, config_path: Optional[Path] = None) -> EngineSettings:
|
||||||
return EngineSettings(
|
return EngineSettings(
|
||||||
make_vector_redaction=False,
|
make_vector_redaction=False,
|
||||||
@@ -43,6 +89,7 @@ class ConfigState:
|
|||||||
enable_gliner=self.enable_gliner,
|
enable_gliner=self.enable_gliner,
|
||||||
ogc_label=self.ogc_label,
|
ogc_label=self.ogc_label,
|
||||||
profile=self.profile,
|
profile=self.profile,
|
||||||
|
disabled_kinds=self.disabled_kinds(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ Aucune logique de détection ici : on orchestre uniquement.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Callable, Dict, Optional
|
from typing import Any, Callable, Dict, FrozenSet, Optional
|
||||||
|
|
||||||
from engine_capabilities import capabilities_map
|
from engine_capabilities import capabilities_map
|
||||||
|
|
||||||
@@ -61,6 +61,9 @@ class EngineSettings:
|
|||||||
enable_gliner: bool = False
|
enable_gliner: bool = False
|
||||||
ogc_label: Optional[str] = None
|
ogc_label: Optional[str] = None
|
||||||
profile: Optional[str] = None
|
profile: Optional[str] = None
|
||||||
|
# Plan 1b (P1-2) — set des CATÉGORIES laissées en clair (toggles décochés).
|
||||||
|
# Vide par défaut ⇒ aucun changement de comportement (tout est masqué).
|
||||||
|
disabled_kinds: FrozenSet[str] = field(default_factory=frozenset)
|
||||||
|
|
||||||
|
|
||||||
def _default_factories() -> dict[str, ManagerFactory]:
|
def _default_factories() -> dict[str, ManagerFactory]:
|
||||||
@@ -206,6 +209,8 @@ def build_engine_kwargs(
|
|||||||
"also_make_raster_burn": settings.also_make_raster_burn,
|
"also_make_raster_burn": settings.also_make_raster_burn,
|
||||||
"config_path": settings.config_path,
|
"config_path": settings.config_path,
|
||||||
"ogc_label": settings.ogc_label,
|
"ogc_label": settings.ogc_label,
|
||||||
|
# Plan 1b (P1-2) — catégories décochées laissées en clair (set vide = no-op).
|
||||||
|
"disabled_kinds": frozenset(settings.disabled_kinds or ()),
|
||||||
}
|
}
|
||||||
if managers is not None and settings.use_local_ner:
|
if managers is not None and settings.use_local_ner:
|
||||||
kwargs.update(managers.as_kwargs())
|
kwargs.update(managers.as_kwargs())
|
||||||
|
|||||||
@@ -26,14 +26,17 @@ _SUBTABS = [
|
|||||||
("shr", "🔄 Partage"),
|
("shr", "🔄 Partage"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Chaque ligne = (libellé, aide, champ ConfigState). Le champ relie le toggle
|
||||||
|
# à la catégorie moteur (cf. gui_v6.config_state.CATEGORY_FIELDS). ON = détecter
|
||||||
|
# (masquer) ; OFF = laisser en clair (entre dans disabled_kinds).
|
||||||
_DETECTION_OPTIONS = [
|
_DETECTION_OPTIONS = [
|
||||||
("Noms et prénoms", "Annuaire + IA"),
|
("Noms et prénoms", "Annuaire + IA", "detect_nom"),
|
||||||
("Dates de naissance", "Contexte naissance"),
|
("Dates de naissance", "Contexte naissance", "detect_date_naissance"),
|
||||||
("Établissements", "FINESS + contexte"),
|
("Établissements", "FINESS + contexte", "detect_etab"),
|
||||||
("Adresses / CP", "Voie, ville, code"),
|
("Adresses / CP", "Voie, ville, code", "detect_adresse"),
|
||||||
("N° sécurité sociale", "NIR"),
|
("N° sécurité sociale", "NIR", "detect_nir"),
|
||||||
("Téléphones / e-mails", "Contact"),
|
("Téléphones / e-mails", "Contact", "detect_tel"),
|
||||||
("N° adhérent mutuelle", "Identifiant local"),
|
("N° adhérent mutuelle", "Identifiant local", "detect_adherent"),
|
||||||
]
|
]
|
||||||
|
|
||||||
_MASK_COLORS = [
|
_MASK_COLORS = [
|
||||||
@@ -353,8 +356,20 @@ class ConfigTab(ctk.CTkFrame):
|
|||||||
help_text=_HELP_DONNEES_DETECTER, help_title="Données à détecter",
|
help_text=_HELP_DONNEES_DETECTER, help_title="Données à détecter",
|
||||||
)
|
)
|
||||||
det.pack(fill="both", expand=True)
|
det.pack(fill="both", expand=True)
|
||||||
for label, hint in _DETECTION_OPTIONS:
|
# Les 7 toggles « Données à détecter » sont câblés sur les booléens
|
||||||
self._mini_toggle(det, label, hint, value=True).pack(fill="x", padx=12, pady=1)
|
# detect_* de ConfigState (lecture initiale + écriture au changement).
|
||||||
|
# ON = détecter/masquer ; OFF = laisser en clair (→ disabled_kinds).
|
||||||
|
self._detect_toggles: dict[str, object] = {}
|
||||||
|
for label, hint, field_name in _DETECTION_OPTIONS:
|
||||||
|
toggle = self._mini_toggle(
|
||||||
|
det,
|
||||||
|
label,
|
||||||
|
hint,
|
||||||
|
value=bool(getattr(self._state, field_name)),
|
||||||
|
command=lambda f=field_name: self._on_detect_toggle(f),
|
||||||
|
)
|
||||||
|
toggle.pack(fill="x", padx=12, pady=1)
|
||||||
|
self._detect_toggles[field_name] = toggle
|
||||||
|
|
||||||
ner = ui_kit.Card(
|
ner = ui_kit.Card(
|
||||||
cols[1], p, title="🧠 Moteurs et masques",
|
cols[1], p, title="🧠 Moteurs et masques",
|
||||||
@@ -865,6 +880,16 @@ class ConfigTab(ctk.CTkFrame):
|
|||||||
def _on_profile(self, value: str) -> None:
|
def _on_profile(self, value: str) -> None:
|
||||||
self._state.profile = value
|
self._state.profile = value
|
||||||
|
|
||||||
|
def _on_detect_toggle(self, field_name: str) -> None:
|
||||||
|
"""Recopie l'état d'un toggle « Données à détecter » dans ConfigState.
|
||||||
|
|
||||||
|
ON = détecter (masquer) ; OFF = laisser en clair. ``disabled_kinds()``
|
||||||
|
de ConfigState dérive ensuite le set des catégories désactivées.
|
||||||
|
"""
|
||||||
|
toggle = self._detect_toggles.get(field_name)
|
||||||
|
if toggle is not None:
|
||||||
|
setattr(self._state, field_name, bool(toggle.get()))
|
||||||
|
|
||||||
def _on_ner(self) -> None:
|
def _on_ner(self) -> None:
|
||||||
self._state.use_local_ner = self._tog_ner.get()
|
self._state.use_local_ner = self._tog_ner.get()
|
||||||
|
|
||||||
|
|||||||
121
tests/unit/test_gui_v6_category_toggles.py
Normal file
121
tests/unit/test_gui_v6_category_toggles.py
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
"""Tests du câblage des 7 toggles « Données à détecter » → moteur (Plan 1b / P1-2).
|
||||||
|
|
||||||
|
Sémantique UI : un toggle ON = « détecter cette catégorie » (= masquer).
|
||||||
|
Un toggle OFF = la catégorie est laissée en clair → elle entre dans
|
||||||
|
``disabled_kinds`` (set des CATÉGORIES désactivées passé au moteur).
|
||||||
|
|
||||||
|
Aucun widget, aucun display : on teste l'état (ConfigState) et le pont
|
||||||
|
(build_engine_kwargs / make_process_fn) en pur Python.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from gui_v6.config_state import (
|
||||||
|
CATEGORY_FIELDS,
|
||||||
|
DETECTION_CATEGORIES,
|
||||||
|
ConfigState,
|
||||||
|
)
|
||||||
|
from gui_v6.engine_bridge import (
|
||||||
|
EngineSettings,
|
||||||
|
NerManagers,
|
||||||
|
build_engine_kwargs,
|
||||||
|
make_process_fn,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# -- catégories canoniques -------------------------------------------------
|
||||||
|
|
||||||
|
def test_seven_categories_match_engine_set():
|
||||||
|
# Les 7 catégories exposées doivent matcher EXACTEMENT le set moteur.
|
||||||
|
assert set(DETECTION_CATEGORIES) == {
|
||||||
|
"NOM",
|
||||||
|
"DATE_NAISSANCE",
|
||||||
|
"ETAB",
|
||||||
|
"ADRESSE",
|
||||||
|
"NIR",
|
||||||
|
"TEL",
|
||||||
|
"ADHERENT",
|
||||||
|
}
|
||||||
|
# Un champ booléen par catégorie.
|
||||||
|
assert set(CATEGORY_FIELDS.values()) == set(DETECTION_CATEGORIES)
|
||||||
|
|
||||||
|
|
||||||
|
# -- disabled_kinds dérivé -------------------------------------------------
|
||||||
|
|
||||||
|
def test_disabled_kinds_empty_by_default():
|
||||||
|
# Défaut : tous les toggles ON ⇒ aucun désactivé (zéro changement vs aujourd'hui).
|
||||||
|
state = ConfigState()
|
||||||
|
assert state.disabled_kinds() == frozenset()
|
||||||
|
|
||||||
|
|
||||||
|
def test_disabled_kinds_unchecking_nir_and_etab():
|
||||||
|
# Décocher « N° sécurité sociale » (NIR) et « Établissements » (ETAB).
|
||||||
|
state = ConfigState(detect_nir=False, detect_etab=False)
|
||||||
|
assert state.disabled_kinds() == frozenset({"NIR", "ETAB"})
|
||||||
|
|
||||||
|
|
||||||
|
def test_disabled_kinds_all_off():
|
||||||
|
state = ConfigState(
|
||||||
|
detect_nom=False,
|
||||||
|
detect_date_naissance=False,
|
||||||
|
detect_etab=False,
|
||||||
|
detect_adresse=False,
|
||||||
|
detect_nir=False,
|
||||||
|
detect_tel=False,
|
||||||
|
detect_adherent=False,
|
||||||
|
)
|
||||||
|
assert state.disabled_kinds() == frozenset(DETECTION_CATEGORIES)
|
||||||
|
|
||||||
|
|
||||||
|
# -- propagation vers EngineSettings --------------------------------------
|
||||||
|
|
||||||
|
def test_to_engine_settings_propagates_disabled_kinds():
|
||||||
|
state = ConfigState(detect_nir=False, detect_tel=False)
|
||||||
|
settings = state.to_engine_settings()
|
||||||
|
assert settings.disabled_kinds == frozenset({"NIR", "TEL"})
|
||||||
|
|
||||||
|
|
||||||
|
def test_to_engine_settings_default_empty():
|
||||||
|
settings = ConfigState().to_engine_settings()
|
||||||
|
assert settings.disabled_kinds == frozenset()
|
||||||
|
|
||||||
|
|
||||||
|
# -- propagation dans les kwargs moteur -----------------------------------
|
||||||
|
|
||||||
|
def test_build_engine_kwargs_includes_disabled_kinds():
|
||||||
|
settings = EngineSettings(disabled_kinds=frozenset({"NIR", "ETAB"}))
|
||||||
|
kwargs = build_engine_kwargs(settings, managers=None)
|
||||||
|
assert kwargs["disabled_kinds"] == frozenset({"NIR", "ETAB"})
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_engine_kwargs_default_empty_disabled_kinds():
|
||||||
|
# Défaut (set vide) = no-op : la clé est présente mais vide.
|
||||||
|
kwargs = build_engine_kwargs(EngineSettings(), managers=None)
|
||||||
|
assert kwargs["disabled_kinds"] == frozenset()
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_fn_threads_disabled_kinds_to_engine(tmp_path):
|
||||||
|
settings = EngineSettings(
|
||||||
|
use_local_ner=False, disabled_kinds=frozenset({"ADRESSE"})
|
||||||
|
)
|
||||||
|
managers = NerManagers(settings)
|
||||||
|
captured = {}
|
||||||
|
|
||||||
|
def fake_engine(doc_path, out_dir, **kwargs):
|
||||||
|
captured["kwargs"] = kwargs
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
fn = make_process_fn(settings, managers=managers, engine=fake_engine)
|
||||||
|
fn(tmp_path / "doc.pdf", tmp_path / "out")
|
||||||
|
assert captured["kwargs"]["disabled_kinds"] == frozenset({"ADRESSE"})
|
||||||
|
|
||||||
|
|
||||||
|
# -- bout-en-bout : ConfigState → settings → kwargs -----------------------
|
||||||
|
|
||||||
|
def test_end_to_end_state_to_kwargs(tmp_path):
|
||||||
|
state = ConfigState(detect_adherent=False)
|
||||||
|
settings = state.to_engine_settings(config_path=Path("/tmp/c.yml"))
|
||||||
|
kwargs = build_engine_kwargs(settings, managers=None)
|
||||||
|
assert kwargs["disabled_kinds"] == frozenset({"ADHERENT"})
|
||||||
@@ -55,10 +55,26 @@ def test_config_interaction_contract_prebuilds_panels_and_mask_editor():
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_detection_options_fields_match_category_fields():
|
||||||
|
"""Garde-fou anti-dérive : les champs déclarés dans _DETECTION_OPTIONS doivent
|
||||||
|
rester alignés (mêmes champs ET même ordre) sur CATEGORY_FIELDS, sinon un
|
||||||
|
toggle pointerait vers un attribut ConfigState inexistant (AttributeError au
|
||||||
|
lancement de la GUI au lieu d'un échec de test)."""
|
||||||
|
from gui_v6.config_state import CATEGORY_FIELDS, ConfigState
|
||||||
|
|
||||||
|
fields = [field for _l, _h, field in _DETECTION_OPTIONS]
|
||||||
|
assert fields == list(CATEGORY_FIELDS) # mêmes champs ET même ordre (ordre UI = ordre catégories)
|
||||||
|
for f in fields: # chacun est bien un booléen réel de ConfigState
|
||||||
|
assert isinstance(getattr(ConfigState(), f), bool)
|
||||||
|
|
||||||
|
|
||||||
def test_detection_rows_are_readable_in_light_theme():
|
def test_detection_rows_are_readable_in_light_theme():
|
||||||
"""Retour Dom : les sous-labels de la colonne détection doivent rester lisibles."""
|
"""Retour Dom : les sous-labels de la colonne détection doivent rester lisibles."""
|
||||||
assert ("Noms et prénoms", "Annuaire + IA") in _DETECTION_OPTIONS
|
# Chaque ligne est désormais (libellé, aide, champ ConfigState) ; on ne
|
||||||
assert ("Noms et prénoms", "Bases de données + IA") not in _DETECTION_OPTIONS
|
# vérifie ici que le couple (libellé, aide) reste lisible.
|
||||||
|
label_hint = [(label, hint) for label, hint, _field in _DETECTION_OPTIONS]
|
||||||
|
assert ("Noms et prénoms", "Annuaire + IA") in label_hint
|
||||||
|
assert ("Noms et prénoms", "Bases de données + IA") not in label_hint
|
||||||
assert MINI_TOGGLE_HEIGHT >= 44
|
assert MINI_TOGGLE_HEIGHT >= 44
|
||||||
assert MINI_TOGGLE_LABEL_FONT_SIZE >= 12
|
assert MINI_TOGGLE_LABEL_FONT_SIZE >= 12
|
||||||
assert MINI_TOGGLE_HINT_FONT_SIZE >= 11
|
assert MINI_TOGGLE_HINT_FONT_SIZE >= 11
|
||||||
|
|||||||
Reference in New Issue
Block a user