"""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( "", 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()], }