feat(gui): onglet Profils éditable (création/modification/persistance)
Retour Dom : remplacer la page vitrine par un vrai éditeur de profils. - gui_v6/profile_editor.py : couche logique (build_profile_spec, profile_is_editable runtime vs defaut, list_profile_choices, slug_for_copy, save/set_default/delete) au-dessus de profile_defaults — persistance dans config/profiles.yml. - gui_v6/editable_list.py : EditableTermList (tableau scrollable de termes, ajout/suppression, pas de pastilles) — reste lisible à 50+ termes. - tab_config : sous-onglet « 👤 Profils » réintroduit comme éditeur — menu déroulant « Profil à modifier », boutons Nouveau / Dupliquer / Enregistrer / Annuler / Définir par défaut, sections Identité, Masquage (require_manual_mask, template), Moteurs (force_disable_vlm), Mots (à masquer/conserver/ignorer éditables), Règles « à venir ». Profils défaut = lecture seule (dupliquer pour modifier). Confirmation non bloquante (pas de modale). - Réglages : bouton « ✏️ Modifier le profil… » → ouvre Profils sur le profil actif. Pas de pastilles inline. Persiste : label, description, require_manual_mask, force_disable_vlm, preferred_manual_mask_template, param_lists (3 listes). 260 tests unit OK (0 régression), self-test OK, nav 5 sous-onglets + thème OK. Préserve 1bbe70a/d30f7b7. Aucun build/push sans GO Dom. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
120
gui_v6/profile_editor.py
Normal file
120
gui_v6/profile_editor.py
Normal file
@@ -0,0 +1,120 @@
|
||||
"""Couche logique de l'éditeur de profils (persistance via profile_defaults).
|
||||
|
||||
Wrappers testables sans display : assemblage de la spec, détection
|
||||
runtime/éditable vs defaut/lecture-seule, sauvegarde dans config/profiles.yml.
|
||||
Aucun profil par défaut (profiles.default.yml) n'est jamais modifié ici.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable, Optional
|
||||
|
||||
from profile_defaults import (
|
||||
delete_runtime_profile,
|
||||
get_default_profile_key,
|
||||
list_effective_profiles,
|
||||
load_runtime_profiles_overlay_dict,
|
||||
save_runtime_profile,
|
||||
set_runtime_default_profile,
|
||||
)
|
||||
|
||||
|
||||
def _clean_list(values: Optional[Iterable]) -> list[str]:
|
||||
return [str(v).strip() for v in (values or []) if str(v).strip()]
|
||||
|
||||
|
||||
def build_profile_spec(
|
||||
*,
|
||||
label: str,
|
||||
description: str = "",
|
||||
require_manual_mask: bool = False,
|
||||
force_disable_vlm: bool = False,
|
||||
preferred_manual_mask_template: str = "",
|
||||
whitelist: Optional[Iterable] = None,
|
||||
blacklist: Optional[Iterable] = None,
|
||||
stopwords: Optional[Iterable] = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Assemble une spec de profil persistable (3 listes normalisées)."""
|
||||
return {
|
||||
"label": str(label or "").strip(),
|
||||
"description": str(description or ""),
|
||||
"require_manual_mask": bool(require_manual_mask),
|
||||
"force_disable_vlm": bool(force_disable_vlm),
|
||||
"preferred_manual_mask_template": str(preferred_manual_mask_template or "").strip(),
|
||||
"has_preferred_manual_mask_template": True,
|
||||
"has_param_lists": True,
|
||||
"param_lists": {
|
||||
"whitelist_phrases": _clean_list(whitelist),
|
||||
"blacklist_force_mask_terms": _clean_list(blacklist),
|
||||
"additional_stopwords": _clean_list(stopwords),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def runtime_profile_keys(path: Path | None = None) -> set[str]:
|
||||
"""Clés des profils définis dans l'overlay runtime (config/profiles.yml)."""
|
||||
try:
|
||||
data = load_runtime_profiles_overlay_dict(path) or {}
|
||||
except Exception:
|
||||
return set()
|
||||
profiles = data.get("profiles") if isinstance(data, dict) else None
|
||||
return set(profiles) if isinstance(profiles, dict) else set()
|
||||
|
||||
|
||||
def profile_is_editable(key: str, path: Path | None = None) -> bool:
|
||||
"""Un profil est éditable s'il est dans l'overlay runtime (pas un defaut pur)."""
|
||||
return key in runtime_profile_keys(path)
|
||||
|
||||
|
||||
def _default_key(path: Path | None = None) -> Optional[str]:
|
||||
try:
|
||||
data = load_runtime_profiles_overlay_dict(path) or {}
|
||||
if isinstance(data, dict) and data.get("default_profile"):
|
||||
return str(data["default_profile"])
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
return get_default_profile_key()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def list_profile_choices(path: Path | None = None) -> list[dict]:
|
||||
"""Liste triée des profils avec méta : ``key``, ``label``, ``editable``, ``is_default``."""
|
||||
profiles = list_effective_profiles(path)
|
||||
runtime = runtime_profile_keys(path)
|
||||
default = _default_key(path)
|
||||
return [
|
||||
{
|
||||
"key": key,
|
||||
"label": str(profiles[key].get("label") or key),
|
||||
"editable": key in runtime,
|
||||
"is_default": key == default,
|
||||
}
|
||||
for key in sorted(profiles)
|
||||
]
|
||||
|
||||
|
||||
def slug_for_copy(key: str, existing: Iterable[str]) -> str:
|
||||
"""Clé de copie unique : ``{key}_copie`` puis ``_2``, ``_3``…"""
|
||||
existing = set(existing)
|
||||
base = f"{key}_copie"
|
||||
if base not in existing:
|
||||
return base
|
||||
index = 2
|
||||
while f"{base}_{index}" in existing:
|
||||
index += 1
|
||||
return f"{base}_{index}"
|
||||
|
||||
|
||||
def save_profile(key: str, spec: dict, path: Path | None = None, *, set_default: bool = False) -> Path:
|
||||
return save_runtime_profile(key, spec, path, set_default=set_default)
|
||||
|
||||
|
||||
def set_default_profile(key: str, path: Path | None = None) -> Path:
|
||||
return set_runtime_default_profile(key, path)
|
||||
|
||||
|
||||
def delete_profile(key: str, path: Path | None = None) -> Path:
|
||||
return delete_runtime_profile(key, path)
|
||||
Reference in New Issue
Block a user