refactor(gui): Réglages — tableau des termes en accès direct, retrait du doublon Profils
Retour Dom après validation visuelle : simplifier.
- Réglages > Listes locales : suppression des pastilles de termes et des
éditeurs inline (_compact_tag_editor). Remplacés par un texte court +
compteurs (À conserver/À masquer/À ignorer du profil actif) + bouton
« Ouvrir le tableau des termes » qui ouvre DIRECTEMENT TermsTableWindow.
- Retrait du bouton « Voir le profil » (son rôle = accéder au tableau).
- Retrait du sous-onglet « Profils » (doublon non câblé) : _SUBTABS,
builders, _build_profils/_rebuild_profils. Les helpers profil
(_active_profile_summary/_open_terms_table) sont conservés pour Réglages.
- Nettoyage du code mort associé : _compact_tag_editor, constantes
_PRESERVE_TERMS/_MASK_TERMS/_STOPWORDS, textes d'aide qui référençaient
l'onglet Profils.
Chemin utilisateur : Administration > Réglages > Ouvrir le tableau des
termes. 247 tests unit OK (0 régression), self-test OK. Préserve a9e8b2c
(thème, bêta, aide ?, fenêtre tableau). Aucun build/push sans GO Dom.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -22,7 +22,6 @@ from manual_masking import ensure_mask_templates_dir, list_mask_templates, mask_
|
|||||||
|
|
||||||
_SUBTABS = [
|
_SUBTABS = [
|
||||||
("reg", "⚙️ Réglages"),
|
("reg", "⚙️ Réglages"),
|
||||||
("pro", "👤 Profils"),
|
|
||||||
("msk", "🎭 Masquage"),
|
("msk", "🎭 Masquage"),
|
||||||
("shr", "🔄 Partage"),
|
("shr", "🔄 Partage"),
|
||||||
("rul", "🛡️ Règles"),
|
("rul", "🛡️ Règles"),
|
||||||
@@ -57,10 +56,6 @@ _MASK_COLORS = [
|
|||||||
("Bleu marine", "#1e3a5f"),
|
("Bleu marine", "#1e3a5f"),
|
||||||
]
|
]
|
||||||
|
|
||||||
_PRESERVE_TERMS = ["FUROSEMIDE", "rééducation fonctionnelle", "classification internationale"]
|
|
||||||
_MASK_TERMS = ["CHUXX"]
|
|
||||||
_STOPWORDS = ["hospitalisation", "contrôle", "prescription"]
|
|
||||||
|
|
||||||
MANUAL_MASK_NONE_LABEL = "Aucun masque manuel"
|
MANUAL_MASK_NONE_LABEL = "Aucun masque manuel"
|
||||||
|
|
||||||
# Textes d'aide « ? » (français simple, pour utilisateurs non informaticiens).
|
# Textes d'aide « ? » (français simple, pour utilisateurs non informaticiens).
|
||||||
@@ -101,7 +96,7 @@ _HELP_PROFIL = (
|
|||||||
"(ex. : interne standard, diffusion prudente, recherche…).\n\n"
|
"(ex. : interne standard, diffusion prudente, recherche…).\n\n"
|
||||||
"Il définit les moteurs utilisés, les données détectées, les termes à conserver "
|
"Il définit les moteurs utilisés, les données détectées, les termes à conserver "
|
||||||
"ou à masquer, et si un masque manuel est requis.\n\n"
|
"ou à masquer, et si un masque manuel est requis.\n\n"
|
||||||
"Choisissez un profil ici, et consultez son détail dans l'onglet « Profils »."
|
"Choisissez un profil ici ; ses termes sont consultables via « Ouvrir le tableau des termes »."
|
||||||
)
|
)
|
||||||
_HELP_MOTEURS = (
|
_HELP_MOTEURS = (
|
||||||
"Les moteurs détectent les données personnelles.\n\n"
|
"Les moteurs détectent les données personnelles.\n\n"
|
||||||
@@ -116,7 +111,7 @@ _HELP_LISTES = (
|
|||||||
"• À conserver : termes à ne jamais masquer (vocabulaire métier).\n"
|
"• À conserver : termes à ne jamais masquer (vocabulaire métier).\n"
|
||||||
"• À masquer : termes à toujours masquer (sigles, en-têtes…).\n"
|
"• À masquer : termes à toujours masquer (sigles, en-têtes…).\n"
|
||||||
"• À ignorer : mots à ne pas considérer.\n\n"
|
"• À ignorer : mots à ne pas considérer.\n\n"
|
||||||
"Pour une liste longue, ouvrez le tableau des termes (onglet « Profils ») : "
|
"Pour une liste longue, ouvrez le tableau des termes : "
|
||||||
"il reste lisible et permet la recherche."
|
"il reste lisible et permet la recherche."
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -226,7 +221,6 @@ class ConfigTab(ctk.CTkFrame):
|
|||||||
|
|
||||||
builders = {
|
builders = {
|
||||||
"reg": self._build_reglages,
|
"reg": self._build_reglages,
|
||||||
"pro": self._build_profils,
|
|
||||||
"msk": self._build_masquage,
|
"msk": self._build_masquage,
|
||||||
"shr": self._build_partage,
|
"shr": self._build_partage,
|
||||||
"rul": self._build_regles,
|
"rul": self._build_regles,
|
||||||
@@ -294,9 +288,6 @@ class ConfigTab(ctk.CTkFrame):
|
|||||||
self._profile_menu.set(current)
|
self._profile_menu.set(current)
|
||||||
self._profile_menu.pack(side="left", pady=10)
|
self._profile_menu.pack(side="left", pady=10)
|
||||||
ui_kit.help_button(top, p, _HELP_PROFIL, title="Profil d'anonymisation").pack(side="left", padx=(6, 0), pady=10)
|
ui_kit.help_button(top, p, _HELP_PROFIL, title="Profil d'anonymisation").pack(side="left", padx=(6, 0), pady=10)
|
||||||
ui_kit.secondary_button(top, p, "👤 Voir le profil", command=lambda: self._show_sub("pro")).pack(
|
|
||||||
side="left", padx=(10, 4), pady=10
|
|
||||||
)
|
|
||||||
|
|
||||||
sortie = ui_kit.secondary_button(top, p, "📁 Dossier de sortie…", command=self._pick_output)
|
sortie = ui_kit.secondary_button(top, p, "📁 Dossier de sortie…", command=self._pick_output)
|
||||||
sortie.pack(side="left", padx=(6, 6), pady=10)
|
sortie.pack(side="left", padx=(6, 6), pady=10)
|
||||||
@@ -383,24 +374,32 @@ class ConfigTab(ctk.CTkFrame):
|
|||||||
terms_help, text="Termes propres à votre établissement", text_color=p["text_muted"], font=ui_kit.font(11), anchor="w"
|
terms_help, text="Termes propres à votre établissement", text_color=p["text_muted"], font=ui_kit.font(11), anchor="w"
|
||||||
).pack(side="left")
|
).pack(side="left")
|
||||||
ui_kit.help_button(terms_help, p, _HELP_LISTES, title="Listes locales").pack(side="right")
|
ui_kit.help_button(terms_help, p, _HELP_LISTES, title="Listes locales").pack(side="right")
|
||||||
self._compact_tag_editor(terms, "Termes à conserver", "Ex : FUROSEMIDE", _PRESERVE_TERMS, "keep")
|
ctk.CTkLabel(
|
||||||
self._compact_tag_editor(terms, "Termes à masquer", "Ex : CHUXX", _MASK_TERMS, "mask")
|
|
||||||
self._compact_tag_editor(terms, "Mots à ignorer", "Ex : prescription", _STOPWORDS, "stop")
|
|
||||||
ctk.CTkButton(
|
|
||||||
terms,
|
terms,
|
||||||
text="📋 Ouvrir le tableau des termes",
|
text="Les termes du profil actif sont consultables dans un tableau dédié.",
|
||||||
command=lambda: self._show_sub("pro"),
|
text_color=p["text_dim"],
|
||||||
fg_color=p["btn_sec_bg"],
|
|
||||||
hover_color=p["card_border"],
|
|
||||||
text_color=p["text"],
|
|
||||||
border_color=p["btn_sec_border"],
|
|
||||||
border_width=1,
|
|
||||||
corner_radius=8,
|
|
||||||
height=30,
|
|
||||||
font=ui_kit.font(12),
|
font=ui_kit.font(12),
|
||||||
).pack(fill="x", padx=12, pady=(6, 12))
|
justify="left",
|
||||||
|
wraplength=240,
|
||||||
|
anchor="w",
|
||||||
|
).pack(fill="x", padx=12, pady=(2, 6))
|
||||||
|
counts = self._active_profile_summary().list_counts
|
||||||
|
chips = ctk.CTkFrame(terms, fg_color="transparent")
|
||||||
|
chips.pack(fill="x", padx=12, pady=(0, 8))
|
||||||
|
for label, count in counts.items():
|
||||||
|
ctk.CTkLabel(
|
||||||
|
chips,
|
||||||
|
text=f"{label} : {count}",
|
||||||
|
text_color=p["text"],
|
||||||
|
fg_color=p["divider"],
|
||||||
|
corner_radius=8,
|
||||||
|
font=ui_kit.font(11, "bold"),
|
||||||
|
).pack(side="left", padx=(0, 6), ipadx=7, ipady=2)
|
||||||
|
ui_kit.primary_button(
|
||||||
|
terms, p, "📋 Ouvrir le tableau des termes", command=self._open_terms_table
|
||||||
|
).pack(fill="x", padx=12, pady=(2, 12))
|
||||||
|
|
||||||
# -- Profils ----------------------------------------------------------
|
# -- Profil actif / tableau des termes --------------------------------
|
||||||
|
|
||||||
def _active_profile_dict(self) -> dict:
|
def _active_profile_dict(self) -> dict:
|
||||||
try:
|
try:
|
||||||
@@ -427,66 +426,6 @@ class ConfigTab(ctk.CTkFrame):
|
|||||||
rows = profile_term_rows(self._active_profile_dict())
|
rows = profile_term_rows(self._active_profile_dict())
|
||||||
TermsTableWindow(self.winfo_toplevel(), self._p, rows, profile_label=summary.label)
|
TermsTableWindow(self.winfo_toplevel(), self._p, rows, profile_label=summary.label)
|
||||||
|
|
||||||
def _rebuild_profils(self) -> None:
|
|
||||||
panel = self._panels.get("pro")
|
|
||||||
if panel is None:
|
|
||||||
return
|
|
||||||
for child in panel.winfo_children():
|
|
||||||
child.destroy()
|
|
||||||
self._build_profils(panel)
|
|
||||||
|
|
||||||
def _build_profils(self, parent) -> None:
|
|
||||||
p = self._p
|
|
||||||
self._section_intro(
|
|
||||||
parent,
|
|
||||||
"Un profil regroupe tous les réglages d'anonymisation. Voici le profil actif.",
|
|
||||||
_HELP_PROFIL,
|
|
||||||
"Création / modification d'un profil d'anonymisation",
|
|
||||||
)
|
|
||||||
summary = self._active_profile_summary()
|
|
||||||
|
|
||||||
card = ui_kit.Card(parent, p, title=f"👤 {summary.label}")
|
|
||||||
card.pack(fill="x", pady=(0, 8))
|
|
||||||
if summary.description:
|
|
||||||
self._note(card, summary.description)
|
|
||||||
grid = ctk.CTkFrame(card, fg_color="transparent")
|
|
||||||
grid.pack(fill="x", padx=12, pady=(0, 10))
|
|
||||||
infos = [
|
|
||||||
("Masque manuel requis", "Oui" if summary.require_manual_mask else "Non"),
|
|
||||||
("Template de masque", summary.mask_template or "—"),
|
|
||||||
("Moteur VLM (images)", "désactivé" if summary.disable_vlm else "selon réglages"),
|
|
||||||
]
|
|
||||||
for idx, (key, val) in enumerate(infos):
|
|
||||||
ctk.CTkLabel(grid, text=key, text_color=p["text_muted"], font=ui_kit.font(11), anchor="w").grid(
|
|
||||||
row=idx, column=0, sticky="w", pady=1
|
|
||||||
)
|
|
||||||
ctk.CTkLabel(grid, text=val, text_color=p["text"], font=ui_kit.font(11, "bold"), anchor="w").grid(
|
|
||||||
row=idx, column=1, sticky="w", padx=(12, 0), pady=1
|
|
||||||
)
|
|
||||||
grid.grid_columnconfigure(1, weight=1)
|
|
||||||
|
|
||||||
lists_card = ui_kit.Card(parent, p, title="✅ Listes locales du profil")
|
|
||||||
lists_card.pack(fill="x", pady=(0, 8))
|
|
||||||
chips = ctk.CTkFrame(lists_card, fg_color="transparent")
|
|
||||||
chips.pack(fill="x", padx=12, pady=(0, 8))
|
|
||||||
for label, count in summary.list_counts.items():
|
|
||||||
ctk.CTkLabel(
|
|
||||||
chips,
|
|
||||||
text=f"{label} : {count}",
|
|
||||||
text_color=p["text"],
|
|
||||||
fg_color=p["divider"],
|
|
||||||
corner_radius=8,
|
|
||||||
font=ui_kit.font(11, "bold"),
|
|
||||||
).pack(side="left", padx=(0, 8), ipadx=8, ipady=3)
|
|
||||||
ui_kit.primary_button(lists_card, p, "📋 Ouvrir le tableau des termes", command=self._open_terms_table).pack(
|
|
||||||
anchor="w", padx=12, pady=(0, 12)
|
|
||||||
)
|
|
||||||
|
|
||||||
create = ui_kit.Card(parent, p, title="🧩 Créer / modifier un profil")
|
|
||||||
create.pack(fill="x")
|
|
||||||
self._note(create, "La création et la modification de profils seront disponibles prochainement.")
|
|
||||||
self._mockup_button(create, "+ Nouveau profil").pack(anchor="w", padx=12, pady=(0, 12))
|
|
||||||
|
|
||||||
# -- Masquage ---------------------------------------------------------
|
# -- Masquage ---------------------------------------------------------
|
||||||
|
|
||||||
def _build_masquage(self, parent) -> None:
|
def _build_masquage(self, parent) -> None:
|
||||||
@@ -691,7 +630,6 @@ 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
|
||||||
self._rebuild_profils()
|
|
||||||
|
|
||||||
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()
|
||||||
@@ -883,38 +821,6 @@ class ConfigTab(ctk.CTkFrame):
|
|||||||
row.get = lambda: bool(var.get()) # type: ignore[attr-defined]
|
row.get = lambda: bool(var.get()) # type: ignore[attr-defined]
|
||||||
return row
|
return row
|
||||||
|
|
||||||
def _compact_tag_editor(self, parent, title: str, placeholder: str, terms: list[str], kind: str) -> None:
|
|
||||||
p = self._p
|
|
||||||
color = {"keep": p["success"], "mask": p["primary"], "stop": p["warning"]}.get(kind, p["primary"])
|
|
||||||
ctk.CTkLabel(parent, text=title, text_color=p["text"], font=ui_kit.font(12, "bold"), anchor="w").pack(
|
|
||||||
fill="x", padx=12, pady=(0, 2)
|
|
||||||
)
|
|
||||||
row = ctk.CTkFrame(parent, fg_color="transparent")
|
|
||||||
row.pack(fill="x", padx=12, pady=(0, 5))
|
|
||||||
ctk.CTkEntry(
|
|
||||||
row,
|
|
||||||
placeholder_text=placeholder,
|
|
||||||
fg_color=p["btn_sec_bg"],
|
|
||||||
border_color=p["btn_sec_border"],
|
|
||||||
text_color=p["text"],
|
|
||||||
height=28,
|
|
||||||
).pack(side="left", fill="x", expand=True, padx=(0, 6))
|
|
||||||
ui_kit.secondary_button(row, p, "+").pack(side="right")
|
|
||||||
cloud = ctk.CTkFrame(parent, fg_color="transparent")
|
|
||||||
cloud.pack(fill="x", padx=12, pady=(0, 8))
|
|
||||||
for term in terms[:2]:
|
|
||||||
display = f"{term[:18]}{'…' if len(term) > 18 else ''} ×"
|
|
||||||
ctk.CTkLabel(
|
|
||||||
cloud,
|
|
||||||
text=display,
|
|
||||||
width=150,
|
|
||||||
anchor="w",
|
|
||||||
text_color=color,
|
|
||||||
fg_color=p["btn_sec_bg"],
|
|
||||||
corner_radius=99,
|
|
||||||
font=ui_kit.font(10),
|
|
||||||
).pack(anchor="w", fill="x", pady=2, ipadx=5, ipady=2)
|
|
||||||
|
|
||||||
def _slider_row(self, parent, label: str, variable: ctk.IntVar, command) -> None:
|
def _slider_row(self, parent, label: str, variable: ctk.IntVar, command) -> None:
|
||||||
p = self._p
|
p = self._p
|
||||||
row = ctk.CTkFrame(parent, fg_color="transparent")
|
row = ctk.CTkFrame(parent, fg_color="transparent")
|
||||||
|
|||||||
@@ -118,13 +118,14 @@ def test_attach_tooltip_does_not_break_widget(ctk_root):
|
|||||||
assert lbl.winfo_exists()
|
assert lbl.winfo_exists()
|
||||||
|
|
||||||
|
|
||||||
def test_subtabs_include_profils():
|
def test_subtabs_no_profils_subtab():
|
||||||
|
"""Retour Dom : le sous-onglet Profils (doublon non câblé) est retiré."""
|
||||||
from gui_v6.tabs.tab_config import _SUBTABS
|
from gui_v6.tabs.tab_config import _SUBTABS
|
||||||
|
|
||||||
keys = [k for k, _ in _SUBTABS]
|
keys = [k for k, _ in _SUBTABS]
|
||||||
labels = [lbl for _, lbl in _SUBTABS]
|
labels = [lbl for _, lbl in _SUBTABS]
|
||||||
assert "pro" in keys
|
assert "pro" not in keys
|
||||||
assert any("Profils" in lbl for lbl in labels)
|
assert not any("Profils" in lbl for lbl in labels)
|
||||||
|
|
||||||
|
|
||||||
def _all_texts(widget):
|
def _all_texts(widget):
|
||||||
@@ -150,11 +151,16 @@ def test_reglages_labels_renamed_and_profile_readable(ctk_root, tmp_path, monkey
|
|||||||
assert "Profil métier" not in texts
|
assert "Profil métier" not in texts
|
||||||
assert "Dossier de sortie" in texts # addendum : « Sortie… » clarifié
|
assert "Dossier de sortie" in texts # addendum : « Sortie… » clarifié
|
||||||
|
|
||||||
|
# retour Dom : accès direct au tableau depuis Réglages, plus de pastilles inline
|
||||||
|
assert "Ouvrir le tableau des termes" in texts
|
||||||
|
assert "Voir le profil" not in texts
|
||||||
|
assert "FUROSEMIDE" not in texts # plus de pastilles de termes exemple inline
|
||||||
|
|
||||||
# profil lisible : résumé avec les 3 listes
|
# profil lisible : résumé avec les 3 listes
|
||||||
summary = tab._active_profile_summary()
|
summary = tab._active_profile_summary()
|
||||||
assert set(summary.list_counts.keys()) == {"À conserver", "À masquer", "À ignorer"}
|
assert set(summary.list_counts.keys()) == {"À conserver", "À masquer", "À ignorer"}
|
||||||
|
|
||||||
# tableau des termes ouvrable sans erreur
|
# tableau des termes ouvrable DIRECTEMENT depuis Réglages (sans onglet Profils)
|
||||||
tab._open_terms_table()
|
tab._open_terms_table()
|
||||||
tab.update_idletasks()
|
tab.update_idletasks()
|
||||||
tab.destroy()
|
tab.destroy()
|
||||||
|
|||||||
Reference in New Issue
Block a user