feat: export/import paramètres par email + script merge côté serveur
GUI : - Bouton "Exporter pour envoi" → fichier JSON sur le Bureau avec whitelist + blacklist + version + date, prêt à envoyer par email - Bouton "Importer" → charge un JSON et fusionne (sans doublons) Serveur : - scripts/merge_params.py : fusionne les JSON reçus des établissements dans la config maîtresse dictionnaires.yml Usage : python scripts/merge_params.py export1.json export2.json Workflow : 1. L'établissement ajuste les paramètres dans la GUI 2. Clique "Exporter" → fichier JSON 3. Envoie par email 4. On fusionne avec merge_params.py 5. On reconstruit l'exe avec la config enrichie Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -546,15 +546,34 @@ class App:
|
|||||||
color_tag="#fce4ec",
|
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(
|
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",
|
font=self._f_small, bg=CLR_PRIMARY, fg="white",
|
||||||
activebackground="#1d4ed8", activeforeground="white",
|
activebackground="#1d4ed8", activeforeground="white",
|
||||||
relief=tk.FLAT, cursor="hand2", padx=12, pady=4,
|
relief=tk.FLAT, cursor="hand2", padx=12, pady=4,
|
||||||
command=self._save_params,
|
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
|
# Charger les valeurs initiales depuis la config
|
||||||
self._load_params()
|
self._load_params()
|
||||||
@@ -1198,6 +1217,99 @@ class App:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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):
|
def _save_params(self):
|
||||||
"""Sauvegarde les whitelist/blacklist dans la config YAML."""
|
"""Sauvegarde les whitelist/blacklist dans la config YAML."""
|
||||||
try:
|
try:
|
||||||
|
|||||||
115
scripts/merge_params.py
Normal file
115
scripts/merge_params.py
Normal file
@@ -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()
|
||||||
Reference in New Issue
Block a user