Améliorations GUI
This commit is contained in:
+160
-18
@@ -8,7 +8,7 @@ from tkinter import filedialog, messagebox, ttk
|
||||
from typing import Any
|
||||
|
||||
from copienator import ExitCode
|
||||
from platform_utils import WindowsLabelError, validate_windows_labels
|
||||
from platform_utils import WindowsLabelError, open_path, validate_windows_labels
|
||||
|
||||
from .diagnostics import collect_diagnostics
|
||||
from .runner import ProcessRunner
|
||||
@@ -35,6 +35,65 @@ STATUS_LABELS = {
|
||||
"detected": "Détectée",
|
||||
}
|
||||
|
||||
DEFAULT_HTTPS_PROXY = "http://10.0.0.1:3128"
|
||||
|
||||
|
||||
def copy_pdf_paths(evaluation: Path) -> list[Path]:
|
||||
"""List the most relevant version of each scanned copy."""
|
||||
locations = (evaluation / "Copies", evaluation, evaluation / "Copies Originales")
|
||||
for location in locations:
|
||||
if not location.is_dir():
|
||||
continue
|
||||
copies = sorted(
|
||||
(
|
||||
path
|
||||
for path in location.glob("*.pdf")
|
||||
if path.name.casefold() not in {"enonce.pdf", "énoncé.pdf"}
|
||||
),
|
||||
key=lambda path: path.name.casefold(),
|
||||
)
|
||||
if copies:
|
||||
return copies
|
||||
return []
|
||||
|
||||
|
||||
def plotting_shortcut_lines() -> list[str]:
|
||||
try:
|
||||
from config import PLOTTING_KB
|
||||
except (ImportError, AttributeError):
|
||||
from default_config import PLOTTING_KB
|
||||
|
||||
labels = (
|
||||
("OK", "valider et passer à la suivante"),
|
||||
("previous", "revenir à la précédente"),
|
||||
("edit", "éditer le fichier JSON"),
|
||||
("open pdf", "ouvrir la copie traitée"),
|
||||
("open original pdf", "ouvrir la copie originale"),
|
||||
("open eval", "ouvrir l’énoncé"),
|
||||
)
|
||||
display_names = {"<Return>": "Entrée", "<Escape>": "Échap"}
|
||||
lines = [
|
||||
f"{display_names.get(PLOTTING_KB[action], PLOTTING_KB[action])} : {description}"
|
||||
for action, description in labels
|
||||
]
|
||||
lines.append("Échap : fermer la fenêtre")
|
||||
return lines
|
||||
|
||||
|
||||
def build_runner_environment(
|
||||
base: dict[str, str], api_key: str, proxy: str, use_proxy: bool
|
||||
) -> dict[str, str]:
|
||||
environment = dict(base)
|
||||
environment["PYTHONUNBUFFERED"] = "1"
|
||||
environment["PYTHONIOENCODING"] = "utf-8"
|
||||
if api_key.strip():
|
||||
environment["GEMINI_API_KEY"] = api_key.strip()
|
||||
environment.pop("HTTPS_PROXY", None)
|
||||
environment.pop("https_proxy", None)
|
||||
if use_proxy and proxy.strip():
|
||||
environment["HTTPS_PROXY"] = proxy.strip()
|
||||
return environment
|
||||
|
||||
|
||||
def process_status(return_code: int, interrupted: bool = False) -> str:
|
||||
if interrupted or return_code == ExitCode.INTERRUPTED:
|
||||
@@ -76,6 +135,7 @@ class CopienatorApp(tk.Tk):
|
||||
self.active_step_id: str | None = None
|
||||
self.current_step: StepDefinition | None = None
|
||||
self.arg_vars: dict[str, tk.Variable] = {}
|
||||
self.copy_paths: dict[str, Path] = {}
|
||||
self._rendering = False
|
||||
|
||||
self.title("Copienator — assistant de correction")
|
||||
@@ -85,7 +145,9 @@ class CopienatorApp(tk.Tk):
|
||||
|
||||
self.evaluation_var = tk.StringVar()
|
||||
self.api_key_var = tk.StringVar(value=os.environ.get("GEMINI_API_KEY", ""))
|
||||
self.proxy_var = tk.StringVar(value=os.environ.get("HTTPS_PROXY", ""))
|
||||
self.proxy_var = tk.StringVar(value=DEFAULT_HTTPS_PROXY)
|
||||
self.use_proxy_var = tk.BooleanVar(value=False)
|
||||
self.copy_var = tk.StringVar()
|
||||
self.variant_var = tk.StringVar()
|
||||
self.extra_var = tk.StringVar()
|
||||
self.command_var = tk.StringVar(value="Sélectionnez une étape.")
|
||||
@@ -113,13 +175,17 @@ class CopienatorApp(tk.Tk):
|
||||
|
||||
top = ttk.Frame(self, padding=(10, 8))
|
||||
top.grid(row=0, column=0, sticky="ew")
|
||||
top.columnconfigure(1, weight=1)
|
||||
ttk.Label(top, text="Évaluation").grid(row=0, column=0, sticky="w", padx=(0, 8))
|
||||
top.columnconfigure(2, weight=1)
|
||||
ttk.Button(top, text="Charger", command=self._load_evaluation).grid(
|
||||
row=0, column=0, padx=(0, 8)
|
||||
)
|
||||
ttk.Label(top, text="Évaluation").grid(row=0, column=1, sticky="w", padx=(0, 8))
|
||||
path_entry = ttk.Entry(top, textvariable=self.evaluation_var)
|
||||
path_entry.grid(row=0, column=1, sticky="ew")
|
||||
path_entry.grid(row=0, column=2, sticky="ew")
|
||||
path_entry.bind("<Return>", lambda _event: self._load_evaluation())
|
||||
ttk.Button(top, text="Parcourir…", command=self._browse_evaluation).grid(row=0, column=2, padx=6)
|
||||
ttk.Button(top, text="Charger", command=self._load_evaluation).grid(row=0, column=3)
|
||||
ttk.Button(top, text="Parcourir…", command=self._browse_evaluation).grid(
|
||||
row=0, column=3, padx=6
|
||||
)
|
||||
ttk.Button(top, text="Diagnostic…", command=self._show_diagnostics).grid(row=0, column=4, padx=(6, 0))
|
||||
|
||||
ttk.Label(top, text="Clé Gemini").grid(row=1, column=0, sticky="w", padx=(0, 8), pady=(7, 0))
|
||||
@@ -127,9 +193,15 @@ class CopienatorApp(tk.Tk):
|
||||
row=1, column=1, sticky="w", pady=(7, 0)
|
||||
)
|
||||
environment = ttk.Frame(top)
|
||||
environment.grid(row=1, column=2, columnspan=2, sticky="e", pady=(7, 0))
|
||||
ttk.Label(environment, text="HTTPS_PROXY").pack(side="left", padx=(0, 6))
|
||||
ttk.Entry(environment, textvariable=self.proxy_var, width=28).pack(side="left")
|
||||
environment.grid(row=1, column=2, columnspan=3, sticky="e", pady=(7, 0))
|
||||
ttk.Checkbutton(
|
||||
environment,
|
||||
text="Utiliser le proxy HTTPS",
|
||||
variable=self.use_proxy_var,
|
||||
command=self._toggle_proxy,
|
||||
).pack(side="left", padx=(0, 6))
|
||||
self.proxy_entry = ttk.Entry(environment, textvariable=self.proxy_var, width=28, state="disabled")
|
||||
self.proxy_entry.pack(side="left")
|
||||
profile = "standard + personnel" if show_personal_steps else "standard"
|
||||
ttk.Label(environment, text=f"Profil : {profile}").pack(side="left", padx=(12, 0))
|
||||
|
||||
@@ -269,6 +341,9 @@ class CopienatorApp(tk.Tk):
|
||||
self.evaluation_var.set(selected)
|
||||
self._load_evaluation()
|
||||
|
||||
def _toggle_proxy(self) -> None:
|
||||
self.proxy_entry.configure(state="normal" if self.use_proxy_var.get() else "disabled")
|
||||
|
||||
def _load_evaluation(self) -> None:
|
||||
evaluation = self.evaluation
|
||||
if not evaluation or not evaluation.is_dir():
|
||||
@@ -437,7 +512,7 @@ class CopienatorApp(tk.Tk):
|
||||
description += "\nPrérequis non détectés : " + ", ".join(missing)
|
||||
self.description_label.configure(text=description)
|
||||
|
||||
row = 0
|
||||
row = self._render_context_controls(step, 0)
|
||||
if len(step.variants) > 1:
|
||||
ttk.Label(self.form, text="Mode").grid(row=row, column=0, sticky="w", pady=4, padx=(0, 8))
|
||||
labels = [variant.label for variant in step.variants]
|
||||
@@ -499,6 +574,74 @@ class CopienatorApp(tk.Tk):
|
||||
self._update_command_preview()
|
||||
self._update_controls()
|
||||
|
||||
def _render_context_controls(self, step: StepDefinition, row: int) -> int:
|
||||
if step.id == "review_persp":
|
||||
ttk.Button(
|
||||
self.form,
|
||||
text="Ouvrir le dossier Persp",
|
||||
command=self._open_persp,
|
||||
).grid(row=row, column=0, columnspan=2, sticky="w", pady=(0, 8))
|
||||
row += 1
|
||||
|
||||
if step.section == "Prétraitement des copies":
|
||||
evaluation = self.evaluation
|
||||
paths = copy_pdf_paths(evaluation) if evaluation else []
|
||||
self.copy_paths = {path.name: path for path in paths}
|
||||
names = list(self.copy_paths)
|
||||
copies = ttk.LabelFrame(self.form, text="Copies détectées", padding=7)
|
||||
copies.grid(row=row, column=0, columnspan=2, sticky="ew", pady=(0, 8))
|
||||
copies.columnconfigure(0, weight=1)
|
||||
summary = (
|
||||
f"{len(names)} copie(s) : {', '.join(names)}"
|
||||
if names
|
||||
else "Aucune copie PDF détectée."
|
||||
)
|
||||
ttk.Label(copies, text=summary, wraplength=570, justify="left").grid(
|
||||
row=0, column=0, columnspan=2, sticky="ew"
|
||||
)
|
||||
self.copy_var.set(names[0] if names else "")
|
||||
selector = ttk.Combobox(
|
||||
copies,
|
||||
textvariable=self.copy_var,
|
||||
values=names,
|
||||
state="readonly" if names else "disabled",
|
||||
)
|
||||
selector.grid(row=1, column=0, sticky="ew", pady=(7, 0))
|
||||
ttk.Button(
|
||||
copies,
|
||||
text="Afficher la copie",
|
||||
command=self._open_selected_copy,
|
||||
state="normal" if names else "disabled",
|
||||
).grid(row=1, column=1, padx=(6, 0), pady=(7, 0))
|
||||
row += 1
|
||||
|
||||
if step.id == "plotting":
|
||||
shortcuts = ttk.LabelFrame(self.form, text="Raccourcis clavier", padding=7)
|
||||
shortcuts.grid(row=row, column=0, columnspan=2, sticky="ew", pady=(0, 8))
|
||||
ttk.Label(
|
||||
shortcuts,
|
||||
text="\n".join(plotting_shortcut_lines()),
|
||||
justify="left",
|
||||
).grid(row=0, column=0, sticky="w")
|
||||
row += 1
|
||||
return row
|
||||
|
||||
def _open_persp(self) -> None:
|
||||
evaluation = self.evaluation
|
||||
self._open_desktop_path(evaluation / "Persp" if evaluation else None, "dossier Persp")
|
||||
|
||||
def _open_selected_copy(self) -> None:
|
||||
self._open_desktop_path(self.copy_paths.get(self.copy_var.get()), "copie")
|
||||
|
||||
def _open_desktop_path(self, path: Path | None, label: str) -> None:
|
||||
if path is None or not path.exists():
|
||||
messagebox.showerror("Élément introuvable", f"Le {label} n’existe pas encore.")
|
||||
return
|
||||
try:
|
||||
open_path(path)
|
||||
except (OSError, RuntimeError) as exc:
|
||||
messagebox.showerror("Ouverture impossible", str(exc))
|
||||
|
||||
def _select_variant(self, index: int) -> None:
|
||||
if not self.current_step or index < 0:
|
||||
return
|
||||
@@ -640,13 +783,12 @@ class CopienatorApp(tk.Tk):
|
||||
assert workspace is not None
|
||||
log_path = workspace.log_path(step.id)
|
||||
|
||||
environment = os.environ.copy()
|
||||
environment["PYTHONUNBUFFERED"] = "1"
|
||||
environment["PYTHONIOENCODING"] = "utf-8"
|
||||
if self.api_key_var.get().strip():
|
||||
environment["GEMINI_API_KEY"] = self.api_key_var.get().strip()
|
||||
if self.proxy_var.get().strip():
|
||||
environment["HTTPS_PROXY"] = self.proxy_var.get().strip()
|
||||
environment = build_runner_environment(
|
||||
os.environ,
|
||||
self.api_key_var.get(),
|
||||
self.proxy_var.get(),
|
||||
self.use_proxy_var.get(),
|
||||
)
|
||||
|
||||
self._append_console(f"\n$ {command_display(command)}\n")
|
||||
try:
|
||||
|
||||
@@ -109,7 +109,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
||||
StepDefinition(
|
||||
"review_persp",
|
||||
"Prétraitement de l’énoncé",
|
||||
"Relire les barèmes dans Persp",
|
||||
"Relire les barèmes",
|
||||
"Étape manuelle facultative : vérifier et modifier les instructions de correction.",
|
||||
(manual("review"),),
|
||||
optional=True,
|
||||
|
||||
+44
-1
@@ -31,7 +31,14 @@ from copienator import (
|
||||
from copienator.annotation_actions import apply_checkbox_actions, apply_score_overrides
|
||||
from copienator.annotation_data import AnnotationLoadResult, load_annotation_data
|
||||
from copienator.filesystem import staged_directory, staged_files
|
||||
from copienator_gui.app import has_manual_conflicts, process_status
|
||||
from copienator_gui.app import (
|
||||
DEFAULT_HTTPS_PROXY,
|
||||
build_runner_environment,
|
||||
copy_pdf_paths,
|
||||
has_manual_conflicts,
|
||||
plotting_shortcut_lines,
|
||||
process_status,
|
||||
)
|
||||
from copienator_gui.diagnostics import collect_diagnostics
|
||||
from copienator_gui.runner import ProcessRunner
|
||||
from copienator_gui.state import StateStore
|
||||
@@ -1396,6 +1403,42 @@ class WorkflowTests(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(skip_without_conflicts, {"manual_resolution"})
|
||||
|
||||
def test_review_persp_has_shorter_title(self) -> None:
|
||||
self.assertEqual(self.steps["review_persp"].title, "Relire les barèmes")
|
||||
|
||||
def test_copy_listing_prefers_processed_copies(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
evaluation = Path(directory)
|
||||
(evaluation / "enonce.pdf").touch()
|
||||
(evaluation / "scan.pdf").touch()
|
||||
copies = evaluation / "Copies"
|
||||
copies.mkdir()
|
||||
(copies / "Copie02.pdf").touch()
|
||||
(copies / "Copie01.pdf").touch()
|
||||
|
||||
self.assertEqual(
|
||||
[path.name for path in copy_pdf_paths(evaluation)],
|
||||
["Copie01.pdf", "Copie02.pdf"],
|
||||
)
|
||||
|
||||
def test_plotting_shortcuts_describe_open_actions(self) -> None:
|
||||
rendered = "\n".join(plotting_shortcut_lines())
|
||||
self.assertIn("ouvrir l’énoncé", rendered)
|
||||
self.assertIn("ouvrir la copie traitée", rendered)
|
||||
self.assertIn("ouvrir la copie originale", rendered)
|
||||
|
||||
def test_proxy_is_opt_in_and_prefilled(self) -> None:
|
||||
base = {"HTTPS_PROXY": "http://system-proxy", "OTHER": "kept"}
|
||||
without_proxy = build_runner_environment(
|
||||
base, " secret ", DEFAULT_HTTPS_PROXY, False
|
||||
)
|
||||
with_proxy = build_runner_environment(base, "", DEFAULT_HTTPS_PROXY, True)
|
||||
|
||||
self.assertNotIn("HTTPS_PROXY", without_proxy)
|
||||
self.assertEqual(without_proxy["GEMINI_API_KEY"], "secret")
|
||||
self.assertEqual(without_proxy["OTHER"], "kept")
|
||||
self.assertEqual(with_proxy["HTTPS_PROXY"], DEFAULT_HTTPS_PROXY)
|
||||
|
||||
def test_manual_conflicts_ignore_blank_and_comment_lines(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "manual_resolutions.txt"
|
||||
|
||||
Reference in New Issue
Block a user