feat(gui): recâbler import/export de configuration par email (P1-3)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-29 19:20:42 +02:00
parent 1d65d42430
commit d3189d5bb7
4 changed files with 225 additions and 9 deletions

View File

@@ -170,12 +170,15 @@ _HELP_PROFIL_MOTS = (
"Pour de longues listes, utilisez le tableau des termes afin de rechercher et vérifier plus facilement."
)
_HELP_EXPORT_CONFIG = (
"Exporte uniquement les réglages de l'application : profils, listes locales, règles et style de masque.\n\n"
"Exporte vos deux listes locales — termes à toujours conserver et termes à toujours "
"masquer — dans un fichier .json à envoyer à un autre poste ou à l'administrateur.\n\n"
"Les documents patients, résultats d'anonymisation et audits ne sont pas exportés."
)
_HELP_IMPORT_CONFIG = (
"Importe des réglages reçus d'un administrateur ou d'un autre poste.\n\n"
"L'import ne lit pas de documents patients. Vérifiez toujours le profil actif après import."
"Importe les deux listes locales (termes à conserver, termes à masquer) reçues d'un "
"autre poste ou de l'administrateur. La fusion ajoute ces termes à votre configuration "
"sans rien écraser.\n\n"
"L'import ne lit pas de documents patients. Redémarrez l'application pour appliquer."
)
CONFIG_MOCKUP_SECTIONS = {
@@ -223,10 +226,11 @@ def _app_base_dir() -> Path:
class ConfigTab(ctk.CTkFrame):
def __init__(self, master, state: ConfigState | None = None, palette: dict | None = None, **kwargs):
def __init__(self, master, state: ConfigState | None = None, palette: dict | None = None, config_path: Path | None = None, **kwargs):
self._p = palette or theme_mod.get_palette(theme_mod.DEFAULT_THEME)
super().__init__(master, fg_color=self._p["bg"], **kwargs)
self._state = state if state is not None else ConfigState()
self._config_path = config_path
self._sub = "reg"
self._sub_buttons: dict[str, ctk.CTkButton] = {}
self._panels: dict[str, ctk.CTkFrame] = {}
@@ -868,16 +872,20 @@ class ConfigTab(ctk.CTkFrame):
help_text=_HELP_EXPORT_CONFIG, help_title="Exporter la configuration",
)
export.pack(fill="both", expand=True)
self._note(export, "Listes locales, règles admin, style de masquage et template actif.")
self._mockup_button(export, "⬇ Exporter (.json)").pack(anchor="w", padx=12, pady=(0, 12))
self._note(export, "Vos listes locales : termes à toujours conserver et termes à toujours masquer.")
ui_kit.secondary_button(
export, p, "⬇ Exporter (.json)", command=self._on_export_config
).pack(anchor="w", padx=12, pady=(0, 12))
import_card = ui_kit.Card(
cols[1], p, title="📥 Importer une configuration",
help_text=_HELP_IMPORT_CONFIG, help_title="Importer une configuration",
)
import_card.pack(fill="both", expand=True)
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._note(import_card, "Fusionne les listes reçues (à conserver / à masquer) avec vos listes locales.")
ui_kit.secondary_button(
import_card, p, "⬆ Importer (.json)", command=self._on_import_config
).pack(anchor="w", padx=12, pady=(0, 12))
# -- helpers aide / maquette -----------------------------------------
@@ -942,6 +950,47 @@ class ConfigTab(ctk.CTkFrame):
def _on_manual_mask_template(self, label: str) -> None:
self._state.manual_mask_template = self._manual_mask_templates.get(label)
def _on_export_config(self) -> None:
from gui_v6.config_share import build_export_payload
import json
lists = getattr(self, "_pro_term_lists", {})
wl = lists["whitelist"].terms() if "whitelist" in lists else []
bl = lists["blacklist"].terms() if "blacklist" in lists else []
path = filedialog.asksaveasfilename(
title="Exporter la configuration", defaultextension=".json",
filetypes=[("Configuration JSON", "*.json")],
)
if not path:
return
try:
payload = build_export_payload(wl, bl)
Path(path).write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
messagebox.showinfo("Export", f"Configuration exportée :\n{path}")
except Exception as exc: # pragma: no cover - chemin UI
messagebox.showerror("Export", f"Échec de l'export : {exc}")
def _on_import_config(self) -> None:
from gui_v6.config_share import import_config_file
if self._config_path is None:
messagebox.showerror("Import", "Aucune configuration cible résolue.")
return
path = filedialog.askopenfilename(
title="Importer une configuration",
filetypes=[("Configuration JSON", "*.json")],
)
if not path:
return
try:
changed = import_config_file(path, self._config_path)
if changed:
messagebox.showinfo("Import", "Configuration fusionnée. Redémarrez pour appliquer.")
else:
messagebox.showinfo("Import", "Rien de nouveau à fusionner.")
except Exception as exc: # pragma: no cover - chemin UI
messagebox.showerror("Import", f"Échec de l'import : {exc}")
def _pick_output(self) -> None:
path = filedialog.askdirectory(title="Dossier de sortie")
if path: