fix(gui): clarifier aide et disponibilite moteurs
Passe theme clair, libelles utilisateur, aides conteneurs, recherche de mise a jour et indication honnete des moteurs optionnels non embarques. Tests GUI unitaires: 126 passed.
This commit is contained in:
@@ -4,6 +4,7 @@ Contrat final aligné sur le portail ``app_aivanov`` :
|
|||||||
|
|
||||||
- ``activate(token, machine_id)`` → ``POST /api/v1/activate``
|
- ``activate(token, machine_id)`` → ``POST /api/v1/activate``
|
||||||
- ``check(license_ref, machine_id)`` → ``POST /api/v1/check``
|
- ``check(license_ref, machine_id)`` → ``POST /api/v1/check``
|
||||||
|
- ``latest_version()`` → ``GET /api/v1/version``
|
||||||
|
|
||||||
Principes :
|
Principes :
|
||||||
|
|
||||||
@@ -44,6 +45,8 @@ class _HttpResponse(Protocol):
|
|||||||
class _HttpSession(Protocol):
|
class _HttpSession(Protocol):
|
||||||
def post(self, url: str, json: dict, timeout: float) -> _HttpResponse: ...
|
def post(self, url: str, json: dict, timeout: float) -> _HttpResponse: ...
|
||||||
|
|
||||||
|
def get(self, url: str, timeout: float) -> _HttpResponse: ...
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LicenseStatus:
|
class LicenseStatus:
|
||||||
@@ -138,6 +141,13 @@ class LicenseClient:
|
|||||||
# Réseau indisponible, DNS, timeout, requests absent… : pas de crash.
|
# Réseau indisponible, DNS, timeout, requests absent… : pas de crash.
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _get(self, endpoint: str) -> Optional[_HttpResponse]:
|
||||||
|
try:
|
||||||
|
session = self._get_session()
|
||||||
|
return session.get(f"{self._base_url}{endpoint}", timeout=self._timeout)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse(response: Optional[_HttpResponse]) -> Optional[dict]:
|
def _parse(response: Optional[_HttpResponse]) -> Optional[dict]:
|
||||||
if response is None:
|
if response is None:
|
||||||
@@ -187,6 +197,14 @@ class LicenseClient:
|
|||||||
self._store.save(payload)
|
self._store.save(payload)
|
||||||
return status
|
return status
|
||||||
|
|
||||||
|
def latest_version(self) -> Optional[dict[str, Any]]:
|
||||||
|
"""Retourne les métadonnées de la version active publiée sur le portail."""
|
||||||
|
response = self._get("/api/v1/version")
|
||||||
|
if response is None or getattr(response, "status_code", 500) >= 400:
|
||||||
|
return None
|
||||||
|
payload = self._parse(response)
|
||||||
|
return payload if payload is not None else None
|
||||||
|
|
||||||
def local_status(self) -> LicenseStatus:
|
def local_status(self) -> LicenseStatus:
|
||||||
"""État de licence depuis le stockage local, sans appel réseau."""
|
"""État de licence depuis le stockage local, sans appel réseau."""
|
||||||
data = self._store.load()
|
data = self._store.load()
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ démarrage (seul ``local_status`` est lu).
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from tkinter import messagebox
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import customtkinter as ctk
|
import customtkinter as ctk
|
||||||
@@ -35,6 +36,11 @@ _STATUS_LABELS = {
|
|||||||
"none": "Aucune licence",
|
"none": "Aucune licence",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_UPDATE_CHECK_MESSAGE = (
|
||||||
|
"Aucune version publiée n'a été trouvée sur le portail.\n\n"
|
||||||
|
"Vérifiez que le serveur web est joignable ou contactez votre administrateur."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _build_info() -> str:
|
def _build_info() -> str:
|
||||||
try:
|
try:
|
||||||
@@ -86,9 +92,9 @@ class AboutTab(ctk.CTkFrame):
|
|||||||
items = [
|
items = [
|
||||||
("🏷️", "Version", f"Interface V6 — {GUI_VERSION}"),
|
("🏷️", "Version", f"Interface V6 — {GUI_VERSION}"),
|
||||||
("📅", "Build", _build_info()),
|
("📅", "Build", _build_info()),
|
||||||
("🧠", "Moteurs NER", "CamemBERT · EDS-Pseudo · GLiNER"),
|
("🧠", "Moteurs NER", "CamemBERT inclus · EDS/GLiNER optionnels non embarqués"),
|
||||||
("🔒", "Traitement", "100 % local — aucune donnée transmise"),
|
("🔒", "Traitement", "100 % local — aucune donnée transmise"),
|
||||||
("📚", "Gazetteers", "INSEE 219K · FINESS 108K · BDPM 7K"),
|
("📚", "Bases de données", "INSEE 219K · FINESS 108K · BDPM 7K"),
|
||||||
("📁", "Formats", "PDF · DOCX · ODT · RTF · TXT · Images"),
|
("📁", "Formats", "PDF · DOCX · ODT · RTF · TXT · Images"),
|
||||||
("🖥️", "Poste", self._machine_id),
|
("🖥️", "Poste", self._machine_id),
|
||||||
]
|
]
|
||||||
@@ -102,6 +108,9 @@ class AboutTab(ctk.CTkFrame):
|
|||||||
txt.pack(side="left")
|
txt.pack(side="left")
|
||||||
ctk.CTkLabel(txt, text=key.upper(), text_color=p["text_muted"], font=ui_kit.font(10), anchor="w").pack(anchor="w")
|
ctk.CTkLabel(txt, text=key.upper(), text_color=p["text_muted"], font=ui_kit.font(10), anchor="w").pack(anchor="w")
|
||||||
ctk.CTkLabel(txt, text=val, text_color=p["text"], font=ui_kit.font(12, "bold"), anchor="w").pack(anchor="w")
|
ctk.CTkLabel(txt, text=val, text_color=p["text"], font=ui_kit.font(12, "bold"), anchor="w").pack(anchor="w")
|
||||||
|
ui_kit.secondary_button(info, p, "🔄 Rechercher une mise à jour", command=self._check_updates).pack(
|
||||||
|
anchor="w", padx=16, pady=(0, 14)
|
||||||
|
)
|
||||||
|
|
||||||
# Bloc licence
|
# Bloc licence
|
||||||
lic = ui_kit.Card(self, p, title="🔑 Licence")
|
lic = ui_kit.Card(self, p, title="🔑 Licence")
|
||||||
@@ -151,3 +160,19 @@ class AboutTab(ctk.CTkFrame):
|
|||||||
self.set_status(LicenseStatus.none("Aucune licence à vérifier"))
|
self.set_status(LicenseStatus.none("Aucune licence à vérifier"))
|
||||||
return
|
return
|
||||||
self.set_status(self._client.check(ref, self._machine_id))
|
self.set_status(self._client.check(ref, self._machine_id))
|
||||||
|
|
||||||
|
def _check_updates(self) -> None:
|
||||||
|
if self._client is None:
|
||||||
|
messagebox.showinfo("Mise à jour", _UPDATE_CHECK_MESSAGE)
|
||||||
|
return
|
||||||
|
payload = self._client.latest_version()
|
||||||
|
if not payload:
|
||||||
|
messagebox.showinfo("Mise à jour", _UPDATE_CHECK_MESSAGE)
|
||||||
|
return
|
||||||
|
version = str(payload.get("version") or "version inconnue")
|
||||||
|
channel = str(payload.get("channel") or "canal non précisé")
|
||||||
|
filename = str(payload.get("filename") or "fichier non précisé")
|
||||||
|
messagebox.showinfo(
|
||||||
|
"Mise à jour",
|
||||||
|
f"Version publiée : {version}\nCanal : {channel}\nFichier : {filename}",
|
||||||
|
)
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ Partage/Règles.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import webbrowser
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tkinter import filedialog, messagebox
|
from tkinter import filedialog, messagebox
|
||||||
|
|
||||||
@@ -28,7 +27,7 @@ _SUBTABS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
_DETECTION_OPTIONS = [
|
_DETECTION_OPTIONS = [
|
||||||
("Noms et prénoms", "Gazetteers + IA"),
|
("Noms et prénoms", "Annuaire + IA"),
|
||||||
("Dates de naissance", "Contexte naissance"),
|
("Dates de naissance", "Contexte naissance"),
|
||||||
("Établissements", "FINESS + contexte"),
|
("Établissements", "FINESS + contexte"),
|
||||||
("Adresses / CP", "Voie, ville, code"),
|
("Adresses / CP", "Voie, ville, code"),
|
||||||
@@ -47,6 +46,10 @@ _MASK_COLORS = [
|
|||||||
|
|
||||||
MANUAL_MASK_NONE_LABEL = "Aucun masque manuel"
|
MANUAL_MASK_NONE_LABEL = "Aucun masque manuel"
|
||||||
|
|
||||||
|
MINI_TOGGLE_HEIGHT = 46
|
||||||
|
MINI_TOGGLE_LABEL_FONT_SIZE = 12
|
||||||
|
MINI_TOGGLE_HINT_FONT_SIZE = 11
|
||||||
|
|
||||||
# Textes d'aide « ? » (français simple, pour utilisateurs non informaticiens).
|
# Textes d'aide « ? » (français simple, pour utilisateurs non informaticiens).
|
||||||
_HELP_REGLAGES = (
|
_HELP_REGLAGES = (
|
||||||
"Réglages de l'anonymisation.\n\n"
|
"Réglages de l'anonymisation.\n\n"
|
||||||
@@ -95,6 +98,57 @@ _HELP_LISTES = (
|
|||||||
"Pour une liste longue, ouvrez le tableau des termes : "
|
"Pour une liste longue, ouvrez le tableau des termes : "
|
||||||
"il reste lisible et permet la recherche."
|
"il reste lisible et permet la recherche."
|
||||||
)
|
)
|
||||||
|
_HELP_DONNEES_DETECTER = (
|
||||||
|
"Cette zone indique les familles de données que le profil cherche à anonymiser : "
|
||||||
|
"noms, dates de naissance, établissements, adresses, identifiants, téléphones et e-mails.\n\n"
|
||||||
|
"Ces options décrivent le périmètre fonctionnel attendu. Les règles exactes restent "
|
||||||
|
"contrôlées par le moteur et par le profil actif."
|
||||||
|
)
|
||||||
|
_HELP_MOTEURS_MASQUES = (
|
||||||
|
"Cette zone regroupe les moteurs de détection et les réglages de masque manuel.\n\n"
|
||||||
|
"Le masque manuel sert aux zones fixes d'un document que le texte ne suffit pas à détecter "
|
||||||
|
"correctement : logo, en-tête, coordonnées, bloc institutionnel ou tampon scanné.\n\n"
|
||||||
|
"Si « Masque manuel obligatoire » est actif, le profil impose cette étape de contrôle "
|
||||||
|
"avant de considérer le traitement complet."
|
||||||
|
)
|
||||||
|
_HELP_PROFIL_CHOIX = (
|
||||||
|
"Choisissez ici le profil à modifier.\n\n"
|
||||||
|
"Les profils livrés par défaut sont en lecture seule pour éviter une modification accidentelle. "
|
||||||
|
"Dupliquez un profil pour créer une version adaptée à votre établissement."
|
||||||
|
)
|
||||||
|
_HELP_PROFIL_IDENTITE = (
|
||||||
|
"Nom et description visibles dans l'interface.\n\n"
|
||||||
|
"Utilisez un nom simple que les utilisateurs comprendront, par exemple « Standard local », "
|
||||||
|
"« Recherche » ou « Diffusion externe prudente »."
|
||||||
|
)
|
||||||
|
_HELP_PROFIL_MASQUAGE = (
|
||||||
|
"Cette zone règle les masques propres au profil.\n\n"
|
||||||
|
"Masquage manuel obligatoire : le profil impose une vérification avec un masque de zones fixes "
|
||||||
|
"avant le traitement. C'est utile pour les documents qui ont toujours les mêmes zones sensibles "
|
||||||
|
"au même endroit : logos, en-têtes, coordonnées, tampons ou blocs scannés.\n\n"
|
||||||
|
"Template de masque préféré : modèle proposé automatiquement par ce profil. "
|
||||||
|
"L'éditeur de masque permet de créer ou ajuster ces zones visuellement."
|
||||||
|
)
|
||||||
|
_HELP_PROFIL_MOTEURS = (
|
||||||
|
"Cette zone précise les moteurs utilisés par le profil.\n\n"
|
||||||
|
"CamemBERT-bio est le moteur standard. Les moteurs optionnels ne sont proposés que s'ils sont "
|
||||||
|
"réellement embarqués dans cette version. Le moteur VLM concerne surtout les documents images."
|
||||||
|
)
|
||||||
|
_HELP_PROFIL_MOTS = (
|
||||||
|
"Ces listes appartiennent au profil.\n\n"
|
||||||
|
"À masquer : termes à remplacer systématiquement.\n"
|
||||||
|
"À conserver : termes à ne jamais masquer, même s'ils ressemblent à des noms.\n"
|
||||||
|
"À ignorer : mots qui ne doivent pas déclencher de détection.\n\n"
|
||||||
|
"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"
|
||||||
|
"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."
|
||||||
|
)
|
||||||
|
|
||||||
CONFIG_MOCKUP_SECTIONS = {
|
CONFIG_MOCKUP_SECTIONS = {
|
||||||
"reglages": [
|
"reglages": [
|
||||||
@@ -293,12 +347,18 @@ class ConfigTab(ctk.CTkFrame):
|
|||||||
|
|
||||||
cols = self._columns(parent, 3, gap=8, height=455)
|
cols = self._columns(parent, 3, gap=8, height=455)
|
||||||
|
|
||||||
det = ui_kit.Card(cols[0], p, title="🔍 Données à détecter")
|
det = ui_kit.Card(
|
||||||
|
cols[0], p, title="🔍 Données à détecter",
|
||||||
|
help_text=_HELP_DONNEES_DETECTER, help_title="Données à détecter",
|
||||||
|
)
|
||||||
det.pack(fill="both", expand=True)
|
det.pack(fill="both", expand=True)
|
||||||
for label, hint in _DETECTION_OPTIONS:
|
for label, hint in _DETECTION_OPTIONS:
|
||||||
self._mini_toggle(det, label, hint, value=True).pack(fill="x", padx=12, pady=1)
|
self._mini_toggle(det, label, hint, value=True).pack(fill="x", padx=12, pady=1)
|
||||||
|
|
||||||
ner = ui_kit.Card(cols[1], p, title="🧠 Moteurs et masques")
|
ner = ui_kit.Card(
|
||||||
|
cols[1], p, title="🧠 Moteurs et masques",
|
||||||
|
help_text=_HELP_MOTEURS_MASQUES, help_title="Moteurs et masques",
|
||||||
|
)
|
||||||
ner.pack(fill="both", expand=True)
|
ner.pack(fill="both", expand=True)
|
||||||
hint_row = ctk.CTkFrame(ner, fg_color="transparent")
|
hint_row = ctk.CTkFrame(ner, fg_color="transparent")
|
||||||
hint_row.pack(fill="x", padx=12, pady=(0, 2))
|
hint_row.pack(fill="x", padx=12, pady=(0, 2))
|
||||||
@@ -359,13 +419,13 @@ class ConfigTab(ctk.CTkFrame):
|
|||||||
mask_actions = ctk.CTkFrame(ner, fg_color="transparent")
|
mask_actions = ctk.CTkFrame(ner, fg_color="transparent")
|
||||||
mask_actions.pack(fill="x", padx=12, pady=(0, 12))
|
mask_actions.pack(fill="x", padx=12, pady=(0, 12))
|
||||||
ui_kit.secondary_button(mask_actions, p, "🔄 Actualiser", command=self._refresh_manual_mask_templates).pack(
|
ui_kit.secondary_button(mask_actions, p, "🔄 Actualiser", command=self._refresh_manual_mask_templates).pack(
|
||||||
side="left", fill="x", expand=True, padx=(0, 4)
|
side="left", fill="x", expand=True
|
||||||
)
|
|
||||||
ui_kit.secondary_button(mask_actions, p, "📁 Dossier", command=self._open_templates_dir).pack(
|
|
||||||
side="left", fill="x", expand=True, padx=(4, 0)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
terms = ui_kit.Card(cols[2], p, title="✅ Listes locales")
|
terms = ui_kit.Card(
|
||||||
|
cols[2], p, title="✅ Listes locales",
|
||||||
|
help_text=_HELP_LISTES, help_title="Listes locales",
|
||||||
|
)
|
||||||
terms.pack(fill="both", expand=True)
|
terms.pack(fill="both", expand=True)
|
||||||
terms_help = ctk.CTkFrame(terms, fg_color="transparent")
|
terms_help = ctk.CTkFrame(terms, fg_color="transparent")
|
||||||
terms_help.pack(fill="x", padx=12, pady=(0, 2))
|
terms_help.pack(fill="x", padx=12, pady=(0, 2))
|
||||||
@@ -438,7 +498,10 @@ class ConfigTab(ctk.CTkFrame):
|
|||||||
"Profils d'anonymisation",
|
"Profils d'anonymisation",
|
||||||
)
|
)
|
||||||
|
|
||||||
bar = ui_kit.Card(parent, p, title="👤 Profil à modifier")
|
bar = ui_kit.Card(
|
||||||
|
parent, p, title="👤 Profil à modifier",
|
||||||
|
help_text=_HELP_PROFIL_CHOIX, help_title="Profil à modifier",
|
||||||
|
)
|
||||||
bar.pack(fill="x", pady=(0, 8))
|
bar.pack(fill="x", pady=(0, 8))
|
||||||
top = ctk.CTkFrame(bar, fg_color="transparent")
|
top = ctk.CTkFrame(bar, fg_color="transparent")
|
||||||
top.pack(fill="x", padx=12, pady=(0, 4))
|
top.pack(fill="x", padx=12, pady=(0, 4))
|
||||||
@@ -465,7 +528,10 @@ class ConfigTab(ctk.CTkFrame):
|
|||||||
cols = self._columns(parent, 2, gap=8)
|
cols = self._columns(parent, 2, gap=8)
|
||||||
left, right = cols[0], cols[1]
|
left, right = cols[0], cols[1]
|
||||||
|
|
||||||
ident = ui_kit.Card(left, p, title="🏷️ Identité")
|
ident = ui_kit.Card(
|
||||||
|
left, p, title="🏷️ Identité",
|
||||||
|
help_text=_HELP_PROFIL_IDENTITE, help_title="Identité du profil",
|
||||||
|
)
|
||||||
ident.pack(fill="x", pady=(0, 8))
|
ident.pack(fill="x", pady=(0, 8))
|
||||||
self._pro_label_var = ctk.StringVar()
|
self._pro_label_var = ctk.StringVar()
|
||||||
self._pro_label_entry = ctk.CTkEntry(ident, textvariable=self._pro_label_var, fg_color=p["btn_sec_bg"], border_color=p["btn_sec_border"], text_color=p["text"], height=30)
|
self._pro_label_entry = ctk.CTkEntry(ident, textvariable=self._pro_label_var, fg_color=p["btn_sec_bg"], border_color=p["btn_sec_border"], text_color=p["text"], height=30)
|
||||||
@@ -476,7 +542,10 @@ class ConfigTab(ctk.CTkFrame):
|
|||||||
ctk.CTkLabel(ident, text="Description", text_color=p["text_muted"], font=ui_kit.font(11), anchor="w").pack(fill="x", padx=12, pady=(0, 2))
|
ctk.CTkLabel(ident, text="Description", text_color=p["text_muted"], font=ui_kit.font(11), anchor="w").pack(fill="x", padx=12, pady=(0, 2))
|
||||||
self._pro_desc_entry.pack(fill="x", padx=12, pady=(0, 12))
|
self._pro_desc_entry.pack(fill="x", padx=12, pady=(0, 12))
|
||||||
|
|
||||||
mask = ui_kit.Card(left, p, title="⬛ Masquage")
|
mask = ui_kit.Card(
|
||||||
|
left, p, title="⬛ Masquage",
|
||||||
|
help_text=_HELP_PROFIL_MASQUAGE, help_title="Masquage manuel",
|
||||||
|
)
|
||||||
mask.pack(fill="x", pady=(0, 8))
|
mask.pack(fill="x", pady=(0, 8))
|
||||||
self._pro_require_mask_var = ctk.BooleanVar(value=False)
|
self._pro_require_mask_var = ctk.BooleanVar(value=False)
|
||||||
self._pro_require_switch = ctk.CTkSwitch(mask, text="Masque manuel obligatoire", variable=self._pro_require_mask_var, progress_color=p["primary"], text_color=p["text"], font=ui_kit.font(12))
|
self._pro_require_switch = ctk.CTkSwitch(mask, text="Masque manuel obligatoire", variable=self._pro_require_mask_var, progress_color=p["primary"], text_color=p["text"], font=ui_kit.font(12))
|
||||||
@@ -488,7 +557,6 @@ class ConfigTab(ctk.CTkFrame):
|
|||||||
mask_actions = ctk.CTkFrame(mask, fg_color="transparent")
|
mask_actions = ctk.CTkFrame(mask, fg_color="transparent")
|
||||||
mask_actions.pack(fill="x", padx=12, pady=(0, 6))
|
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, "🖊 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.
|
# 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))
|
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))
|
||||||
@@ -518,7 +586,10 @@ class ConfigTab(ctk.CTkFrame):
|
|||||||
variable=self._mask_rounded_var, command=self._on_rounded_corners,
|
variable=self._mask_rounded_var, command=self._on_rounded_corners,
|
||||||
).pack(fill="x", padx=12, pady=(2, 12))
|
).pack(fill="x", padx=12, pady=(2, 12))
|
||||||
|
|
||||||
eng = ui_kit.Card(left, p, title="🧠 Moteurs")
|
eng = ui_kit.Card(
|
||||||
|
left, p, title="🧠 Moteurs",
|
||||||
|
help_text=_HELP_PROFIL_MOTEURS, help_title="Moteurs du profil",
|
||||||
|
)
|
||||||
eng.pack(fill="x")
|
eng.pack(fill="x")
|
||||||
self._pro_disable_vlm_var = ctk.BooleanVar(value=False)
|
self._pro_disable_vlm_var = ctk.BooleanVar(value=False)
|
||||||
self._pro_vlm_switch = ctk.CTkSwitch(eng, text="Désactiver le moteur VLM (images)", variable=self._pro_disable_vlm_var, progress_color=p["primary"], text_color=p["text"], font=ui_kit.font(12))
|
self._pro_vlm_switch = ctk.CTkSwitch(eng, text="Désactiver le moteur VLM (images)", variable=self._pro_disable_vlm_var, progress_color=p["primary"], text_color=p["text"], font=ui_kit.font(12))
|
||||||
@@ -532,7 +603,10 @@ class ConfigTab(ctk.CTkFrame):
|
|||||||
moteurs_note = "CamemBERT-bio (standard) toujours actif ; EDS-Pseudo / GLiNER non embarqués dans cette version."
|
moteurs_note = "CamemBERT-bio (standard) toujours actif ; EDS-Pseudo / GLiNER non embarqués dans cette version."
|
||||||
ctk.CTkLabel(eng, text=moteurs_note, text_color=p["text_muted"], font=ui_kit.font(11), anchor="w", wraplength=300, justify="left").pack(fill="x", padx=12, pady=(0, 12))
|
ctk.CTkLabel(eng, text=moteurs_note, text_color=p["text_muted"], font=ui_kit.font(11), anchor="w", wraplength=300, justify="left").pack(fill="x", padx=12, pady=(0, 12))
|
||||||
|
|
||||||
words = ui_kit.Card(right, p, title="📝 Mots du profil")
|
words = ui_kit.Card(
|
||||||
|
right, p, title="📝 Mots du profil",
|
||||||
|
help_text=_HELP_PROFIL_MOTS, help_title="Mots du profil",
|
||||||
|
)
|
||||||
words.pack(fill="both", expand=True)
|
words.pack(fill="both", expand=True)
|
||||||
self._pro_term_lists = {
|
self._pro_term_lists = {
|
||||||
"blacklist": EditableTermList(words, p, title="À masquer", height=104),
|
"blacklist": EditableTermList(words, p, title="À masquer", height=104),
|
||||||
@@ -542,7 +616,10 @@ class ConfigTab(ctk.CTkFrame):
|
|||||||
for tl in self._pro_term_lists.values():
|
for tl in self._pro_term_lists.values():
|
||||||
tl.pack(fill="x", padx=12, pady=(0, 8))
|
tl.pack(fill="x", padx=12, pady=(0, 8))
|
||||||
|
|
||||||
rules = ui_kit.Card(parent, p, title="🛡️ Règles du profil")
|
rules = ui_kit.Card(
|
||||||
|
parent, p, title="🛡️ Règles du profil",
|
||||||
|
help_text=_HELP_REGLES, help_title="Règles du profil",
|
||||||
|
)
|
||||||
rules.pack(fill="x", pady=(8, 0))
|
rules.pack(fill="x", pady=(8, 0))
|
||||||
rules_intro = ctk.CTkFrame(rules, fg_color="transparent")
|
rules_intro = ctk.CTkFrame(rules, fg_color="transparent")
|
||||||
rules_intro.pack(fill="x", padx=12, pady=(0, 2))
|
rules_intro.pack(fill="x", padx=12, pady=(0, 2))
|
||||||
@@ -740,12 +817,18 @@ class ConfigTab(ctk.CTkFrame):
|
|||||||
"À quoi sert le Partage ?",
|
"À quoi sert le Partage ?",
|
||||||
)
|
)
|
||||||
cols = self._columns(parent, 2, gap=8, height=180)
|
cols = self._columns(parent, 2, gap=8, height=180)
|
||||||
export = ui_kit.Card(cols[0], p, title="📤 Exporter la configuration")
|
export = ui_kit.Card(
|
||||||
|
cols[0], p, title="📤 Exporter la configuration",
|
||||||
|
help_text=_HELP_EXPORT_CONFIG, help_title="Exporter la configuration",
|
||||||
|
)
|
||||||
export.pack(fill="both", expand=True)
|
export.pack(fill="both", expand=True)
|
||||||
self._note(export, "Listes locales, règles admin, style de masquage et template actif.")
|
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._mockup_button(export, "⬇ Exporter (.json)").pack(anchor="w", padx=12, pady=(0, 12))
|
||||||
|
|
||||||
import_card = ui_kit.Card(cols[1], p, title="📥 Importer une configuration")
|
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)
|
import_card.pack(fill="both", expand=True)
|
||||||
self._note(import_card, "Fusionne la configuration reçue avec vos réglages locaux.")
|
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._mockup_button(import_card, "⬆ Importer (.json)").pack(anchor="w", padx=12, pady=(0, 12))
|
||||||
@@ -820,10 +903,7 @@ class ConfigTab(ctk.CTkFrame):
|
|||||||
|
|
||||||
def _open_templates_dir(self) -> None:
|
def _open_templates_dir(self) -> None:
|
||||||
path = ensure_mask_templates_dir(_app_base_dir())
|
path = ensure_mask_templates_dir(_app_base_dir())
|
||||||
try:
|
messagebox.showinfo("Dossier modèles", str(path))
|
||||||
webbrowser.open(path.as_uri())
|
|
||||||
except Exception:
|
|
||||||
messagebox.showinfo("Dossier modèles", str(path))
|
|
||||||
|
|
||||||
# -- callbacks masquage ----------------------------------------------
|
# -- callbacks masquage ----------------------------------------------
|
||||||
|
|
||||||
@@ -962,15 +1042,27 @@ class ConfigTab(ctk.CTkFrame):
|
|||||||
def _mini_toggle(self, parent, label: str, hint: str, value: bool = True, variable=None,
|
def _mini_toggle(self, parent, label: str, hint: str, value: bool = True, variable=None,
|
||||||
command=None, disabled: bool = False, disabled_hint: str | None = None):
|
command=None, disabled: bool = False, disabled_hint: str | None = None):
|
||||||
p = self._p
|
p = self._p
|
||||||
row = ctk.CTkFrame(parent, fg_color="transparent", height=34)
|
row = ctk.CTkFrame(parent, fg_color="transparent", height=MINI_TOGGLE_HEIGHT)
|
||||||
row.pack_propagate(False)
|
row.pack_propagate(False)
|
||||||
left = ctk.CTkFrame(row, fg_color="transparent")
|
left = ctk.CTkFrame(row, fg_color="transparent")
|
||||||
left.pack(side="left", fill="x", expand=True)
|
left.pack(side="left", fill="both", expand=True, pady=(3, 2))
|
||||||
lbl_color = p["text_muted"] if disabled else p["text"]
|
lbl_color = p["text_muted"] if disabled else p["text"]
|
||||||
ctk.CTkLabel(left, text=label, text_color=lbl_color, font=ui_kit.font(12), anchor="w").pack(anchor="w")
|
ctk.CTkLabel(
|
||||||
|
left,
|
||||||
|
text=label,
|
||||||
|
text_color=lbl_color,
|
||||||
|
font=ui_kit.font(MINI_TOGGLE_LABEL_FONT_SIZE, "bold"),
|
||||||
|
anchor="w",
|
||||||
|
).pack(anchor="w")
|
||||||
shown_hint = disabled_hint if (disabled and disabled_hint) else hint
|
shown_hint = disabled_hint if (disabled and disabled_hint) else hint
|
||||||
if shown_hint:
|
if shown_hint:
|
||||||
ctk.CTkLabel(left, text=shown_hint, text_color=p["text_muted"], font=ui_kit.font(10), anchor="w").pack(anchor="w")
|
ctk.CTkLabel(
|
||||||
|
left,
|
||||||
|
text=shown_hint,
|
||||||
|
text_color=p["text_muted"] if disabled else p["text_dim"],
|
||||||
|
font=ui_kit.font(MINI_TOGGLE_HINT_FONT_SIZE),
|
||||||
|
anchor="w",
|
||||||
|
).pack(anchor="w", pady=(1, 0))
|
||||||
# Moteur indisponible : on force l'état à False (jamais « coché mais absent »).
|
# Moteur indisponible : on force l'état à False (jamais « coché mais absent »).
|
||||||
if disabled and variable is None:
|
if disabled and variable is None:
|
||||||
value = False
|
value = False
|
||||||
@@ -980,7 +1072,7 @@ class ConfigTab(ctk.CTkFrame):
|
|||||||
switch = ctk.CTkSwitch(row, text="", variable=var, command=command, progress_color=p["primary"], width=38)
|
switch = ctk.CTkSwitch(row, text="", variable=var, command=command, progress_color=p["primary"], width=38)
|
||||||
if disabled:
|
if disabled:
|
||||||
switch.configure(state="disabled")
|
switch.configure(state="disabled")
|
||||||
switch.pack(side="right", padx=(6, 0))
|
switch.pack(side="right", padx=(8, 0), pady=(8, 0))
|
||||||
row.var = var # type: ignore[attr-defined]
|
row.var = var # type: ignore[attr-defined]
|
||||||
row.switch = switch # type: ignore[attr-defined]
|
row.switch = switch # type: ignore[attr-defined]
|
||||||
row.get = lambda: bool(var.get()) # type: ignore[attr-defined]
|
row.get = lambda: bool(var.get()) # type: ignore[attr-defined]
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ PALETTES: Dict[str, dict] = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
DEFAULT_THEME = "sombre"
|
DEFAULT_THEME = "clair"
|
||||||
|
|
||||||
THEME_LABELS = {
|
THEME_LABELS = {
|
||||||
"sombre": "🌙 Sombre",
|
"sombre": "🌙 Sombre",
|
||||||
|
|||||||
@@ -22,7 +22,15 @@ def font(size: int = 13, weight: str = "normal") -> "ctk.CTkFont":
|
|||||||
class Card(ctk.CTkFrame):
|
class Card(ctk.CTkFrame):
|
||||||
"""Carte maquette : fond `card`, bordure `card_border`, titre uppercase optionnel."""
|
"""Carte maquette : fond `card`, bordure `card_border`, titre uppercase optionnel."""
|
||||||
|
|
||||||
def __init__(self, master, palette: dict, title: Optional[str] = None, **kwargs):
|
def __init__(
|
||||||
|
self,
|
||||||
|
master,
|
||||||
|
palette: dict,
|
||||||
|
title: Optional[str] = None,
|
||||||
|
help_text: Optional[str] = None,
|
||||||
|
help_title: Optional[str] = None,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
master,
|
master,
|
||||||
fg_color=palette["card"],
|
fg_color=palette["card"],
|
||||||
@@ -34,13 +42,22 @@ class Card(ctk.CTkFrame):
|
|||||||
self._palette = palette
|
self._palette = palette
|
||||||
self.body = self # alias pour clarté
|
self.body = self # alias pour clarté
|
||||||
if title:
|
if title:
|
||||||
|
header = ctk.CTkFrame(self, fg_color="transparent")
|
||||||
|
header.pack(fill="x", padx=16, pady=(14, 8))
|
||||||
ctk.CTkLabel(
|
ctk.CTkLabel(
|
||||||
self,
|
header,
|
||||||
text=title.upper(),
|
text=title.upper(),
|
||||||
text_color=palette["text_dim"],
|
text_color=palette["text_dim"],
|
||||||
font=font(11, "bold"),
|
font=font(11, "bold"),
|
||||||
anchor="w",
|
anchor="w",
|
||||||
).pack(anchor="w", padx=16, pady=(14, 8))
|
).pack(side="left", fill="x", expand=True)
|
||||||
|
if help_text:
|
||||||
|
HelpButton(
|
||||||
|
header,
|
||||||
|
palette,
|
||||||
|
help_text,
|
||||||
|
title=help_title or title.lstrip("⚙️👤🏷️⬛🧠📝🛡️🔍✅📤📥 ").strip() or "Aide",
|
||||||
|
).pack(side="right", padx=(8, 0))
|
||||||
|
|
||||||
|
|
||||||
def primary_button(master, palette: dict, text: str, command=None, large: bool = False):
|
def primary_button(master, palette: dict, text: str, command=None, large: bool = False):
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ def test_usage_tab_survives_theme_change(app):
|
|||||||
|
|
||||||
def test_main_tab_renamed_to_administration():
|
def test_main_tab_renamed_to_administration():
|
||||||
"""Retour Dom #2 : l'onglet principal Configuration devient Administration."""
|
"""Retour Dom #2 : l'onglet principal Configuration devient Administration."""
|
||||||
|
pytest.importorskip("customtkinter")
|
||||||
from gui_v6.app import _TABS
|
from gui_v6.app import _TABS
|
||||||
|
|
||||||
labels = [label for _, label in _TABS]
|
labels = [label for _, label in _TABS]
|
||||||
@@ -56,6 +57,7 @@ def test_main_tab_renamed_to_administration():
|
|||||||
def test_no_separate_rules_subtab():
|
def test_no_separate_rules_subtab():
|
||||||
"""Retour Dom : les règles appartiennent au profil → plus de sous-onglet
|
"""Retour Dom : les règles appartiennent au profil → plus de sous-onglet
|
||||||
« Règles » séparé (et donc plus de « Règles 2 » incompréhensible)."""
|
« Règles » séparé (et donc plus de « Règles 2 » incompréhensible)."""
|
||||||
|
pytest.importorskip("customtkinter")
|
||||||
from gui_v6.tabs.tab_config import _SUBTABS
|
from gui_v6.tabs.tab_config import _SUBTABS
|
||||||
|
|
||||||
keys = [key for key, _ in _SUBTABS]
|
keys = [key for key, _ in _SUBTABS]
|
||||||
@@ -99,6 +101,25 @@ def test_beta_label_in_product_identity(app):
|
|||||||
assert any("bêta" in t or "beta" in t for t in texts)
|
assert any("bêta" in t or "beta" in t for t in texts)
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_theme_is_light():
|
||||||
|
"""Retour Dom : le thème clair est le thème par défaut de la GUI."""
|
||||||
|
from gui_v6 import theme as theme_mod
|
||||||
|
|
||||||
|
assert theme_mod.DEFAULT_THEME == "clair"
|
||||||
|
|
||||||
|
|
||||||
|
def test_about_uses_user_facing_database_label(app):
|
||||||
|
"""Retour Dom : éviter le terme technique anglais « Gazetteers » dans À propos."""
|
||||||
|
app._show("about")
|
||||||
|
app.update_idletasks()
|
||||||
|
|
||||||
|
texts = _all_texts(app._tab_frames["about"])
|
||||||
|
joined = " | ".join(texts)
|
||||||
|
assert "bases de données" in joined.lower()
|
||||||
|
assert "Gazetteers" not in joined
|
||||||
|
assert "Rechercher une mise à jour" in joined
|
||||||
|
|
||||||
|
|
||||||
def _count_help_buttons(widget) -> int:
|
def _count_help_buttons(widget) -> int:
|
||||||
from gui_v6.ui_kit import HelpButton
|
from gui_v6.ui_kit import HelpButton
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,18 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from gui_v6.tabs.tab_config import CONFIG_INTERACTION_CONTRACT, CONFIG_MOCKUP_SECTIONS
|
import pytest
|
||||||
|
|
||||||
|
pytest.importorskip("customtkinter")
|
||||||
|
|
||||||
|
from gui_v6.tabs.tab_config import (
|
||||||
|
CONFIG_INTERACTION_CONTRACT,
|
||||||
|
CONFIG_MOCKUP_SECTIONS,
|
||||||
|
MINI_TOGGLE_HEIGHT,
|
||||||
|
MINI_TOGGLE_HINT_FONT_SIZE,
|
||||||
|
MINI_TOGGLE_LABEL_FONT_SIZE,
|
||||||
|
_DETECTION_OPTIONS,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_config_mockup_sections_cover_admin_surface():
|
def test_config_mockup_sections_cover_admin_surface():
|
||||||
@@ -42,3 +53,12 @@ def test_config_interaction_contract_prebuilds_panels_and_mask_editor():
|
|||||||
"clear_page",
|
"clear_page",
|
||||||
"apply_template_selection",
|
"apply_template_selection",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_detection_rows_are_readable_in_light_theme():
|
||||||
|
"""Retour Dom : les sous-labels de la colonne détection doivent rester lisibles."""
|
||||||
|
assert ("Noms et prénoms", "Annuaire + IA") in _DETECTION_OPTIONS
|
||||||
|
assert ("Noms et prénoms", "Bases de données + IA") not in _DETECTION_OPTIONS
|
||||||
|
assert MINI_TOGGLE_HEIGHT >= 44
|
||||||
|
assert MINI_TOGGLE_LABEL_FONT_SIZE >= 12
|
||||||
|
assert MINI_TOGGLE_HINT_FONT_SIZE >= 11
|
||||||
|
|||||||
@@ -57,7 +57,11 @@ def test_kwargs_defaults_v5_like():
|
|||||||
def test_kwargs_with_loaded_managers():
|
def test_kwargs_with_loaded_managers():
|
||||||
settings = EngineSettings(enable_eds=True, enable_gliner=True)
|
settings = EngineSettings(enable_eds=True, enable_gliner=True)
|
||||||
counter = {"camembert": 0, "eds": 0, "gliner": 0}
|
counter = {"camembert": 0, "eds": 0, "gliner": 0}
|
||||||
managers = NerManagers(settings, factories=_counting_factories(counter))
|
managers = NerManagers(
|
||||||
|
settings,
|
||||||
|
factories=_counting_factories(counter),
|
||||||
|
caps_provider=_caps_provider(eds_ok=True, gliner_ok=True),
|
||||||
|
)
|
||||||
managers.ensure_loaded()
|
managers.ensure_loaded()
|
||||||
kwargs = build_engine_kwargs(settings, managers)
|
kwargs = build_engine_kwargs(settings, managers)
|
||||||
assert kwargs["use_hf"] is True
|
assert kwargs["use_hf"] is True
|
||||||
@@ -89,7 +93,11 @@ def test_managers_not_loaded_on_init():
|
|||||||
def test_managers_load_once_and_state():
|
def test_managers_load_once_and_state():
|
||||||
settings = EngineSettings(enable_eds=True)
|
settings = EngineSettings(enable_eds=True)
|
||||||
counter = {"camembert": 0, "eds": 0, "gliner": 0}
|
counter = {"camembert": 0, "eds": 0, "gliner": 0}
|
||||||
managers = NerManagers(settings, factories=_counting_factories(counter))
|
managers = NerManagers(
|
||||||
|
settings,
|
||||||
|
factories=_counting_factories(counter),
|
||||||
|
caps_provider=_caps_provider(eds_ok=True, gliner_ok=True),
|
||||||
|
)
|
||||||
assert managers.state == ManagerState.NOT_LOADED
|
assert managers.state == ManagerState.NOT_LOADED
|
||||||
assert managers.ensure_loaded() == ManagerState.READY
|
assert managers.ensure_loaded() == ManagerState.READY
|
||||||
assert managers.ensure_loaded() == ManagerState.READY # idempotent
|
assert managers.ensure_loaded() == ManagerState.READY # idempotent
|
||||||
@@ -121,7 +129,11 @@ def test_optional_manager_failure_is_tolerated():
|
|||||||
|
|
||||||
return {"camembert": camembert, "eds": eds, "gliner": gliner}
|
return {"camembert": camembert, "eds": eds, "gliner": gliner}
|
||||||
|
|
||||||
managers = NerManagers(settings, factories=factories())
|
managers = NerManagers(
|
||||||
|
settings,
|
||||||
|
factories=factories(),
|
||||||
|
caps_provider=_caps_provider(eds_ok=True, gliner_ok=True),
|
||||||
|
)
|
||||||
assert managers.ensure_loaded() == ManagerState.READY # gliner ko ne bloque pas
|
assert managers.ensure_loaded() == ManagerState.READY # gliner ko ne bloque pas
|
||||||
assert managers.use_hf is True
|
assert managers.use_hf is True
|
||||||
|
|
||||||
|
|||||||
@@ -28,9 +28,11 @@ class FakeResponse:
|
|||||||
class FakeSession:
|
class FakeSession:
|
||||||
"""Session HTTP mockable : enregistre les appels, renvoie des réponses scriptées."""
|
"""Session HTTP mockable : enregistre les appels, renvoie des réponses scriptées."""
|
||||||
|
|
||||||
def __init__(self, response=None, exc=None):
|
def __init__(self, response=None, exc=None, get_response=None, get_exc=None):
|
||||||
self._response = response
|
self._response = response
|
||||||
self._exc = exc
|
self._exc = exc
|
||||||
|
self._get_response = response if get_response is None else get_response
|
||||||
|
self._get_exc = exc if get_exc is None else get_exc
|
||||||
self.calls = []
|
self.calls = []
|
||||||
|
|
||||||
def post(self, url, json, timeout):
|
def post(self, url, json, timeout):
|
||||||
@@ -39,6 +41,12 @@ class FakeSession:
|
|||||||
raise self._exc
|
raise self._exc
|
||||||
return self._response
|
return self._response
|
||||||
|
|
||||||
|
def get(self, url, timeout):
|
||||||
|
self.calls.append({"url": url, "timeout": timeout})
|
||||||
|
if self._get_exc is not None:
|
||||||
|
raise self._get_exc
|
||||||
|
return self._get_response
|
||||||
|
|
||||||
|
|
||||||
def _client(tmp_path, session):
|
def _client(tmp_path, session):
|
||||||
store = LicenseStore(tmp_path / "license.json")
|
store = LicenseStore(tmp_path / "license.json")
|
||||||
@@ -173,6 +181,29 @@ def test_local_status_reads_store(tmp_path):
|
|||||||
assert status.license_ref == "LIC-7"
|
assert status.license_ref == "LIC-7"
|
||||||
|
|
||||||
|
|
||||||
|
def test_latest_version_reads_active_artifact(tmp_path):
|
||||||
|
payload = {
|
||||||
|
"version": "v11.0-beta",
|
||||||
|
"channel": "beta",
|
||||||
|
"filename": "Anonymisation-Setup.exe",
|
||||||
|
}
|
||||||
|
session = FakeSession(get_response=FakeResponse(200, payload))
|
||||||
|
client, _ = _client(tmp_path, session)
|
||||||
|
|
||||||
|
version = client.latest_version()
|
||||||
|
|
||||||
|
assert version == payload
|
||||||
|
assert session.calls[0]["url"] == "https://portail.example/api/v1/version"
|
||||||
|
|
||||||
|
|
||||||
|
def test_latest_version_unavailable_on_404_or_network_error(tmp_path):
|
||||||
|
client_404, _ = _client(tmp_path, FakeSession(get_response=FakeResponse(404, {"detail": "No active version"})))
|
||||||
|
assert client_404.latest_version() is None
|
||||||
|
|
||||||
|
client_down, _ = _client(tmp_path, FakeSession(get_exc=TimeoutError("timeout")))
|
||||||
|
assert client_down.latest_version() is None
|
||||||
|
|
||||||
|
|
||||||
def test_status_never_exposes_token():
|
def test_status_never_exposes_token():
|
||||||
# Le statut ne porte pas de token : la repr ne peut pas le fuiter.
|
# Le statut ne porte pas de token : la repr ne peut pas le fuiter.
|
||||||
status = LicenseStatus.from_payload({"status": "active", "license_ref": "LIC-1"})
|
status = LicenseStatus.from_payload({"status": "active", "license_ref": "LIC-1"})
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ def test_attach_tooltip_does_not_break_widget(ctk_root):
|
|||||||
|
|
||||||
def test_subtabs_include_editable_profils():
|
def test_subtabs_include_editable_profils():
|
||||||
"""Retour Dom : sous-onglet Profils réintroduit (éditeur)."""
|
"""Retour Dom : sous-onglet Profils réintroduit (éditeur)."""
|
||||||
|
pytest.importorskip("customtkinter")
|
||||||
from gui_v6.tabs.tab_config import _SUBTABS
|
from gui_v6.tabs.tab_config import _SUBTABS
|
||||||
|
|
||||||
keys = [k for k, _ in _SUBTABS]
|
keys = [k for k, _ in _SUBTABS]
|
||||||
@@ -297,6 +298,21 @@ def test_regles_moved_into_profils(ctk_root, tmp_path, monkeypatch):
|
|||||||
tab.destroy()
|
tab.destroy()
|
||||||
|
|
||||||
|
|
||||||
|
def test_profile_masking_does_not_expose_templates_folder_button(ctk_root, tmp_path, monkeypatch):
|
||||||
|
"""Retour Dom : le bouton Dossier ouvrait un navigateur et n'aide pas l'utilisateur."""
|
||||||
|
from gui_v6.tabs import tab_config
|
||||||
|
|
||||||
|
monkeypatch.setattr(tab_config, "_app_base_dir", lambda: tmp_path)
|
||||||
|
tab = tab_config.ConfigTab(ctk_root)
|
||||||
|
tab._show_sub("pro")
|
||||||
|
tab.update_idletasks()
|
||||||
|
|
||||||
|
texts = _all_texts(tab._panels["pro"])
|
||||||
|
assert "🖊 Ouvrir l'éditeur de masque" in texts
|
||||||
|
assert "📁 Dossier" not in texts
|
||||||
|
tab.destroy()
|
||||||
|
|
||||||
|
|
||||||
def test_unavailable_engines_disabled_in_reglages(ctk_root, tmp_path, monkeypatch):
|
def test_unavailable_engines_disabled_in_reglages(ctk_root, tmp_path, monkeypatch):
|
||||||
"""Honnêteté moteurs : EDS-Pseudo / GLiNER non embarqués → switch désactivé
|
"""Honnêteté moteurs : EDS-Pseudo / GLiNER non embarqués → switch désactivé
|
||||||
et état forcé à False ; CamemBERT-bio reste actif."""
|
et état forcé à False ; CamemBERT-bio reste actif."""
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ _REQUIRED_TOKENS = {
|
|||||||
|
|
||||||
def test_four_themes_present():
|
def test_four_themes_present():
|
||||||
assert set(theme_mod.PALETTES) == {"sombre", "clair", "medical", "neutre"}
|
assert set(theme_mod.PALETTES) == {"sombre", "clair", "medical", "neutre"}
|
||||||
assert theme_mod.DEFAULT_THEME == "sombre"
|
assert theme_mod.DEFAULT_THEME == "clair"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("name", ["sombre", "clair", "medical", "neutre"])
|
@pytest.mark.parametrize("name", ["sombre", "clair", "medical", "neutre"])
|
||||||
@@ -31,11 +31,11 @@ def test_palette_has_all_tokens(name):
|
|||||||
|
|
||||||
|
|
||||||
def test_default_palette_matches_mockup():
|
def test_default_palette_matches_mockup():
|
||||||
p = theme_mod.get_palette("sombre")
|
p = theme_mod.get_palette(theme_mod.DEFAULT_THEME)
|
||||||
assert p["bg"] == "#1a1a2e"
|
assert p["bg"] == "#cdd2da"
|
||||||
assert p["card"] == "#16213e"
|
assert p["card"] == "#ffffff"
|
||||||
assert p["primary"] == "#e94560"
|
assert p["primary"] == "#c93050"
|
||||||
assert p["accent"] == "#f5a623"
|
assert p["accent"] == "#b45309"
|
||||||
|
|
||||||
|
|
||||||
def test_get_palette_fallback():
|
def test_get_palette_fallback():
|
||||||
|
|||||||
Reference in New Issue
Block a user