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 = [
|
_SUBTABS = [
|
||||||
("reg", "⚙️ Réglages"),
|
("reg", "⚙️ Réglages"),
|
||||||
("pro", "👤 Profils"),
|
("pro", "👤 Profils"),
|
||||||
("msk", "🎭 Masquage"),
|
|
||||||
("shr", "🔄 Partage"),
|
("shr", "🔄 Partage"),
|
||||||
("rul", "🛡️ Règles"),
|
("rul", "🛡️ Règles"),
|
||||||
]
|
]
|
||||||
@@ -38,17 +37,6 @@ _DETECTION_OPTIONS = [
|
|||||||
("N° adhérent mutuelle", "Identifiant local"),
|
("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 = [
|
_MASK_COLORS = [
|
||||||
("Noir", "#000000"),
|
("Noir", "#000000"),
|
||||||
("Bleu nuit", "#1a1a2e"),
|
("Bleu nuit", "#1a1a2e"),
|
||||||
@@ -68,14 +56,6 @@ _HELP_REGLAGES = (
|
|||||||
"• Listes locales : vos termes à toujours masquer ou toujours conserver.\n\n"
|
"• 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."
|
"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 = (
|
_HELP_PARTAGE = (
|
||||||
"À quoi sert le Partage ?\n\n"
|
"À quoi sert le Partage ?\n\n"
|
||||||
"Il permet d'échanger les RÉGLAGES de l'application (listes de termes, règles, "
|
"Il permet d'échanger les RÉGLAGES de l'application (listes de termes, règles, "
|
||||||
@@ -229,7 +209,6 @@ class ConfigTab(ctk.CTkFrame):
|
|||||||
builders = {
|
builders = {
|
||||||
"reg": self._build_reglages,
|
"reg": self._build_reglages,
|
||||||
"pro": self._build_profils,
|
"pro": self._build_profils,
|
||||||
"msk": self._build_masquage,
|
|
||||||
"shr": self._build_partage,
|
"shr": self._build_partage,
|
||||||
"rul": self._build_regles,
|
"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))
|
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_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 = 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 = ui_kit.Card(left, p, title="🧠 Moteurs")
|
||||||
eng.pack(fill="x")
|
eng.pack(fill="x")
|
||||||
@@ -683,124 +694,6 @@ class ConfigTab(ctk.CTkFrame):
|
|||||||
|
|
||||||
# -- Masquage ---------------------------------------------------------
|
# -- 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 -------------------------------------------------
|
# -- Partage / Règles -------------------------------------------------
|
||||||
|
|
||||||
@@ -999,13 +892,22 @@ class ConfigTab(ctk.CTkFrame):
|
|||||||
messagebox.showerror("Masques PDF", f"Impossible d'ouvrir l'éditeur : {exc}")
|
messagebox.showerror("Masques PDF", f"Impossible d'ouvrir l'éditeur : {exc}")
|
||||||
|
|
||||||
def _on_mask_template_saved(self, path: Path) -> None:
|
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()
|
self._refresh_manual_mask_templates()
|
||||||
try:
|
try:
|
||||||
self._manual_mask_var.set(mask_template_label(path, _app_base_dir()))
|
self._manual_mask_var.set(mask_template_label(path, _app_base_dir()))
|
||||||
self._state.manual_mask_template = path
|
self._state.manual_mask_template = path
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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}")
|
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 called.wait(timeout=3.0) # reporter appelé en thread daemon
|
||||||
assert captured["summary"] is summary
|
assert captured["summary"] is summary
|
||||||
tab.destroy()
|
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