diff --git a/Pseudonymisation_Gui_V5.py b/Pseudonymisation_Gui_V5.py index 067c190..2aad33f 100644 --- a/Pseudonymisation_Gui_V5.py +++ b/Pseudonymisation_Gui_V5.py @@ -546,15 +546,34 @@ class App: color_tag="#fce4ec", ) - # Bouton sauvegarder + # Boutons sauvegarder + exporter + btn_row = tk.Frame(self._params_frame, bg=CLR_BG) + btn_row.pack(fill=tk.X, pady=(4, 4)) + + export_btn = tk.Button( + btn_row, text="\u2709 Exporter pour envoi", + font=self._f_small, bg="#e3f2fd", fg="#1565c0", + relief=tk.GROOVE, cursor="hand2", padx=10, pady=4, + command=self._export_params, + ) + export_btn.pack(side=tk.LEFT) + + import_btn = tk.Button( + btn_row, text="\u2B07 Importer", + font=self._f_small, bg="#fff3e0", fg="#e65100", + relief=tk.GROOVE, cursor="hand2", padx=10, pady=4, + command=self._import_params, + ) + import_btn.pack(side=tk.LEFT, padx=(4, 0)) + save_btn = tk.Button( - self._params_frame, text="Sauvegarder les paramètres", + btn_row, text="Sauvegarder", font=self._f_small, bg=CLR_PRIMARY, fg="white", activebackground="#1d4ed8", activeforeground="white", relief=tk.FLAT, cursor="hand2", padx=12, pady=4, command=self._save_params, ) - save_btn.pack(anchor="e", pady=(4, 4)) + save_btn.pack(side=tk.RIGHT) # Charger les valeurs initiales depuis la config self._load_params() @@ -1198,6 +1217,99 @@ class App: except Exception: pass + def _export_params(self): + """Exporte les paramètres whitelist/blacklist dans un fichier JSON pour envoi par email.""" + try: + import json as _json + from datetime import datetime + + wl = list(self._wl_listbox.get(0, tk.END)) + bl = list(self._bl_listbox.get(0, tk.END)) + + export_data = { + "version": APP_VERSION, + "date_export": datetime.now().isoformat(), + "etablissement": "", # à remplir par l'utilisateur + "whitelist_phrases": wl, + "blacklist_force_mask_terms": bl, + "instructions": ( + "Ce fichier contient les paramètres d'anonymisation personnalisés. " + "Envoyez-le par email à l'équipe technique pour mise à jour du programme." + ), + } + + # Proposer le Bureau comme destination par défaut + desktop = Path.home() / "Desktop" + if not desktop.exists(): + desktop = Path.home() / "Bureau" + if not desktop.exists(): + desktop = Path.home() + + dest = filedialog.asksaveasfilename( + title="Exporter les paramètres", + initialdir=str(desktop), + initialfile="parametres_anonymisation.json", + defaultextension=".json", + filetypes=[("JSON", "*.json"), ("Tous", "*.*")], + ) + if dest: + Path(dest).write_text( + _json.dumps(export_data, ensure_ascii=False, indent=2), + encoding="utf-8", + ) + messagebox.showinfo( + "Export réussi", + f"Paramètres exportés dans :\n{dest}\n\n" + f"Vous pouvez envoyer ce fichier par email\n" + f"à l'équipe technique.", + ) + except Exception as e: + messagebox.showerror("Erreur", f"Erreur à l'export :\n{e}") + + def _import_params(self): + """Importe des paramètres depuis un fichier JSON (fusionne avec l'existant).""" + try: + import json as _json + + src = filedialog.askopenfilename( + title="Importer des paramètres", + filetypes=[("JSON", "*.json"), ("Tous", "*.*")], + ) + if not src: + return + + data = _json.loads(Path(src).read_text(encoding="utf-8")) + + # Fusionner whitelist + new_wl = data.get("whitelist_phrases", []) + existing_wl = set(self._wl_listbox.get(0, tk.END)) + added_wl = 0 + for phrase in new_wl: + if phrase and phrase.strip() and phrase.strip() not in existing_wl: + self._wl_listbox.insert(tk.END, phrase.strip()) + added_wl += 1 + + # Fusionner blacklist + new_bl = data.get("blacklist_force_mask_terms", []) + existing_bl = set(self._bl_listbox.get(0, tk.END)) + added_bl = 0 + for term in new_bl: + if term and str(term).strip() and str(term).strip() not in existing_bl: + self._bl_listbox.insert(tk.END, str(term).strip()) + added_bl += 1 + + version = data.get("version", "?") + date_exp = data.get("date_export", "?")[:10] + messagebox.showinfo( + "Import réussi", + f"Paramètres importés (v{version}, {date_exp}) :\n\n" + f" + {added_wl} phrase(s) ajoutée(s) à la whitelist\n" + f" + {added_bl} terme(s) ajouté(s) à la blacklist\n\n" + f"Cliquez sur « Sauvegarder » pour appliquer.", + ) + except Exception as e: + messagebox.showerror("Erreur", f"Erreur à l'import :\n{e}") + def _save_params(self): """Sauvegarde les whitelist/blacklist dans la config YAML.""" try: diff --git a/scripts/merge_params.py b/scripts/merge_params.py new file mode 100644 index 0000000..be5f450 --- /dev/null +++ b/scripts/merge_params.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +"""Fusionne les fichiers de paramètres envoyés par les établissements. + +Usage : + python scripts/merge_params.py fichier1.json [fichier2.json ...] + python scripts/merge_params.py --dir /chemin/vers/exports/ + +Fusionne les whitelist_phrases et blacklist_force_mask_terms de chaque +fichier JSON exporté par la GUI dans la config maîtresse (dictionnaires.yml). +""" +import argparse +import json +import sys +from pathlib import Path + +try: + import yaml +except ImportError: + print("ERREUR : pyyaml requis (pip install pyyaml)") + sys.exit(1) + +CONFIG = Path(__file__).parent.parent / "config" / "dictionnaires.yml" + + +def merge_params(json_files: list, config_path: Path = CONFIG, dry_run: bool = False): + """Fusionne les paramètres des fichiers JSON dans la config YAML.""" + if not config_path.exists(): + print(f"ERREUR : config introuvable : {config_path}") + return + + cfg = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {} + + # Charger les listes existantes + existing_wl = set(cfg.get("whitelist_phrases", [])) + existing_bl = set(cfg.get("blacklist", {}).get("force_mask_terms", [])) + + added_wl = set() + added_bl = set() + sources = [] + + for jf in json_files: + try: + data = json.loads(Path(jf).read_text(encoding="utf-8")) + src = f"{Path(jf).name} (v{data.get('version', '?')}, {data.get('date_export', '?')[:10]})" + sources.append(src) + + for phrase in data.get("whitelist_phrases", []): + if phrase and phrase.strip() and phrase.strip() not in existing_wl: + added_wl.add(phrase.strip()) + + for term in data.get("blacklist_force_mask_terms", []): + if term and str(term).strip() and str(term).strip() not in existing_bl: + added_bl.add(str(term).strip()) + + except Exception as e: + print(f" ERREUR lecture {jf}: {e}") + + print(f"\nSources traitées : {len(sources)}") + for s in sources: + print(f" - {s}") + + print(f"\nNouvelles phrases whitelist : {len(added_wl)}") + for p in sorted(added_wl): + print(f" + {p}") + + print(f"\nNouveaux termes blacklist : {len(added_bl)}") + for t in sorted(added_bl): + print(f" + {t}") + + if not added_wl and not added_bl: + print("\nRien de nouveau à fusionner.") + return + + if dry_run: + print("\n(dry-run — aucune modification)") + return + + # Appliquer + cfg["whitelist_phrases"] = sorted(existing_wl | added_wl) + if "blacklist" not in cfg: + cfg["blacklist"] = {} + cfg["blacklist"]["force_mask_terms"] = sorted(existing_bl | added_bl) + + config_path.write_text( + yaml.dump(cfg, allow_unicode=True, default_flow_style=False, sort_keys=False), + encoding="utf-8", + ) + print(f"\nConfig mise à jour : {config_path}") + print(f" Whitelist : {len(cfg['whitelist_phrases'])} phrases") + print(f" Blacklist : {len(cfg['blacklist']['force_mask_terms'])} termes") + + +def main(): + parser = argparse.ArgumentParser(description="Fusionner les paramètres d'anonymisation") + parser.add_argument("files", nargs="*", help="Fichiers JSON à fusionner") + parser.add_argument("--dir", type=Path, help="Dossier contenant les fichiers JSON") + parser.add_argument("--config", type=Path, default=CONFIG, help="Config YAML cible") + parser.add_argument("--dry-run", action="store_true", help="Afficher sans modifier") + args = parser.parse_args() + + json_files = list(args.files) + if args.dir and args.dir.is_dir(): + json_files.extend(str(f) for f in args.dir.glob("*.json")) + + if not json_files: + print("Aucun fichier JSON spécifié. Usage :") + print(" python scripts/merge_params.py export1.json export2.json") + print(" python scripts/merge_params.py --dir /chemin/exports/") + return + + merge_params(json_files, config_path=args.config, dry_run=args.dry_run) + + +if __name__ == "__main__": + main()