from __future__ import annotations import re import tkinter as tk from decimal import Decimal, InvalidOperation from pathlib import Path from tkinter import messagebox, ttk from copienator import EvaluationWorkspace, atomic_update_json, read_json from copienator.utils import natural_key from copienator.custom_remarks import remarks_for_copy, set_remark from .giving_names import RETURN_FOLDER_PATTERN def score_path_for_id(evaluation: Path, copy_id: str) -> Path: identifier = copy_id.strip() if not re.fullmatch(r"[0-9]+", identifier): raise ValueError("Saisissez un numéro d’id.") return_dir = EvaluationWorkspace(evaluation).return_dir matches = [] if return_dir.is_dir(): for folder in return_dir.iterdir(): match = RETURN_FOLDER_PATTERN.fullmatch(folder.name) if folder.is_dir() and not folder.is_symlink() and match and int(match["copy_id"]) == int(identifier): matches.append(folder / "score.json") if not matches: raise FileNotFoundError(f"Aucun dossier dans A Rendre pour l’id {identifier}.") if len(matches) > 1: raise ValueError(f"Plusieurs dossiers dans A Rendre portent l’id {identifier}.") if not matches[0].is_file(): raise FileNotFoundError(f"score.json introuvable : {matches[0]}") return matches[0] from copienator.score_editing import read_scores, set_score class ScoreEditorPanel(ttk.Frame): def __init__(self, parent, evaluation: Path, on_change, on_remark_change=None): super().__init__(parent) self.evaluation = evaluation self.on_change = on_change self.on_remark_change = on_remark_change self.current_path = None self.busy = False self.id_var = tk.StringVar() self.label_var = tk.StringVar() self.score_var = tk.StringVar() self.status_var = tk.StringVar() self.columnconfigure(1, weight=1) ttk.Label(self, text="Id de copie").grid(row=0, column=0, sticky="w", padx=(0, 8), pady=4) self.id_entry = ttk.Entry(self, textvariable=self.id_var) self.id_entry.grid(row=0, column=1, sticky="ew", pady=4) ttk.Label(self, text="Label").grid(row=1, column=0, sticky="w", padx=(0, 8), pady=4) self.label_entry = ttk.Combobox(self, textvariable=self.label_var) self.label_entry.grid(row=1, column=1, sticky="ew", pady=4) ttk.Label(self, text="Score").grid(row=2, column=0, sticky="w", padx=(0, 8), pady=4) self.score_entry = ttk.Entry(self, textvariable=self.score_var) self.score_entry.grid(row=2, column=1, sticky="ew", pady=4) self.set_button = ttk.Button(self, text="Enregistrer le score", command=self.save_score) self.set_button.grid(row=2, column=2, padx=(8, 0), pady=4) ttk.Label(self, text="Remarque personnelle").grid(row=3, column=0, sticky="nw", pady=4) self.remark_entry = tk.Text(self, height=4, width=45, wrap="word", undo=True) self.remark_entry.grid(row=3, column=1, sticky="ew", pady=4) self.remark_button = ttk.Button(self, text="Enregistrer la remarque", command=self.save_remark) self.remark_button.grid(row=3, column=2, padx=(8, 0), pady=4) ttk.Label(self, text="Videz le champ puis enregistrez pour supprimer votre remarque.").grid( row=4, column=1, columnspan=2, sticky="w") ttk.Label(self, textvariable=self.status_var, wraplength=650).grid( row=5, column=0, columnspan=3, sticky="w", pady=8 ) self.id_var.trace_add("write", self.load_score) self.label_var.trace_add("write", self.load_score) self.score_entry.bind("", lambda _event: self.save_score()) self.load_score() def load_score(self, *_args): self.current_path = None self.score_var.set("") self.remark_entry.configure(state="normal") self.remark_entry.delete("1.0", "end") self.label_entry.configure(values=()) try: path = score_path_for_id(self.evaluation, self.id_var.get()) scores = read_scores(path) self.label_entry.configure(values=sorted(scores, key=natural_key)) label = self.label_var.get().strip() if not label: self.status_var.set("Choisissez ou saisissez un label.") elif label not in scores: self.status_var.set(f"Label absent de score.json : {label}") else: remark = remarks_for_copy(self.evaluation, self.id_var.get()).get(label, "") self.remark_entry.insert("1.0", remark) self.current_path = path self.score_var.set(str(scores[label])) self.status_var.set(path.parent.name) except (OSError, ValueError) as exc: self.status_var.set(str(exc)) self.remark_entry.edit_reset() self.set_busy(self.busy) def set_busy(self, busy: bool): self.busy = busy self.id_entry.configure(state="disabled" if busy else "normal") self.label_entry.configure(state="disabled" if busy else "normal") state = "normal" if self.current_path is not None and not busy else "disabled" self.score_entry.configure(state=state) self.set_button.configure(state=state) self.remark_entry.configure(state=state) self.remark_button.configure(state=state) def save_score(self): if self.current_path is None or self.busy: return label = self.label_var.get().strip() try: previous = set_score(self.current_path, label, self.score_var.get()) current = str(read_scores(self.current_path)[label]) self.score_var.set(current) if previous != current: self.on_change(self.id_var.get().strip(), label, previous, current) self.status_var.set(f"Score enregistré : {self.current_path.parent.name} — {label} : {current}. Régénération nécessaire.") except (OSError, ValueError) as exc: messagebox.showerror("Modification du score", str(exc)) def save_remark(self): if self.current_path is None or self.busy: return label = self.label_var.get().strip() try: if label not in read_scores(self.current_path): raise ValueError(f"Label absent de score.json : {label}") current = self.remark_entry.get("1.0", "end-1c").strip() previous = set_remark(self.evaluation, self.id_var.get(), label, current) if previous != current and self.on_remark_change: self.on_remark_change(self.id_var.get().strip(), label, previous, current) self.status_var.set("Remarque enregistrée — régénération nécessaire dans « Lire les annotations manuscrites ».") except (OSError, ValueError) as exc: messagebox.showerror("Modification de la remarque", str(exc))