From 42a5b9778e4e58b9c4c922f7338055f1df6eb455 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Miquel?= Date: Tue, 22 Sep 2026 12:57:37 +0200 Subject: [PATCH] personal workflow improvements --- copienator_gui/app.py | 53 +++++++++++++++++++++++++++++++++-- copienator_gui/workflow.py | 7 +++-- tests/test_gui_convenience.py | 23 +++++++++++++++ 3 files changed, 77 insertions(+), 6 deletions(-) diff --git a/copienator_gui/app.py b/copienator_gui/app.py index 87ae9cd..b4de6d2 100644 --- a/copienator_gui/app.py +++ b/copienator_gui/app.py @@ -25,6 +25,7 @@ from .diagnostics import collect_diagnostics from .batch_monitor import BatchMonitor from .giving_names import GivingNamesPanel, find_name_issues from .notifications import notify_desktop +from .syncthing import SyncthingPanel from .manual_resolution import ManualResolutionPanel from .refaire import SECTION as REFAIRE_SECTION from .refaire import ( @@ -426,6 +427,7 @@ class CopienatorApp(tk.Tk): self.form.columnconfigure(1, weight=1) command_box = ttk.LabelFrame(self.detail, text="Commande", padding=6) + self.command_box = command_box command_box.grid(row=4, column=0, sticky="ew", pady=(8, 5)) command_box.columnconfigure(0, weight=1) ttk.Label(command_box, textvariable=self.command_var, wraplength=690, justify="left").grid( @@ -782,6 +784,10 @@ class CopienatorApp(tk.Tk): if not step: return self._rendering = True + if step.id == "personal_read": + self.command_box.grid_remove() + else: + self.command_box.grid() redo = step.section == REFAIRE_SECTION expanded_form = redo or step.id in { "page_splitter", @@ -907,12 +913,32 @@ class CopienatorApp(tk.Tk): row += 1 self.run_button.configure(text="Enregistrer la sélection" if step.id == "refaire_selection" else ("Marquer terminée" if step.is_manual else "Exécuter")) + if step.id == "personal_create": + self.run_button.configure(text="Marquer Réussie") + if step.id == "personal_read": + self.run_button.configure(text="Marquer réussie") self.skip_button.configure(state="normal" if step.optional else "disabled") self._rendering = False self._update_command_preview() self._update_controls() def _render_context_controls(self, step: StepDefinition, row: int) -> int: + if step.id == "personal_read": + controls = ttk.Frame(self.form) + controls.grid(row=row, column=0, columnspan=2, sticky="w", pady=8) + self.review_buttons = [] + for command in ("re", "wsn", "we"): + button = ttk.Button( + controls, text=f"gestion_classe {command}", + command=lambda command=command: self._run_current_step(review_command=command), + ) + button.pack(side="left", padx=(0, 6)) + self.review_buttons.append(button) + return row + 1 + if step.id == "export": + self.syncthing_panel = SyncthingPanel(self.form) + self.syncthing_panel.grid(row=row, column=0, columnspan=2, sticky="ew", pady=8) + return row + 1 if step.id == "batch_status": controls = ttk.Frame(self.form) controls.grid(row=row, column=0, columnspan=2, sticky="w", pady=8) @@ -1407,9 +1433,13 @@ class CopienatorApp(tk.Tk): return False return True - def _run_current_step(self) -> None: + def _run_current_step(self, *, syncthing_ready=False, review_command=None) -> None: step = self.current_step evaluation = self.evaluation + if review_command is not None and ( + not step or step.id != "personal_read" or review_command not in {"re", "wsn", "we"} + ): + return if not step or not evaluation or not self.state_store.evaluation: messagebox.showerror("Évaluation absente", "Chargez d’abord un dossier d’évaluation.") return @@ -1427,7 +1457,8 @@ class CopienatorApp(tk.Tk): except (OSError, ValueError, TypeError) as exc: messagebox.showerror("Sélection à vérifier", str(exc)) return - if step.is_manual: + if (step.is_manual or step.id == "personal_create" + or (step.id == "personal_read" and review_command is None)): self._mark_step("success") self._move_selection_from(step.id, 1) return @@ -1456,6 +1487,8 @@ class CopienatorApp(tk.Tk): return variant = self._current_variant() command = self._make_command() + if step.id == "personal_read": + command = ["gestion_classe", review_command] if variant.dangerous and not messagebox.askyesno( "Nettoyage irréversible" if step.id == "clean" else "Confirmation requise", variant.danger_warning @@ -1464,6 +1497,13 @@ class CopienatorApp(tk.Tk): ): return + if step.id == "export" and not syncthing_ready: + def continue_export(): + if self.current_step is step and self.evaluation == evaluation: + self._run_current_step(syncthing_ready=True) + self.syncthing_panel.ensure_started(continue_export) + return + self._save_current_form() run_values = self._values() ordered_ids = self._progression_ids(step.id) @@ -1701,7 +1741,7 @@ class CopienatorApp(tk.Tk): return stored_status = ( "detected" - if step_id == "giving_names" and status == "success" + if step_id in {"giving_names", "personal_read"} and status == "success" else status ) self.state_store.update_step( @@ -1732,6 +1772,10 @@ class CopienatorApp(tk.Tk): self.active_step_id = None self._populate_tree() self._update_controls() + if step_id == "personal_read": + if status == "success": + self.info_var.set("Commande terminée. Poursuivez la relecture, puis cliquez sur « Marquer réussie ».") + return if step_id == "giving_names" and status == "success": if self.giving_names_panel: self.giving_names_panel.reload() @@ -1820,6 +1864,9 @@ class CopienatorApp(tk.Tk): def _update_controls(self) -> None: running = bool(self.active_step_id) or self.runner.running + if self.current_step and self.current_step.id == "personal_read": + for button in self.review_buttons: + button.configure(state="disabled" if running or not self.state_store.evaluation else "normal") if self.current_step and self.current_step.id == "batch_status": self.watch_batches_button.configure(state="disabled" if running or self.batch_monitor.active or not self.state_store.evaluation else "normal") self.stop_watch_batches_button.configure(state="normal" if self.batch_monitor.active else "disabled") diff --git a/copienator_gui/workflow.py b/copienator_gui/workflow.py index c54a4f3..6a3c1cf 100644 --- a/copienator_gui/workflow.py +++ b/copienator_gui/workflow.py @@ -633,7 +633,8 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]: "personal_create", "Étapes personnelles", "Créer l’interro dans gestion_classe", - "Exécute gestion_classe ne.", + "Copiez la commande gestion_classe ne et exécutez-la dans un terminal, " + "puis cliquez sur « Marquer Réussie ».", (CommandVariant("default", "gestion_classe ne", "gestion_classe", "external", ("ne",)),), personal=True, ), @@ -667,14 +668,14 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]: "personal_read", "Étapes personnelles", "Relire avec gestion_classe", - "Exécute gestion_classe re.", + "Utilisez les boutons gestion_classe re, wsn et we, puis cliquez sur « Marquer réussie ».", (CommandVariant("default", "gestion_classe re", "gestion_classe", "external", ("re",)),), personal=True, ), StepDefinition( "personal_sent", "Étapes personnelles", - "Marquer comme envoyé", + "Écrire les notes finales", "Exécute gestion_classe wsent.", (CommandVariant("default", "gestion_classe wsent", "gestion_classe", "external", ("wsent",)),), personal=True, diff --git a/tests/test_gui_convenience.py b/tests/test_gui_convenience.py index df35848..ac3c907 100644 --- a/tests/test_gui_convenience.py +++ b/tests/test_gui_convenience.py @@ -27,6 +27,29 @@ class GuiConvenienceTests(unittest.TestCase): self.app.destroy() self.temp.cleanup() + def test_personal_review_commands_require_explicit_completion(self): + from copienator_gui.workflow import build_workflow + step = next(step for step in build_workflow(True) if step.id == "personal_read") + self.app.step_by_id[step.id] = step + self.app.current_step = step + self.app._render_step() + self.assertEqual([button.cget("text") for button in self.app.review_buttons], + ["gestion_classe re", "gestion_classe wsn", "gestion_classe we"]) + self.assertEqual(self.app.command_box.winfo_manager(), "") + self.assertEqual(self.app.run_button.cget("text"), "Marquer réussie") + with patch.object(self.app.runner, "start") as start, \ + patch.object(self.app, "_move_selection_from") as advance: + for button, command in zip(self.app.review_buttons, ("re", "wsn", "we")): + button.invoke() + self.assertEqual(start.call_args.args[0], ["gestion_classe", command]) + self.app._finish_process(0, False) + self.assertNotEqual(self.app.state_store.step(step.id)["status"], "success") + advance.assert_not_called() + start.reset_mock() + self.app.run_button.invoke() + start.assert_not_called() + self.assertEqual(self.app.state_store.step(step.id)["status"], "success") + @staticmethod def _label_texts(widget): return [