refactor(gui): intégrer le Masquage dans Administration > Profils
Retour Dom : le sous-onglet Masquage séparé créait de la confusion. Le masquage fait partie de la manière d'anonymiser associée au profil. - Retrait du sous-onglet « Administration > Masquage » (_SUBTABS, builder, méthode _build_masquage). - Section « Profils > Masquage » enrichie : masque manuel requis, template de masque (lié au profil édité), bouton « Ouvrir l'éditeur de masque » (fenêtre dédiée) + dossier des templates, et apparence du masque (couleur, style des marqueurs + aperçu, marges H/V, coins arrondis). - Le template enregistré depuis l'éditeur remplit désormais le champ Template du profil (preferred_manual_mask_template via _pro_template_var). - Profils devient le centre des réglages métier (général/masquage/mots/ moteurs/règles). Réglages inchangé (pas de pastilles, pas de grosse refonte). Nettoyage du code mort (_REPLACEMENT_CODES, _HELP_MASQUAGE). 261 tests unit OK (0 régression), self-test OK, nav 4 sous-onglets + éditeur de masque depuis Profils + thème OK. Préserve 72841ed/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:
@@ -23,7 +23,6 @@ from manual_masking import ensure_mask_templates_dir, list_mask_templates, mask_
|
||||
_SUBTABS = [
|
||||
("reg", "⚙️ Réglages"),
|
||||
("pro", "👤 Profils"),
|
||||
("msk", "🎭 Masquage"),
|
||||
("shr", "🔄 Partage"),
|
||||
("rul", "🛡️ Règles"),
|
||||
]
|
||||
@@ -38,17 +37,6 @@ _DETECTION_OPTIONS = [
|
||||
("N° adhérent mutuelle", "Identifiant local"),
|
||||
]
|
||||
|
||||
_REPLACEMENT_CODES = [
|
||||
("Nom/Prénom", "[NOM]"),
|
||||
("Date naissance", "[DATE_NAISSANCE]"),
|
||||
("Établissement", "[ETABLISSEMENT]"),
|
||||
("Adresse", "[ADRESSE]"),
|
||||
("Téléphone", "[TEL]"),
|
||||
("N° sécu", "[NIR]"),
|
||||
("IPP", "[IPP]"),
|
||||
("Email", "[EMAIL]"),
|
||||
]
|
||||
|
||||
_MASK_COLORS = [
|
||||
("Noir", "#000000"),
|
||||
("Bleu nuit", "#1a1a2e"),
|
||||
@@ -68,14 +56,6 @@ _HELP_REGLAGES = (
|
||||
"• Listes locales : vos termes à toujours masquer ou toujours conserver.\n\n"
|
||||
"Tout fonctionne 100 % en local sur ce poste. Aucun document patient n'est envoyé sur Internet."
|
||||
)
|
||||
_HELP_MASQUAGE = (
|
||||
"Masquage des documents.\n\n"
|
||||
"• Couleur, style et marges du masque appliqué sur les PDF.\n"
|
||||
"• Codes de remplacement affichés à la place des données ([NOM], [DATE_NAISSANCE]…).\n"
|
||||
"• Masques de zones fixes : ouvrez l'éditeur pour dessiner les zones à masquer "
|
||||
"(en-têtes, blocs identité) directement sur un PDF modèle, puis enregistrez un modèle réutilisable.\n\n"
|
||||
"Le traitement reste local ; rien n'est envoyé sur Internet."
|
||||
)
|
||||
_HELP_PARTAGE = (
|
||||
"À quoi sert le Partage ?\n\n"
|
||||
"Il permet d'échanger les RÉGLAGES de l'application (listes de termes, règles, "
|
||||
@@ -229,7 +209,6 @@ class ConfigTab(ctk.CTkFrame):
|
||||
builders = {
|
||||
"reg": self._build_reglages,
|
||||
"pro": self._build_profils,
|
||||
"msk": self._build_masquage,
|
||||
"shr": self._build_partage,
|
||||
"rul": self._build_regles,
|
||||
}
|
||||
@@ -496,7 +475,39 @@ class ConfigTab(ctk.CTkFrame):
|
||||
ctk.CTkLabel(mask, text="Template de masque préféré", text_color=p["text_muted"], font=ui_kit.font(11), anchor="w").pack(fill="x", padx=12, pady=(0, 2))
|
||||
self._pro_template_var = ctk.StringVar()
|
||||
self._pro_template_entry = ctk.CTkEntry(mask, textvariable=self._pro_template_var, fg_color=p["btn_sec_bg"], border_color=p["btn_sec_border"], text_color=p["text"], height=30)
|
||||
self._pro_template_entry.pack(fill="x", padx=12, pady=(0, 12))
|
||||
self._pro_template_entry.pack(fill="x", padx=12, pady=(0, 6))
|
||||
mask_actions = ctk.CTkFrame(mask, fg_color="transparent")
|
||||
mask_actions.pack(fill="x", padx=12, pady=(0, 6))
|
||||
ui_kit.secondary_button(mask_actions, p, "🖊 Ouvrir l'éditeur de masque", command=self._open_full_mask_editor).pack(side="left")
|
||||
ui_kit.secondary_button(mask_actions, p, "📁 Dossier", command=self._open_templates_dir).pack(side="left", padx=(6, 0))
|
||||
|
||||
# Apparence du masque (couleur / style / marges) — réglage global appliqué aux PDF.
|
||||
ctk.CTkLabel(mask, text="Apparence du masque", text_color=p["text_muted"], font=ui_kit.font(11, "bold"), anchor="w").pack(fill="x", padx=12, pady=(6, 2))
|
||||
swatches = ctk.CTkFrame(mask, fg_color="transparent")
|
||||
swatches.pack(fill="x", padx=12, pady=(0, 6))
|
||||
self._swatch_buttons: dict[str, ctk.CTkButton] = {}
|
||||
for _label, color_value in _MASK_COLORS:
|
||||
btn = ctk.CTkButton(
|
||||
swatches, text="", width=28, height=24, fg_color=color_value, hover_color=color_value,
|
||||
border_color=p["primary"] if color_value == self._mask_color else p["card_border"],
|
||||
border_width=3 if color_value == self._mask_color else 1, corner_radius=6,
|
||||
command=lambda c=color_value: self._set_mask_color(c),
|
||||
)
|
||||
btn.pack(side="left", padx=(0, 6))
|
||||
self._swatch_buttons[color_value] = btn
|
||||
style_row = ctk.CTkFrame(mask, fg_color="transparent")
|
||||
style_row.pack(fill="x", padx=12, pady=(0, 6))
|
||||
for _label, value, preview in [("Crochets", "brackets", "[NOM]"), ("Étoiles", "stars", "***"), ("Noirci", "blackout", "████")]:
|
||||
ctk.CTkRadioButton(
|
||||
style_row, text=preview, variable=self._mask_style_var, value=value, command=self._update_mask_preview,
|
||||
text_color=p["text"], fg_color=p["primary"], hover_color=p["primary_dim"], font=ui_kit.font(11),
|
||||
).pack(side="left", padx=(0, 10))
|
||||
self._slider_row(mask, "Marge H", self._mask_margin_x_var, self._on_mask_margin_x)
|
||||
self._slider_row(mask, "Marge V", self._mask_margin_y_var, self._on_mask_margin_y)
|
||||
self._mini_toggle(
|
||||
mask, "Coins arrondis", "", value=self._state.mask_rounded_corners,
|
||||
variable=self._mask_rounded_var, command=self._on_rounded_corners,
|
||||
).pack(fill="x", padx=12, pady=(2, 12))
|
||||
|
||||
eng = ui_kit.Card(left, p, title="🧠 Moteurs")
|
||||
eng.pack(fill="x")
|
||||
@@ -683,124 +694,6 @@ class ConfigTab(ctk.CTkFrame):
|
||||
|
||||
# -- Masquage ---------------------------------------------------------
|
||||
|
||||
def _build_masquage(self, parent) -> None:
|
||||
p = self._p
|
||||
self._section_intro(
|
||||
parent,
|
||||
"Apparence des masques et éditeur de zones fixes à masquer sur vos PDF.",
|
||||
_HELP_MASQUAGE,
|
||||
"Le Masquage",
|
||||
)
|
||||
top_cols = self._columns(parent, 3, gap=8, height=300)
|
||||
|
||||
pdf_opts = ui_kit.Card(top_cols[0], p, title="⬛ PDF")
|
||||
pdf_opts.pack(fill="both", expand=True)
|
||||
ctk.CTkLabel(
|
||||
pdf_opts, text="Couleur de masquage", text_color=p["text"], font=ui_kit.font(12, "bold")
|
||||
).pack(anchor="w", padx=12, pady=(0, 4))
|
||||
swatches = ctk.CTkFrame(pdf_opts, fg_color="transparent")
|
||||
swatches.pack(fill="x", padx=12, pady=(0, 8))
|
||||
self._swatch_buttons: dict[str, ctk.CTkButton] = {}
|
||||
for label, color_value in _MASK_COLORS:
|
||||
btn = ctk.CTkButton(
|
||||
swatches,
|
||||
text="",
|
||||
width=30,
|
||||
height=26,
|
||||
fg_color=color_value,
|
||||
hover_color=color_value,
|
||||
border_color=p["primary"] if color_value == self._mask_color else p["card_border"],
|
||||
border_width=3 if color_value == self._mask_color else 1,
|
||||
corner_radius=6,
|
||||
command=lambda c=color_value: self._set_mask_color(c),
|
||||
)
|
||||
btn.pack(side="left", padx=(0, 6))
|
||||
self._swatch_buttons[color_value] = btn
|
||||
|
||||
self._slider_row(pdf_opts, "Marge H", self._mask_margin_x_var, self._on_mask_margin_x)
|
||||
self._slider_row(pdf_opts, "Marge V", self._mask_margin_y_var, self._on_mask_margin_y)
|
||||
self._mini_toggle(
|
||||
pdf_opts,
|
||||
"Coins arrondis",
|
||||
"",
|
||||
value=self._state.mask_rounded_corners,
|
||||
variable=self._mask_rounded_var,
|
||||
command=self._on_rounded_corners,
|
||||
).pack(fill="x", padx=12, pady=(4, 10))
|
||||
|
||||
text_opts = ui_kit.Card(top_cols[1], p, title="🏷️ Texte")
|
||||
text_opts.pack(fill="both", expand=True)
|
||||
for label, value, preview in [
|
||||
("Crochets", "brackets", "[NOM]"),
|
||||
("Étoiles", "stars", "***"),
|
||||
("Noirci", "blackout", "████"),
|
||||
]:
|
||||
ctk.CTkRadioButton(
|
||||
text_opts,
|
||||
text=f"{label} {preview}",
|
||||
variable=self._mask_style_var,
|
||||
value=value,
|
||||
command=self._update_mask_preview,
|
||||
text_color=p["text"],
|
||||
fg_color=p["primary"],
|
||||
hover_color=p["primary_dim"],
|
||||
font=ui_kit.font(12),
|
||||
).pack(anchor="w", padx=12, pady=2)
|
||||
self._mask_preview = ctk.CTkLabel(
|
||||
text_opts,
|
||||
text="Patient [NOM], né le [DATE_NAISSANCE]",
|
||||
text_color=p["text_dim"],
|
||||
fg_color=p["divider"],
|
||||
corner_radius=6,
|
||||
font=ui_kit.font(12),
|
||||
anchor="w",
|
||||
)
|
||||
self._mask_preview.pack(fill="x", padx=12, pady=(8, 10), ipady=7)
|
||||
|
||||
codes = ui_kit.Card(top_cols[2], p, title="🔒 Codes")
|
||||
codes.pack(fill="both", expand=True)
|
||||
grid = ctk.CTkFrame(codes, fg_color="transparent")
|
||||
grid.pack(fill="both", expand=True, padx=12, pady=(0, 10))
|
||||
for idx, (label, code) in enumerate(_REPLACEMENT_CODES):
|
||||
ctk.CTkLabel(grid, text=label, 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=code, text_color=p["primary"], font=ui_kit.font(11, "bold"), anchor="w").grid(
|
||||
row=idx, column=1, sticky="w", padx=(8, 0), pady=1
|
||||
)
|
||||
grid.grid_columnconfigure(0, weight=1)
|
||||
|
||||
editor = ui_kit.Card(parent, p, title="🏠 Masques de zones fixes")
|
||||
editor.pack(fill="x", pady=(8, 0))
|
||||
ctk.CTkLabel(
|
||||
editor,
|
||||
text=(
|
||||
"Définissez les zones à masquer (en-têtes, blocs identité…) directement sur "
|
||||
"votre PDF, dans une fenêtre dédiée où le document est affiché en grand et "
|
||||
"défilable (scroll, zoom, ajuster largeur/page). Les templates enregistrés "
|
||||
"apparaissent ensuite dans « Template de masque manuel » (onglet Réglages)."
|
||||
),
|
||||
text_color=p["text_muted"],
|
||||
font=ui_kit.font(12),
|
||||
justify="left",
|
||||
wraplength=760,
|
||||
anchor="w",
|
||||
).pack(fill="x", padx=14, pady=(0, 10))
|
||||
actions = ctk.CTkFrame(editor, fg_color="transparent")
|
||||
actions.pack(fill="x", padx=14, pady=(0, 6))
|
||||
ui_kit.primary_button(
|
||||
actions, p, "🖊 Ouvrir l'éditeur de masques", command=self._open_full_mask_editor
|
||||
).pack(side="left")
|
||||
ui_kit.secondary_button(
|
||||
actions, p, "📁 Dossier des templates", command=self._open_templates_dir
|
||||
).pack(side="left", padx=(8, 0))
|
||||
ctk.CTkLabel(
|
||||
editor,
|
||||
textvariable=self._mask_status_text,
|
||||
text_color=p["text_muted"],
|
||||
font=ui_kit.font(11),
|
||||
anchor="w",
|
||||
).pack(fill="x", padx=14, pady=(2, 12))
|
||||
|
||||
# -- Partage / Règles -------------------------------------------------
|
||||
|
||||
@@ -999,13 +892,22 @@ class ConfigTab(ctk.CTkFrame):
|
||||
messagebox.showerror("Masques PDF", f"Impossible d'ouvrir l'éditeur : {exc}")
|
||||
|
||||
def _on_mask_template_saved(self, path: Path) -> None:
|
||||
"""Callback déclenché par la fenêtre dédiée après sauvegarde d'un template."""
|
||||
"""Callback déclenché par la fenêtre dédiée après sauvegarde d'un template.
|
||||
|
||||
Lie le template au profil en cours d'édition (`preferred_manual_mask_template`).
|
||||
"""
|
||||
self._refresh_manual_mask_templates()
|
||||
try:
|
||||
self._manual_mask_var.set(mask_template_label(path, _app_base_dir()))
|
||||
self._state.manual_mask_template = path
|
||||
except Exception:
|
||||
pass
|
||||
# Renseigne le champ Template du profil édité (section Profils > Masquage).
|
||||
if hasattr(self, "_pro_template_var"):
|
||||
try:
|
||||
self._pro_template_var.set(str(path))
|
||||
except Exception:
|
||||
pass
|
||||
self._mask_status_text.set(f"Template enregistré : {path.name}")
|
||||
|
||||
|
||||
|
||||
@@ -251,3 +251,25 @@ def test_usage_tab_finish_calls_reporter(ctk_root):
|
||||
assert called.wait(timeout=3.0) # reporter appelé en thread daemon
|
||||
assert captured["summary"] is summary
|
||||
tab.destroy()
|
||||
|
||||
|
||||
def test_masquage_moved_into_profils(ctk_root, tmp_path, monkeypatch):
|
||||
"""Le sous-onglet Masquage est retiré ; son contenu utile est dans Profils."""
|
||||
from gui_v6.tabs import tab_config
|
||||
|
||||
keys = [k for k, _ in tab_config._SUBTABS]
|
||||
assert "msk" not in keys # plus de sous-onglet Masquage 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()
|
||||
|
||||
# apparence du masque relocalisée dans la section Profils > Masquage
|
||||
assert getattr(tab, "_swatch_buttons", None)
|
||||
# un template enregistré depuis l'éditeur remplit le champ Template du profil
|
||||
saved = tmp_path / "config" / "mask_templates" / "depuis_editeur.json"
|
||||
saved.parent.mkdir(parents=True, exist_ok=True)
|
||||
tab._on_mask_template_saved(saved)
|
||||
assert tab._pro_template_var.get().endswith("depuis_editeur.json")
|
||||
tab.destroy()
|
||||
|
||||
Reference in New Issue
Block a user