Refaire fixes and GUI support

This commit is contained in:
2026-09-06 19:06:42 +02:00
parent a9897606ac
commit 2ff1a9b7b9
11 changed files with 1664 additions and 116 deletions
+184 -24
View File
@@ -3,11 +3,12 @@ from __future__ import annotations
import os
import queue
import tkinter as tk
from dataclasses import replace
from pathlib import Path
from tkinter import filedialog, messagebox, ttk
from typing import Any
from copienator import ExitCode
from copienator import EvaluationWorkspace, ExitCode, atomic_write_json
from copienator.platform import (
WindowsLabelError,
add_platform_executable_paths,
@@ -16,6 +17,14 @@ from copienator.platform import (
)
from .diagnostics import collect_diagnostics
from .refaire import SECTION as REFAIRE_SECTION
from .refaire import (
RefaireSelection,
available_copies,
load_selection,
resolve_layout,
validate_selection,
)
from .runner import ProcessRunner
from .state import StateStore
from .workflow import (
@@ -154,6 +163,9 @@ class CopienatorApp(tk.Tk):
self.arg_vars: dict[str, tk.Variable] = {}
self.copy_paths: dict[str, Path] = {}
self._rendering = False
self.refaire_panel: RefaireSelection | None = None
self.pending_refaire_commands: list[list[str]] = []
self.refaire_command_index = 0
self.title("Copienator — assistant de correction")
self.geometry("1180x820")
@@ -245,8 +257,19 @@ class CopienatorApp(tk.Tk):
self.title_label.grid(row=0, column=0, sticky="w")
self.description_label = ttk.Label(self.detail, text="", wraplength=680, justify="left")
self.description_label.grid(row=1, column=0, sticky="ew", pady=(5, 8))
self.form = ttk.Frame(self.detail)
self.form.grid(row=2, column=0, sticky="nsew")
form_container = ttk.Frame(self.detail)
form_container.grid(row=2, column=0, sticky="nsew")
form_container.columnconfigure(0, weight=1)
form_container.rowconfigure(0, weight=1)
self.form_canvas = tk.Canvas(form_container, highlightthickness=0, width=1, height=1)
self.form_canvas.grid(row=0, column=0, sticky="nsew")
form_scroll = ttk.Scrollbar(form_container, orient="vertical", command=self.form_canvas.yview)
form_scroll.grid(row=0, column=1, sticky="ns")
self.form_canvas.configure(yscrollcommand=form_scroll.set)
self.form = ttk.Frame(self.form_canvas)
form_window = self.form_canvas.create_window((0, 0), window=self.form, anchor="nw")
self.form.bind("<Configure>", lambda _event: self.form_canvas.configure(scrollregion=self.form_canvas.bbox("all")))
self.form_canvas.bind("<Configure>", lambda event: self.form_canvas.itemconfigure(form_window, width=event.width))
self.form.columnconfigure(1, weight=1)
command_box = ttk.LabelFrame(self.detail, text="Commande", padding=6)
@@ -362,6 +385,9 @@ class CopienatorApp(tk.Tk):
self.proxy_entry.configure(state="normal" if self.use_proxy_var.get() else "disabled")
def _load_evaluation(self) -> None:
if self.active_step_id or self.runner.running:
messagebox.showinfo("Traitement en cours", "Attendez la fin du traitement avant de changer d’évaluation.")
return
evaluation = self.evaluation
if not evaluation or not evaluation.is_dir():
messagebox.showerror("Dossier invalide", "Choisissez un dossier d’évaluation existant.")
@@ -392,13 +418,14 @@ class CopienatorApp(tk.Tk):
def _populate_tree(self) -> None:
selected = self.current_step.id if self.current_step else None
opened = {self.tree.item(item, "text"): self.tree.item(item, "open") for item in self.tree.get_children()}
self.tree.delete(*self.tree.get_children())
section_items: dict[str, str] = {}
for step in self.steps:
if step.section not in section_items:
section_id = f"section:{len(section_items)}"
section_items[step.section] = section_id
self.tree.insert("", "end", iid=section_id, text=step.section, values=("",), open=True)
self.tree.insert("", "end", iid=section_id, text=step.section, values=("",), open=opened.get(step.section, step.section != REFAIRE_SECTION))
status = self._step_status(step)
suffix = " (facultative)" if step.optional else ""
self.tree.insert(
@@ -513,9 +540,16 @@ class CopienatorApp(tk.Tk):
if not step:
return
self._rendering = True
redo = step.section == REFAIRE_SECTION
self.console.configure(height=8 if redo else 14)
self.rowconfigure(1, weight=4 if redo else 3)
self.rowconfigure(2, weight=1 if redo else 2)
self.form_canvas.configure(height=(340 if step.id == "refaire_selection" else 260) if redo else 200)
self.form_canvas.yview_moveto(0)
for child in self.form.winfo_children():
child.destroy()
self.arg_vars.clear()
self.refaire_panel = None
entry = self.state_store.step(step.id) if self.state_store.evaluation else {}
saved_variant = entry.get("variant", step.variants[0].id)
if saved_variant not in {variant.id for variant in step.variants}:
@@ -544,6 +578,8 @@ class CopienatorApp(tk.Tk):
variant = self._current_variant()
evaluation_arg = self._evaluation_arg()
for spec in step.arguments:
if step.section == REFAIRE_SECTION:
continue
if spec.variants and variant.id not in spec.variants:
continue
value = values.get(spec.name, value_for_default(spec.default, evaluation_arg))
@@ -582,27 +618,44 @@ class CopienatorApp(tk.Tk):
row += 1
row += 1
if not step.is_manual:
if not step.is_manual and step.section != REFAIRE_SECTION:
ttk.Label(self.form, text="Arguments supplémentaires").grid(
row=row, column=0, sticky="w", pady=(10, 4), padx=(0, 8)
)
ttk.Entry(self.form, textvariable=self.extra_var).grid(row=row, column=1, sticky="ew", pady=(10, 4))
self.run_button.configure(text="Marquer terminée" if step.is_manual else "Exécuter")
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"))
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 == "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 == REFAIRE_SECTION:
evaluation = self.evaluation
if step.id in {"refaire_selection", "refaire_correct"}:
row = self._correction_folder_buttons(row)
if step.id == "refaire_selection" and evaluation:
choices, preferred = self._annotation_directory_choices("export")
draft = self.state_store.step(step.id).get("values", {})
self.refaire_panel = RefaireSelection(self.form, evaluation, draft, choices, preferred)
self.refaire_panel.grid(row=row, column=0, columnspan=2, sticky="nsew")
else:
try:
entries = self._refaire_selection()
summary = f"{len(entries)} copie(s) sélectionnée(s) :\n" + " ; ".join(f"{name} : {', '.join(labels) or 'toute la copie'}" for name, labels in entries[:5])
if len(entries) > 5:
summary += f" ; et {len(entries) - 5} autres (voir la sélection)."
source = self.state_store.step("refaire_selection").get("values", {}).get("annotation_dir", "")
choice = self.state_store.step("refaire_selection").get("values", {}).get("layout", "auto")
layout = resolve_layout(entries, EvaluationWorkspace(evaluation).read_labels(), choice)
summary += f"\nPassage principal : {source} — PDF à vérifier : {'par question' if layout == 'grouped' else 'par copie'}"
except (OSError, ValueError, TypeError) as exc:
summary = str(exc)
ttk.Label(self.form, text=summary, wraplength=600, justify="left").grid(row=row, column=0, columnspan=2, sticky="w")
return row + 1
if step.id in {"review_persp", "correction"}:
row = self._correction_folder_buttons(row)
if step.section == "Prétraitement des copies":
evaluation = self.evaluation
paths = copy_pdf_paths(evaluation) if evaluation else []
@@ -646,6 +699,14 @@ class CopienatorApp(tk.Tk):
row += 1
return row
def _correction_folder_buttons(self, row: int) -> int:
buttons = ttk.Frame(self.form)
buttons.grid(row=row, column=0, columnspan=2, sticky="w", pady=(0, 6))
for directory, label in (("Sol", "Corrigés (Sol)"), ("Persp", "Consignes de notation (Persp)")):
ttk.Button(buttons, text=label,
command=lambda name=directory: self._open_desktop_path(self.evaluation / name if self.evaluation else None, f"dossier {name}")).pack(side="left", padx=(0, 6))
return row + 1
def _annotation_directory_choices(self, step_id: str) -> tuple[tuple[str, ...], str]:
evaluation = self.evaluation
detected = detected_annotation_directories(evaluation) if evaluation else ()
@@ -696,6 +757,10 @@ class CopienatorApp(tk.Tk):
return next((variant for variant in self.current_step.variants if variant.id == selected), self.current_step.variants[0])
def _values(self) -> dict[str, object]:
if self.current_step and self.current_step.section == REFAIRE_SECTION:
if self.current_step.id == "refaire_selection" and self.refaire_panel:
return self.refaire_panel.values()
return {"target": self._evaluation_arg(), "annotation_dir": self.state_store.step("refaire_selection").get("values", {}).get("annotation_dir", "BGnot")}
return {name: variable.get() for name, variable in self.arg_vars.items()}
def _save_current_form(self) -> None:
@@ -715,9 +780,62 @@ class CopienatorApp(tk.Tk):
evaluation = self.evaluation
return evaluation_argument(self.repository, evaluation) if evaluation else "<évaluation>"
def _progression_ids(self, step_id: str) -> list[str]:
redo = step_id.startswith("refaire_")
return [step.id for step in self.steps if step.id.startswith("refaire_") == redo]
def _refaire_selection(self) -> list[list]:
evaluation = self.evaluation
if not evaluation or not self.state_store.evaluation:
raise ValueError("Chargez dabord une évaluation.")
entry = self.state_store.step("refaire_selection")
if entry.get("status") != "success":
raise ValueError("Enregistrez dabord les copies et les questions à refaire.")
values = entry.get("values", {})
selection = load_selection(evaluation)
if selection != validate_selection(values.get("selection"), available_copies(evaluation), EvaluationWorkspace(evaluation).read_labels()):
raise ValueError("La sélection a changé. Enregistrez-la avant de continuer.")
source = values.get("annotation_dir")
if source not in ANNOTATION_DIRECTORIES or not (evaluation / source).is_dir():
raise ValueError("Choisissez un dossier existant pour le passage principal.")
return selection
def _save_refaire_selection(self) -> None:
if not self.refaire_panel or not self.evaluation:
return
try:
values = self.refaire_panel.values()
entries = validate_selection(values["selection"], available_copies(self.evaluation), EvaluationWorkspace(self.evaluation).read_labels())
source = values["annotation_dir"]
if source not in detected_annotation_directories(self.evaluation):
raise ValueError("Choisissez un dossier existant pour le passage principal.")
atomic_write_json(self.evaluation / "refaire.json", entries)
self._mark_step("success")
self._move_selection_from("refaire_selection", 1)
except (OSError, ValueError, TypeError) as exc:
messagebox.showerror("Sélection à vérifier", str(exc))
def _refaire_commands(self) -> list[list[str]]:
assert self.current_step is not None
selection = self._refaire_selection()
values = self._values()
targets = [self._evaluation_arg()]
if self.current_step.id in {"refaire_review", "refaire_split"}:
copies = available_copies(self.evaluation)
targets = [str(copies[name]) for name, _labels in selection]
variant = self._current_variant()
if self.current_step.id == "refaire_annotate":
choice = self.state_store.step("refaire_selection").get("values", {}).get("layout", "auto")
layout = resolve_layout(selection, EvaluationWorkspace(self.evaluation).read_labels(), choice)
variant = replace(variant, program="annotate-grouped" if layout == "grouped" else "annotate-checks")
return [build_command(self.repository, self.current_step, variant,
{**values, "target": target}, self._evaluation_arg()) for target in targets]
def _make_command(self) -> list[str]:
if not self.current_step:
return []
if self.current_step.section == REFAIRE_SECTION:
return self._refaire_commands()[0]
return build_command(
self.repository,
self.current_step,
@@ -731,11 +849,15 @@ class CopienatorApp(tk.Tk):
if self._rendering or not self.current_step:
return
if self.current_step.is_manual:
self.command_var.set("Étape manuelle — aucune commande ne sera exécutée.")
self.command_var.set("La sélection sera utilisée pour toutes les étapes de ce parcours." if self.current_step.id == "refaire_selection" else "Étape manuelle — aucune commande ne sera exécutée.")
return
try:
self.command_var.set(command_display(self._make_command()))
except ValueError as exc:
commands = self._refaire_commands() if self.current_step.section == REFAIRE_SECTION else [self._make_command()]
preview = "\n".join(command_display(command) for command in commands[:2])
if len(commands) > 2:
preview += f"\nPuis {len(commands) - 2} autres copies, successivement."
self.command_var.set(preview)
except (OSError, ValueError, TypeError) as exc:
self.command_var.set(f"Arguments invalides : {exc}")
def _browse_target_file(self, variable: tk.Variable) -> None:
@@ -764,7 +886,7 @@ class CopienatorApp(tk.Tk):
return False
try:
self._make_command()
except ValueError as exc:
except (OSError, ValueError, TypeError) as exc:
messagebox.showerror("Arguments invalides", str(exc))
return False
return True
@@ -775,11 +897,22 @@ class CopienatorApp(tk.Tk):
if not step or not evaluation or not self.state_store.evaluation:
messagebox.showerror("Évaluation absente", "Chargez dabord un dossier d’évaluation.")
return
if self.runner.running:
if self.active_step_id or self.runner.running:
messagebox.showwarning("Traitement en cours", "Interrompez le traitement actuel avant den lancer un autre.")
return
if step.id == "refaire_selection":
self._save_refaire_selection()
return
if step.section == REFAIRE_SECTION:
try:
self._refaire_selection()
except (OSError, ValueError, TypeError) as exc:
messagebox.showerror("Sélection à vérifier", str(exc))
return
if step.is_manual:
self._mark_step("success")
if step.section == REFAIRE_SECTION:
self._move_selection_from(step.id, 1)
return
if os.name == "nt" and step.id != "statement":
labels_path = evaluation / "labels"
@@ -816,7 +949,7 @@ class CopienatorApp(tk.Tk):
self._save_current_form()
run_values = self._values()
ordered_ids = [item.id for item in self.steps]
ordered_ids = self._progression_ids(step.id)
self.state_store.invalidate_after(ordered_ids, step.id)
self.state_store.update_step(
step.id,
@@ -837,6 +970,8 @@ class CopienatorApp(tk.Tk):
self.use_proxy_var.get(),
)
self.pending_refaire_commands = self._refaire_commands()[1:] if step.section == REFAIRE_SECTION else []
self.refaire_command_index = 1
self._append_console(f"\n$ {command_display(command)}\n")
try:
self.runner.start(command, self.repository, environment, log_path)
@@ -846,6 +981,7 @@ class CopienatorApp(tk.Tk):
{"step": step.id, "command": command_display(command), "status": "failed", "error": str(exc)}
)
self.active_step_id = None
self.pending_refaire_commands = []
self._append_console(f"Impossible de lancer la commande : {exc}\n")
messagebox.showerror("Échec du lancement", str(exc))
self._populate_tree()
@@ -854,10 +990,10 @@ class CopienatorApp(tk.Tk):
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:
if not self.current_step or not self.state_store.evaluation or self.active_step_id:
return
self._save_current_form()
self.state_store.invalidate_after([item.id for item in self.steps], self.current_step.id)
self.state_store.invalidate_after(self._progression_ids(self.current_step.id), self.current_step.id)
self.state_store.update_step(self.current_step.id, status=status)
history: dict[str, object] = {
"step": self.current_step.id,
@@ -875,7 +1011,10 @@ class CopienatorApp(tk.Tk):
def _skip_step(self) -> None:
if self.current_step and self.current_step.optional:
step_id = self.current_step.id
self._mark_step("skipped")
if step_id.startswith("refaire_"):
self._move_selection_from(step_id, 1)
def _poll_runner(self) -> None:
while True:
@@ -899,6 +1038,26 @@ class CopienatorApp(tk.Tk):
if not step_id:
return
status = process_status(return_code, interrupted)
if status == "success" and self.pending_refaire_commands:
command = self.pending_refaire_commands.pop(0)
self.refaire_command_index += 1
self._append_console(f"\n$ {command_display(command)}\n")
self.state_store.update_step(step_id, command=command_display(command))
try:
self.runner.start(command, self.repository,
build_runner_environment(os.environ, self.api_key_var.get(), self.proxy_var.get(), self.use_proxy_var.get()),
self.state_store.workspace.log_path(f"{step_id}_{self.refaire_command_index}"))
self._update_controls()
return
except (OSError, RuntimeError) as exc:
self._append_console(f"Impossible de lancer la copie suivante : {exc}\n")
status, return_code = "failed", 1
self.pending_refaire_commands = []
if step_id == "refaire_merge" and status == "success":
for downstream in ("giving_names", "update_ods", "final_score", "personal_deploy", "personal_sent"):
entry = self.state_store.step(downstream)
if entry.get("status") in {"success", "detected", "skipped"}:
self.state_store.update_step(downstream, status="stale")
if step_id == "clean" and status == "success":
self._append_console(
f"\n[Terminé — code {return_code}{STATUS_LABELS[status]}]\n"
@@ -977,8 +1136,9 @@ class CopienatorApp(tk.Tk):
self.console.configure(state="disabled")
def _update_controls(self) -> None:
running = self.runner.running
running = bool(self.active_step_id) or self.runner.running
self.run_button.configure(state="disabled" if running or not self.current_step else "normal")
self.skip_button.configure(state="normal" if not running and self.current_step and self.current_step.optional else "disabled")
self.interrupt_button.configure(state="normal" if running else "disabled")
self.force_button.configure(state="normal" if running else "disabled")
self.send_button.configure(state="normal" if running else "disabled")
@@ -990,7 +1150,7 @@ class CopienatorApp(tk.Tk):
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]
ids = self._progression_ids(step_id)
try:
index = ids.index(step_id)
except ValueError:
+244
View File
@@ -0,0 +1,244 @@
"""Selection and command planning for the optional redo workflow."""
from __future__ import annotations
import re
import tkinter as tk
from pathlib import Path
from tkinter import ttk
from copienator import EvaluationWorkspace, read_json
from copienator.utils import natural_key
SECTION = "Refaire des copies (facultatif)"
ALL_LABELS = "Toute la copie"
ALL_COPIES = "Toutes les copies"
LAYOUTS = {
"Automatique": "auto",
"Par question (groupé)": "grouped",
"Par copie": "copies",
}
def resolve_layout(
selection: list[list], labels: list[str], choice: str = "auto"
) -> str:
if choice in {"grouped", "copies"}:
return choice
seen = set()
for _name, selected in selection:
current = set(selected or labels)
if seen & current:
return "grouped"
seen.update(current)
return "copies"
def copies_with_answer(copies: dict[str, Path], label: str) -> list[str]:
return [
name
for name, path in copies.items()
if any(
(path.with_suffix("") / f"{label}{suffix}.pdf").is_file()
for suffix in ("", "_new")
)
]
def available_copies(evaluation: Path) -> dict[str, Path]:
return {
path.stem: path
for path in sorted((evaluation / "Copies").glob("Copie*.pdf"), key=natural_key)
if re.fullmatch(r"Copie\d+", path.stem)
}
def validate_selection(
entries: object, copies: dict[str, Path], labels: list[str]
) -> list[list]:
if not isinstance(entries, list) or not entries:
raise ValueError("Ajoutez au moins une copie à refaire.")
result = {}
for entry in entries:
if not isinstance(entry, list) or len(entry) != 2:
raise ValueError("Sélection de copies invalide.")
name, selected = entry
if not isinstance(name, str) or name not in copies:
raise ValueError(f"Copie introuvable : {name}")
if not isinstance(selected, list) or any(
not isinstance(label, str) or label not in labels for label in selected
):
raise ValueError(f"Question inconnue pour {name}. Reprenez la sélection.")
if name in result:
raise ValueError(f"Copie sélectionnée plusieurs fois : {name}")
result[name] = sorted(set(selected), key=natural_key)
return [[name, result[name]] for name in sorted(result, key=natural_key)]
def load_selection(evaluation: Path) -> list[list]:
return validate_selection(
read_json(evaluation / "refaire.json"),
available_copies(evaluation),
EvaluationWorkspace(evaluation).read_labels(),
)
class RefaireSelection(ttk.Frame):
def __init__(
self,
parent,
evaluation: Path,
draft: dict,
directories: tuple[str, ...],
preferred: str,
):
super().__init__(parent)
self.columnconfigure(1, weight=1)
self.copies = available_copies(evaluation)
self.labels = (
EvaluationWorkspace(evaluation).read_labels()
if (evaluation / "labels").is_file()
else []
)
self.entries = {}
self.error = ""
try:
entries = draft.get("selection")
if entries is None:
entries = read_json(evaluation / "refaire.json", default=[])
if entries:
self.entries = dict(
validate_selection(entries, self.copies, self.labels)
)
except (OSError, ValueError, TypeError) as exc:
self.error = str(exc)
self.copy_var = tk.StringVar(value=next(iter(self.copies), ""))
self.label_var = tk.StringVar(value=ALL_LABELS)
source = draft.get("annotation_dir", preferred)
self.source_var = tk.StringVar(
value=source if source in directories else preferred
)
ttk.Label(self, text="Copie").grid(row=0, column=0, sticky="w", padx=(0, 8))
ttk.Combobox(
self,
textvariable=self.copy_var,
values=(ALL_COPIES, *self.copies),
state="readonly",
width=12,
).grid(row=0, column=1, sticky="ew")
ttk.Label(self, text="Question").grid(row=1, column=0, sticky="w", pady=5)
ttk.Combobox(
self,
textvariable=self.label_var,
values=(ALL_LABELS, *self.labels),
state="readonly",
width=12,
).grid(row=1, column=1, sticky="ew", pady=5)
ttk.Button(self, text="+ Ajouter", command=self.add).grid(
row=1, column=2, padx=(8, 0)
)
self.table = ttk.Treeview(
self, columns=("labels",), height=4, selectmode="browse"
)
self.table.heading("#0", text="Copie")
self.table.heading("labels", text="Questions à refaire")
self.table.column("#0", width=100, stretch=False)
self.table.column("labels", width=330)
self.table.grid(row=4, column=0, columnspan=3, sticky="nsew")
scrollbar = ttk.Scrollbar(self, orient="vertical", command=self.table.yview)
scrollbar.grid(row=4, column=3, sticky="ns")
self.table.configure(yscrollcommand=scrollbar.set)
ttk.Button(
self, text="Retirer la copie sélectionnée", command=self.remove
).grid(row=5, column=0, columnspan=3, sticky="w", pady=5)
ttk.Label(self, text="Passage principal").grid(
row=3, column=0, sticky="w", padx=(0, 8)
)
ttk.Combobox(
self,
textvariable=self.source_var,
values=directories,
state="readonly",
width=12,
).grid(row=3, column=1, sticky="ew")
layout = draft.get("layout", "auto")
self.layout_var = tk.StringVar(
value=next(
(name for name, code in LAYOUTS.items() if code == layout),
"Automatique",
)
)
ttk.Label(self, text="PDF à vérifier").grid(row=2, column=0, sticky="w")
ttk.Combobox(
self,
textvariable=self.layout_var,
values=tuple(LAYOUTS),
state="readonly",
width=12,
).grid(row=2, column=1, sticky="ew")
self.message_var = tk.StringVar(
value=self.error
or "Choisissez « Toutes les copies » pour refaire une question dans toute la classe."
)
help_label = ttk.Label(
self,
textvariable=self.message_var,
wraplength=500,
)
help_label.grid(row=6, column=0, columnspan=3, sticky="w", pady=5)
self.bind(
"<Configure>",
lambda event: help_label.configure(wraplength=max(200, event.width - 10)),
)
self.refresh()
def refresh(self):
self.table.delete(*self.table.get_children())
for name in sorted(self.entries, key=natural_key):
self.table.insert(
"",
"end",
iid=name,
text=name,
values=(", ".join(self.entries[name]) or ALL_LABELS,),
)
def add(self):
name, label = self.copy_var.get(), self.label_var.get()
if name not in (ALL_COPIES, *self.copies) or label not in (
ALL_LABELS,
*self.labels,
):
return
names = (
[name]
if name != ALL_COPIES
else list(self.copies)
if label == ALL_LABELS
else copies_with_answer(self.copies, label)
)
for copy_name in names:
# Adding a question must not narrow a copy already selected in full.
if label == ALL_LABELS:
self.entries[copy_name] = []
elif copy_name not in self.entries or self.entries[copy_name]:
self.entries[copy_name] = sorted(
set(self.entries.get(copy_name, [])) | {label}, key=natural_key
)
message = f"{len(names)} copie(s) ajoutée(s)."
if name == ALL_COPIES and len(names) < len(self.copies):
message += f" {len(self.copies) - len(names)} sans réponse découpée pour cette question."
self.message_var.set(message)
self.refresh()
def remove(self):
for name in self.table.selection():
self.entries.pop(name, None)
self.refresh()
def values(self):
return {
"selection": [[name, labels] for name, labels in self.entries.items()],
"annotation_dir": self.source_var.get(),
"layout": LAYOUTS[self.layout_var.get()],
}
+37
View File
@@ -505,9 +505,46 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
requires=("Copies", "correction.json", "A Rendre"),
),
]
steps[-1:-1] = build_refaire_workflow()
return [step for step in steps if show_personal_steps or not step.personal]
def build_refaire_workflow() -> list[StepDefinition]:
from .refaire import SECTION
selection = StepDefinition(
"refaire_selection", SECTION, "Choisir les copies et les questions",
"Sélectionnez les copies et les questions à refaire, puis enregistrez la sélection. "
"Le passage principal doit être terminé ; conservez ses annotations.",
(CommandVariant("default", "Sélection", None, "manual"),),
requires=("Copies", "labels", "correction.json"),
)
definitions = [
("review", "Reprendre le découpage", "Vérifiez et ajustez les labels des copies sélectionnées. Chaque copie souvre à son tour. Fermez la fenêtre pour passer à la suivante.", "review-labels", (), True),
("split", "Redécouper les réponses", "À exécuter après une modification du découpage. Traite toutes les copies sélectionnées ; vérifiez les fichiers _new et _old en cas de résolution manuelle.", "split-answers", (), True),
("correct", "Refaire la correction", "Relance la correction des seules questions sélectionnées. Peut être ignorée pour corriger manuellement les résultats.", "correct", ("--refaire",), True),
("annotate", "Préparer les copies à vérifier", "Génère les questions sélectionnées avec des cases dans BRnot, par question ou par copie selon la sélection. Remplace le précédent passage dans BRnot.", "annotate-checks", ("--refaire", "--overwrite"), False),
("export", "Exporter vers la tablette", "Exporte BRnot vers EXPORT_DIR. Retirez les anciens fichiers dexport avant le transfert.", "export", ("--refaire",), True),
("tablet", "Vérifier sur la tablette", "Annotez les PDF exportés, puis placez les retours dans IMPORT_DIR sans changer leur nom (nom de groupe ou Copie01.pdf…). Retournez aussi les PDF sans modification. Retirez les anciens fichiers dimport.", None, (), False),
("import", "Importer les copies vérifiées", "Importe les PDF retournés dans BRnot. Vous pouvez ignorer cette étape si les fichiers Concat_annotated.pdf y sont déjà en place.", "import", ("--refaire",), True),
("merge", "Mettre à jour les copies finales", "Fusionne les questions refaites avec le reste de chaque copie dans le dossier du passage principal. Relancez ensuite la préparation de A Rendre, le calcul des notes et la diffusion.", "read-grouped", ("--refaire",), False),
]
steps = [selection]
for suffix, title, description, program, flags, optional in definitions:
arguments = (arg_target(),) if program else ()
if suffix == "merge":
arguments += (ArgumentSpec("annotation_dir", "Passage principal", "choice", "--annotation-dir", default="BGnot", choices=("BGnot", "Bnot", "Anot")),)
requirements = ("refaire.json", "Copies", "labels", "correction.json")
if suffix in {"export", "tablet", "import", "merge"}:
requirements += ("BRnot",)
steps.append(StepDefinition(
f"refaire_{suffix}", SECTION, title, description,
(CommandVariant("default", title, program, "python" if program else "manual", flags),),
arguments=arguments, optional=optional, requires=requirements,
))
return steps
def value_for_default(value: object, evaluation_arg: str) -> object:
return evaluation_arg if value == EVALUATION else value