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:
@@ -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:
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user