refactor(gui): intégrer les Règles dans Administration > Profils

Retour Dom : « les règles du profil doivent être dans le menu profil, pas à
part ! ». Même logique que le Masquage — les règles qui influencent
l'anonymisation appartiennent au profil ; un sous-onglet séparé crée la
même confusion.

- Retrait du sous-onglet « Administration > Règles » (_SUBTABS, builder,
  méthode _build_regles supprimée). Sous-onglets restants : Réglages /
  Profils / Partage.
- Section « Profils > Règles du profil » enrichie : wording clair (règles
  d'anonymisation portées par le profil), aperçu illustratif de la table
  des règles (réutilise _rule_row + _HELP_REGLES), édition fine annoncée
  « à venir ».
- Abandon du « Testeur de règle » (écran outil global) pour ne pas
  réintroduire un second réglage métier.

Cible UX : Réglages / Profils (Général・Masquage・Mots・Moteurs・Règles du
profil) / Partage. Test obsolète test_rules_subtab_has_no_unexplained_2
remplacé par test_no_separate_rules_subtab.

262 tests unit OK (0 régression), self-test OK, nav 3 sous-onglets + section
Règles dans Profils + thème OK. Préserve d8bc0cd + GO Qwen. Aucun build/push
sans GO Dom.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-16 12:00:10 +02:00
parent d8bc0cd8c8
commit 764cf00581
3 changed files with 47 additions and 44 deletions

View File

@@ -24,7 +24,6 @@ _SUBTABS = [
("reg", "⚙️ Réglages"), ("reg", "⚙️ Réglages"),
("pro", "👤 Profils"), ("pro", "👤 Profils"),
("shr", "🔄 Partage"), ("shr", "🔄 Partage"),
("rul", "🛡️ Règles"),
] ]
_DETECTION_OPTIONS = [ _DETECTION_OPTIONS = [
@@ -210,7 +209,6 @@ class ConfigTab(ctk.CTkFrame):
"reg": self._build_reglages, "reg": self._build_reglages,
"pro": self._build_profils, "pro": self._build_profils,
"shr": self._build_partage, "shr": self._build_partage,
"rul": self._build_regles,
} }
for key, builder in builders.items(): for key, builder in builders.items():
panel = ctk.CTkFrame(self._body, fg_color="transparent") panel = ctk.CTkFrame(self._body, fg_color="transparent")
@@ -528,7 +526,25 @@ class ConfigTab(ctk.CTkFrame):
rules = ui_kit.Card(parent, p, title="🛡️ Règles du profil") rules = ui_kit.Card(parent, p, title="🛡️ Règles du profil")
rules.pack(fill="x", pady=(8, 0)) rules.pack(fill="x", pady=(8, 0))
self._note(rules, "Les règles embarquées par profil seront éditables prochainement.") rules_intro = ctk.CTkFrame(rules, fg_color="transparent")
rules_intro.pack(fill="x", padx=12, pady=(0, 2))
ctk.CTkLabel(
rules_intro,
text="Règles d'anonymisation portées par ce profil (adaptées à votre établissement).",
text_color=p["text_dim"], font=ui_kit.font(12), anchor="w", justify="left", wraplength=520,
).pack(side="left", padx=(0, 6))
ui_kit.help_button(rules_intro, p, _HELP_REGLES, title="Les Règles du profil").pack(side="right")
headers = ctk.CTkFrame(rules, fg_color="transparent")
headers.pack(fill="x", padx=12, pady=(2, 4))
for text, width in [("Label", 190), ("Type", 80), ("Cible → Résultat", 210), ("Statut", 70), ("", 70)]:
ctk.CTkLabel(headers, text=text.upper(), width=width, anchor="w", text_color=p["text_muted"], font=ui_kit.font(10, "bold")).pack(side="left")
for row in [
("Masquer le sigle CHUXX", "exact", "CHUXX → [MASK]", "Actif"),
("Préserver “classification internationale”", "preserve", "conservé tel quel", "Actif"),
("Identifier N° 1234567", "norm-id", "N° 1234567 → [NDA]", "Candidat"),
]:
self._rule_row(rules, row)
self._note(rules, "Aperçu illustratif. L'édition fine des règles du profil arrivera dans une prochaine version.")
self._mockup_button(rules, "+ Ajouter une règle").pack(anchor="w", padx=12, pady=(0, 12)) self._mockup_button(rules, "+ Ajouter une règle").pack(anchor="w", padx=12, pady=(0, 12))
self._pro_refresh_and_load() self._pro_refresh_and_load()
@@ -716,43 +732,6 @@ class ConfigTab(ctk.CTkFrame):
self._note(import_card, "Fusionne la configuration reçue avec vos réglages locaux.") self._note(import_card, "Fusionne la configuration reçue avec vos réglages locaux.")
self._mockup_button(import_card, "⬆ Importer (.json)").pack(anchor="w", padx=12, pady=(0, 12)) self._mockup_button(import_card, "⬆ Importer (.json)").pack(anchor="w", padx=12, pady=(0, 12))
def _build_regles(self, parent) -> None:
p = self._p
self._section_intro(
parent,
"Règles d'adaptation du moteur à votre établissement.",
_HELP_REGLES,
"Les Règles",
)
card = ui_kit.Card(parent, p, title="🛡️ Règles actives")
card.pack(fill="x", pady=(0, 8))
self._note(card, "Ces règles adaptent le moteur à votre établissement. Chaque règle est validée avant activation.")
headers = ctk.CTkFrame(card, fg_color="transparent")
headers.pack(fill="x", padx=12, pady=(0, 4))
for text, width in [("Label", 190), ("Type", 80), ("Cible → Résultat", 210), ("Statut", 70), ("", 70)]:
ctk.CTkLabel(headers, text=text.upper(), width=width, anchor="w", text_color=p["text_muted"], font=ui_kit.font(10, "bold")).pack(side="left")
for row in [
("Masquer le sigle CHUXX", "exact", "CHUXX → [MASK]", "Actif"),
("Préserver “classification internationale”", "preserve", "conservé tel quel", "Actif"),
("Identifier N° 1234567", "norm-id", "N° 1234567 → [NDA]", "Candidat"),
]:
self._rule_row(card, row)
actions = ctk.CTkFrame(card, fg_color="transparent")
actions.pack(fill="x", padx=12, pady=(8, 12))
self._mockup_button(actions, "+ Nouvelle règle", primary=True).pack(side="left", padx=(0, 8))
self._mockup_button(actions, "🔄 Recharger").pack(side="left")
sim = ui_kit.Card(parent, p, title="🧪 Testeur de règle")
sim.pack(fill="x")
ctk.CTkLabel(sim, text="Texte de test", text_color=p["text_muted"], font=ui_kit.font(12)).pack(anchor="w", padx=12)
txt = ctk.CTkTextbox(sim, height=74, fg_color=p["divider"], text_color=p["text"], border_color=p["card_border"], border_width=1)
txt.pack(fill="x", padx=12, pady=(5, 8))
txt.insert("1.0", "Compte rendu CHUXX, patient N° 1234567.")
btns = ctk.CTkFrame(sim, fg_color="transparent")
btns.pack(fill="x", padx=12, pady=(0, 12))
self._mockup_button(btns, "▶ Tester", primary=True).pack(side="left", padx=(0, 8))
self._mockup_button(btns, "✖ Fermer").pack(side="left")
# -- helpers aide / maquette ----------------------------------------- # -- helpers aide / maquette -----------------------------------------
def _section_intro(self, parent, sentence: str, help_text: str, help_title: str) -> None: def _section_intro(self, parent, sentence: str, help_text: str, help_title: str) -> None:

View File

@@ -53,13 +53,15 @@ def test_main_tab_renamed_to_administration():
assert not any("Configuration" in lbl for lbl in labels) assert not any("Configuration" in lbl for lbl in labels)
def test_rules_subtab_has_no_unexplained_2(): def test_no_separate_rules_subtab():
"""Retour Dom #3 : « Règles 2 » incompréhensible → simple « Règles ».""" """Retour Dom : les règles appartiennent au profil → plus de sous-onglet
« Règles » séparé (et donc plus de « Règles 2 » incompréhensible)."""
from gui_v6.tabs.tab_config import _SUBTABS from gui_v6.tabs.tab_config import _SUBTABS
keys = [key for key, _ in _SUBTABS]
labels = [label for _, label in _SUBTABS] labels = [label for _, label in _SUBTABS]
assert any(lbl.strip() == "🛡️ Règles" for lbl in labels) assert "rul" not in keys
assert not any("Règles 2" in lbl or "Règles 2" in lbl for lbl in labels) assert not any("Règles" in lbl for lbl in labels)
def test_help_button_opens_help_window(app): def test_help_button_opens_help_window(app):

View File

@@ -273,3 +273,25 @@ def test_masquage_moved_into_profils(ctk_root, tmp_path, monkeypatch):
tab._on_mask_template_saved(saved) tab._on_mask_template_saved(saved)
assert tab._pro_template_var.get().endswith("depuis_editeur.json") assert tab._pro_template_var.get().endswith("depuis_editeur.json")
tab.destroy() tab.destroy()
def test_regles_moved_into_profils(ctk_root, tmp_path, monkeypatch):
"""Retour Dom : le sous-onglet Règles séparé est retiré ; les règles du
profil sont une section de Profils."""
from gui_v6.tabs import tab_config
keys = [k for k, _ in tab_config._SUBTABS]
assert "rul" not in keys # plus de sous-onglet Règles séparé
monkeypatch.setattr(tab_config, "_app_base_dir", lambda: tmp_path)
tab = tab_config.ConfigTab(ctk_root)
tab._show_sub("pro")
tab.update_idletasks()
# la section des règles du profil est dans le panneau Profils
texts = " | ".join(_all_texts(tab._panels["pro"]))
assert "Règles d'anonymisation portées par ce profil" in texts
assert "Masquer le sigle CHUXX" in texts # table de règles relocalisée dans Profils
# le builder du sous-onglet séparé n'existe plus
assert "rul" not in tab._panels
tab.destroy()