Copy removal

This commit is contained in:
sebastien committed 2026-11-02 11:35:21 +01:00
1 parent c36642e8de
commit 213576f83d
8 files changed
+464 -26

No files matched your search

+78 -4
View File
@@ -921,7 +921,7 @@ class CopienatorApp(tk.Tk):
if not step:
return
self._rendering = True
if step.id in {"personal_read", "edit_scores", "general_remarks"}:
if step.id in {"personal_read", "edit_scores", "general_remarks", "remove_copy"}:
self.command_box.grid_remove()
else:
self.command_box.grid()
@@ -997,7 +997,7 @@ class CopienatorApp(tk.Tk):
continue
value = values.get(spec.name, value_for_default(spec.default, evaluation_arg))
if step.id == 'statement_persp' and spec.name == 'delete_persp':
value = False # Deletion is a one-run choice, never a saved default.
value = spec.default # Reset on entry; retries in this form keep completed results.
if step.id == 'statement' and spec.name == 'restart':
value = False
if step.id == "final_score" and spec.name == "copy_merged_histogramme" and spec.name not in values:
@@ -1045,6 +1045,9 @@ class CopienatorApp(tk.Tk):
ttk.Button(self.form, text='Actualiser les barèmes présents',
command=self._refresh_persp_inventory).grid(row=row, column=1, sticky='w')
row += 1
ttk.Button(self.form, text='Ouvrir le dossier Persp',
command=self._open_persp).grid(row=row, column=1, sticky='w', pady=(8, 0))
row += 1
self.arg_vars['target'].trace_add('write', lambda *_args: self._refresh_persp_inventory())
self._refresh_persp_inventory()
@@ -1091,6 +1094,8 @@ class CopienatorApp(tk.Tk):
row += 1
self.run_button.configure(text="Make modifications" if step.id == "modify_annotations" else "Enregistrer la sélection" if step.id == "refaire_selection" else ("Marquer terminée" if step.is_manual else "Exécuter"))
if step.id == "remove_copy":
self.run_button.configure(text="Supprimer la copie sélectionnée")
if step.id == "personal_create":
self.run_button.configure(text="Marquer Réussie")
if step.id == "personal_read":
@@ -1118,6 +1123,31 @@ class CopienatorApp(tk.Tk):
self.persp_inventory_label.configure(text=text)
def _render_context_controls(self, step: StepDefinition, row: int) -> int:
if step.id == "remove_copy":
from copienator.copy_removal import available_copies
self.removal_copy_var = tk.StringVar()
ttk.Label(self.form, text="Copie à supprimer").grid(row=row, column=0, sticky="w")
choices = available_copies(self.evaluation) if self.evaluation else []
combo = ttk.Combobox(self.form, textvariable=self.removal_copy_var,
values=choices, state="readonly")
combo.grid(row=row, column=1, sticky="ew")
self.removal_preview = tk.Text(self.form, height=7, wrap="word", state="disabled")
self.removal_preview.grid(row=row + 1, column=0, columnspan=2, sticky="ew", pady=8)
def preview(*_args):
from copienator.copy_removal import removal_plan
try:
plan = removal_plan(self.evaluation, self.removal_copy_var.get())
text = "Retirer :\n" + "\n".join(str(p.relative_to(plan.root)) for p in plan.removed)
if plan.updated:
text += "\nMettre à jour :\n" + "\n".join(str(p.relative_to(plan.root)) for p in plan.updated)
except (OSError, ValueError, TypeError) as error:
text = str(error)
self.removal_preview.configure(state="normal")
self.removal_preview.delete("1.0", "end")
self.removal_preview.insert("1.0", text)
self.removal_preview.configure(state="disabled")
combo.bind("<<ComboboxSelected>>", preview)
return row + 2
if step.id == "statement_groups" or (
step.id == "annotation" and self.variant_var.get() == "grouped"
):
@@ -1252,7 +1282,7 @@ class CopienatorApp(tk.Tk):
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"}:
if step.id == "correction":
row = self._correction_folder_buttons(row)
copy_selection_steps = {
"page_splitter",
@@ -1435,6 +1465,11 @@ class CopienatorApp(tk.Tk):
def _open_persp(self) -> None:
evaluation = self.evaluation
if self.current_step and self.current_step.id == "statement_persp":
target = self.arg_vars.get("target")
value = str(target.get()).strip() if target else ""
if value:
evaluation = (self.repository / value).resolve()
self._open_desktop_path(evaluation / "Persp" if evaluation else None, "dossier Persp")
def _open_label_groups(self) -> None:
@@ -1738,6 +1773,9 @@ class CopienatorApp(tk.Tk):
if self.active_step_id or self.runner.running:
messagebox.showwarning("Traitement en cours", "Interrompez le traitement actuel avant d’en lancer un autre.")
return
if step.id == "remove_copy":
self._remove_selected_copy()
return
if step.id == "refaire_selection":
self._save_refaire_selection()
return
@@ -1893,7 +1931,8 @@ class CopienatorApp(tk.Tk):
return
self._save_current_form()
# Score edits already invalidate downstream steps when Set saves a change.
if self.current_step.id not in {"edit_scores", "general_remarks"}:
if self.current_step.id not in {"edit_scores", "general_remarks"} and not (
status == "skipped" and self.current_step.skip_on_forward):
self._invalidate_after_step(self.current_step, self._current_variant().id)
self.state_store.update_step(self.current_step.id, status=status)
history: dict[str, object] = {
@@ -1910,6 +1949,32 @@ class CopienatorApp(tk.Tk):
f"{self.current_step.title} : {STATUS_LABELS.get(status, status).lower()}{detail}."
)
def _remove_selected_copy(self) -> None:
from copienator.copy_removal import removal_plan, remove_copy
copy = self.removal_copy_var.get()
try:
plan = removal_plan(self.evaluation, copy)
except (OSError, ValueError, TypeError) as error:
messagebox.showerror("Suppression impossible", str(error))
return
if not messagebox.askyesno(
"Supprimer une copie",
f"Supprimer {copy} et ses artefacts ?\n\n"
f"{len(plan.removed)} fichiers ou dossiers retirés ; {len(plan.updated)} fichiers partagés mis à jour.\n"
"Les originaux seront sauvegardés dans BackUp avant toute modification.\n"
"Les autres copies ne seront pas renumérotées. Les lots de correction existants "
"devront être recréés ; les étapes suivantes seront à revalider.",
):
return
try:
backup = remove_copy(self.evaluation, copy)
except Exception as error:
messagebox.showerror("Suppression impossible", str(error))
return
self._mark_step("success", reason=f"{copy} supprimée ; sauvegarde : {backup}")
self._render_step()
self.info_var.set(f"{copy} supprimée — sauvegarde : {backup}")
def _validate_rename_step(self) -> None:
if (not self.current_step or self.current_step.id != "rename"
or not self.state_store.evaluation or self.active_step_id or self.runner.running):
@@ -2254,6 +2319,7 @@ class CopienatorApp(tk.Tk):
self._render_step()
if step_id == 'statement_persp':
self._refresh_persp_inventory()
return
if step_id == "statement" and self._latex_warning_paths:
self.after_idle(self._show_current_latex_warnings)
if step_id == "personal_read":
@@ -2394,6 +2460,14 @@ class CopienatorApp(tk.Tk):
target = max(0, min(len(ids) - 1, index + delta))
if target == index:
return
while delta > 0 and target < len(ids) - 1 and self.step_by_id[ids[target]].skip_on_forward:
skipped = ids[target]
entry = self.state_store.step(skipped)
if not entry.get("status"):
self.state_store.update_step(skipped, status="skipped", visited=True)
if self.tree.exists(skipped):
self.tree.item(skipped, values=(STATUS_LABELS["skipped"],))
target += 1
self.tree.selection_set(ids[target])
self.tree.see(ids[target])
+17 -13
View File
@@ -53,6 +53,7 @@ class StepDefinition:
personal: bool = False
requires: tuple[str, ...] = ()
artifacts: tuple[str, ...] = ()
skip_on_forward: bool = False
auto_start_first_visit: bool = False
skip_for_live_correction: bool = False
skip_without_manual_conflicts: bool = False
@@ -194,27 +195,19 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
optional=True, personal=True, requires=("labels", "Text2", "Sol2"),
),
StepDefinition(
"statement_persp", "Prétraitement de l’énoncé", "Générer les barèmes manquants avec Gemini",
"Génère uniquement les barèmes manquants dans Persp et conserve ceux déjà présents. "
"statement_persp", "Prétraitement de l’énoncé", "Générer les barèmes avec Gemini",
"Par défaut, supprime les barèmes actuels dans Persp et tous les régénère. "
"Décochez Delete current Persp pour générer uniquement les barèmes manquants et conserver ceux déjà présents. "
"Les fichiers vides ou contenant seulement le label sont considérés comme manquants. "
"Après un échec, relancez cette étape pour compléter les barèmes. "
"Cochez Delete current Persp pour supprimer les barèmes actuels au lancement et tous les régénérer.",
"Après la génération, ouvrez Persp pour vérifier les résultats, puis passez à l’étape suivante.",
(python("default", "Barèmes Gemini", "statement", fixed_args=("--persp-only",)),),
arguments=(arg_target("le dossier de l’évaluation"),
ArgumentSpec('delete_persp', 'Delete current Persp', kind='bool',
flag='--delete-persp', default=False,
flag='--delete-persp', default=True,
help='Supprime tous les fichiers actuels de Persp au lancement, y compris les barèmes modifiés manuellement. Puis régénère tous les barèmes.')),
optional=True, personal=True, requires=("labels", "label_groups", "Text2", "Sol2"),
),
StepDefinition(
"review_persp",
"Prétraitement de l’énoncé",
"Relire les barèmes",
"Étape manuelle facultative : vérifier et modifier les instructions de correction.",
(manual("review"),),
optional=True,
requires=("Persp",),
),
StepDefinition(
"rotate",
"Prétraitement des copies",
@@ -252,6 +245,17 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
),
arguments=(arg_target("le dossier de l’évaluation"),),
),
StepDefinition(
"remove_copy",
"Prétraitement des copies",
"Supprimer une copie",
"Étape facultative, ignorée au premier passage. Sélectionnez une copie pour retirer ses fichiers "
"et artefacts. Chaque fichier supprimé ou modifié est sauvegardé dans BackUp, dans l’évaluation. "
"Les autres copies gardent leur numéro. Les lots de correction partagés devront être recréés.",
(CommandVariant("default", "Suppression avec sauvegarde", None, "manual"),),
optional=True,
skip_on_forward=True,
),
StepDefinition(
"page_splitter",
"Prétraitement des copies",