Améliorations GUI
This commit is contained in:
+84
-4
@@ -46,6 +46,19 @@ def process_status(return_code: int, interrupted: bool = False) -> str:
|
||||
return "failed"
|
||||
|
||||
|
||||
def has_manual_conflicts(path: Path) -> bool:
|
||||
"""Return whether a manual-resolution file contains an instruction."""
|
||||
try:
|
||||
lines = path.read_text(encoding="utf-8").splitlines()
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
except (OSError, UnicodeError):
|
||||
# If the file cannot be inspected, keep the step visible rather than
|
||||
# silently claiming that there is nothing to resolve.
|
||||
return True
|
||||
return any(line.strip() and not line.lstrip().startswith("###") for line in lines)
|
||||
|
||||
|
||||
class CopienatorApp(tk.Tk):
|
||||
def __init__(
|
||||
self,
|
||||
@@ -354,6 +367,54 @@ class CopienatorApp(tk.Tk):
|
||||
self._save_current_form()
|
||||
self.current_step = step
|
||||
self._render_step()
|
||||
self._handle_first_visit(step)
|
||||
|
||||
def _handle_first_visit(self, step: StepDefinition) -> None:
|
||||
if not self.state_store.evaluation:
|
||||
return
|
||||
entry = self.state_store.step(step.id)
|
||||
if entry.get("visited"):
|
||||
return
|
||||
|
||||
# Persist this before scheduling an action so selection callbacks cannot
|
||||
# trigger the same automatic behavior twice.
|
||||
self.state_store.update_step(step.id, visited=True)
|
||||
|
||||
if step.skip_for_live_correction:
|
||||
correction = self.state_store.step("correction")
|
||||
if correction.get("variant", "live") == "live":
|
||||
self.after_idle(
|
||||
lambda step_id=step.id: self._automatic_skip(
|
||||
step_id, "correction immédiate sélectionnée"
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if step.skip_without_manual_conflicts:
|
||||
evaluation = self.evaluation
|
||||
conflicts = evaluation / "manual_resolutions.txt" if evaluation else None
|
||||
if conflicts is None or not has_manual_conflicts(conflicts):
|
||||
self.after_idle(
|
||||
lambda step_id=step.id: self._automatic_skip(
|
||||
step_id, "aucun conflit manuel détecté"
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if step.auto_start_first_visit and not entry.get("status") and not self._artifacts_exist(step):
|
||||
self.after_idle(lambda step_id=step.id: self._automatic_start(step_id))
|
||||
|
||||
def _automatic_start(self, step_id: str) -> None:
|
||||
if self.runner.running or not self.current_step or self.current_step.id != step_id:
|
||||
return
|
||||
self.info_var.set(f"{self.current_step.title} : démarrage automatique.")
|
||||
self._run_current_step()
|
||||
|
||||
def _automatic_skip(self, step_id: str, reason: str) -> None:
|
||||
if self.runner.running or not self.current_step or self.current_step.id != step_id:
|
||||
return
|
||||
self._mark_step("skipped", automatic=True, reason=reason)
|
||||
self._move_selection_from(step_id, 1)
|
||||
|
||||
def _render_step(self) -> None:
|
||||
step = self.current_step
|
||||
@@ -601,15 +662,27 @@ class CopienatorApp(tk.Tk):
|
||||
self._populate_tree()
|
||||
self._update_controls()
|
||||
|
||||
def _mark_step(self, status: str) -> None:
|
||||
def _mark_step(
|
||||
self, status: str, *, automatic: bool = False, reason: str | None = None
|
||||
) -> None:
|
||||
if not self.current_step or not self.state_store.evaluation:
|
||||
return
|
||||
self._save_current_form()
|
||||
self.state_store.invalidate_after([item.id for item in self.steps], self.current_step.id)
|
||||
self.state_store.update_step(self.current_step.id, status=status)
|
||||
self.state_store.add_history({"step": self.current_step.id, "status": status, "manual": True})
|
||||
history: dict[str, object] = {
|
||||
"step": self.current_step.id,
|
||||
"status": status,
|
||||
"manual": not automatic,
|
||||
}
|
||||
if reason:
|
||||
history["reason"] = reason
|
||||
self.state_store.add_history(history)
|
||||
self._populate_tree()
|
||||
self.info_var.set(f"{self.current_step.title} : {STATUS_LABELS.get(status, status).lower()}.")
|
||||
detail = f" ({reason})" if reason else ""
|
||||
self.info_var.set(
|
||||
f"{self.current_step.title} : {STATUS_LABELS.get(status, status).lower()}{detail}."
|
||||
)
|
||||
|
||||
def _skip_step(self) -> None:
|
||||
if self.current_step and self.current_step.optional:
|
||||
@@ -652,6 +725,8 @@ class CopienatorApp(tk.Tk):
|
||||
self.active_step_id = None
|
||||
self._populate_tree()
|
||||
self._update_controls()
|
||||
if status == "success":
|
||||
self.after_idle(lambda completed_id=step_id: self._move_selection_from(completed_id, 1))
|
||||
|
||||
def _send_input(self) -> None:
|
||||
text = self.stdin_var.get()
|
||||
@@ -703,12 +778,17 @@ class CopienatorApp(tk.Tk):
|
||||
def _move_selection(self, delta: int) -> None:
|
||||
if not self.current_step:
|
||||
return
|
||||
self._move_selection_from(self.current_step.id, delta)
|
||||
|
||||
def _move_selection_from(self, step_id: str, delta: int) -> None:
|
||||
ids = [step.id for step in self.steps]
|
||||
try:
|
||||
index = ids.index(self.current_step.id)
|
||||
index = ids.index(step_id)
|
||||
except ValueError:
|
||||
return
|
||||
target = max(0, min(len(ids) - 1, index + delta))
|
||||
if target == index:
|
||||
return
|
||||
self.tree.selection_set(ids[target])
|
||||
self.tree.see(ids[target])
|
||||
|
||||
|
||||
@@ -46,6 +46,9 @@ class StepDefinition:
|
||||
personal: bool = False
|
||||
requires: tuple[str, ...] = ()
|
||||
artifacts: tuple[str, ...] = ()
|
||||
auto_start_first_visit: bool = False
|
||||
skip_for_live_correction: bool = False
|
||||
skip_without_manual_conflicts: bool = False
|
||||
|
||||
@property
|
||||
def is_manual(self) -> bool:
|
||||
@@ -201,6 +204,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
||||
arguments=(arg_target(),),
|
||||
requires=("Copies",),
|
||||
artifacts=("Copies/Copie*/*",),
|
||||
auto_start_first_visit=True,
|
||||
),
|
||||
StepDefinition(
|
||||
"grouping",
|
||||
@@ -211,6 +215,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
||||
arguments=(arg_target("Dossier de l’évaluation"),),
|
||||
requires=("Copies",),
|
||||
artifacts=("Par label",),
|
||||
auto_start_first_visit=True,
|
||||
),
|
||||
StepDefinition(
|
||||
"verify_groups",
|
||||
@@ -258,6 +263,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
||||
arguments=(arg_target("Dossier de l’évaluation"),),
|
||||
optional=True,
|
||||
artifacts=("batch_jobs.json",),
|
||||
skip_for_live_correction=True,
|
||||
),
|
||||
StepDefinition(
|
||||
"batch_status",
|
||||
@@ -267,6 +273,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
||||
(python("default", "État des batchs", "batch_status.py"),),
|
||||
arguments=(ArgumentSpec("download", "Télécharger le job", "text", "--download"),),
|
||||
optional=True,
|
||||
skip_for_live_correction=True,
|
||||
),
|
||||
StepDefinition(
|
||||
"fetch_batches",
|
||||
@@ -276,6 +283,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
||||
(python("default", "Récupération", "fetch_batched_results.py"),),
|
||||
arguments=(arg_target("Dossier de l’évaluation"),),
|
||||
optional=True,
|
||||
skip_for_live_correction=True,
|
||||
),
|
||||
StepDefinition(
|
||||
"post_correction",
|
||||
@@ -295,6 +303,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
||||
arguments=(arg_target("Dossier de l’évaluation"),),
|
||||
optional=True,
|
||||
requires=("manual_resolutions.txt", "correction.json"),
|
||||
skip_without_manual_conflicts=True,
|
||||
),
|
||||
StepDefinition(
|
||||
"annotation",
|
||||
|
||||
Reference in New Issue
Block a user