Manual score modification step
This commit is contained in:
1 parent
261a50a7d2
commit
7bc588e9a1
5 files changed
+383
-2
No files matched your search
@@ -0,0 +1,135 @@
|
||||
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 .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]
|
||||
|
||||
|
||||
def read_scores(path: Path) -> dict:
|
||||
scores = read_json(path)
|
||||
if not isinstance(scores, dict):
|
||||
raise ValueError(f"score.json doit contenir un objet JSON : {path}")
|
||||
return scores
|
||||
|
||||
|
||||
def set_score(path: Path, label: str, value: str) -> str:
|
||||
try:
|
||||
number = Decimal(value.strip().replace(",", "."))
|
||||
except InvalidOperation:
|
||||
raise ValueError("Le score doit être un nombre.") from None
|
||||
if not number.is_finite() or number < 0:
|
||||
raise ValueError("Le score doit être un nombre positif ou nul.")
|
||||
previous = ""
|
||||
|
||||
def update(scores):
|
||||
nonlocal previous
|
||||
if not isinstance(scores, dict) or label not in scores:
|
||||
raise ValueError(f"Label absent de score.json : {label}")
|
||||
previous = str(scores[label])
|
||||
scores[label] = str(number)
|
||||
|
||||
# A Rendre/score.json is often a symlink to the annotation score file.
|
||||
# Resolve it before atomic replacement so the link continues to work.
|
||||
atomic_update_json(path.resolve(strict=True), update, default_factory=dict)
|
||||
return previous
|
||||
|
||||
|
||||
class ScoreEditorPanel(ttk.Frame):
|
||||
def __init__(self, parent, evaluation: Path, on_change):
|
||||
super().__init__(parent)
|
||||
self.evaluation = evaluation
|
||||
self.on_change = on_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="Set", command=self.save_score)
|
||||
self.set_button.grid(row=2, column=2, padx=(8, 0), pady=4)
|
||||
ttk.Label(self, textvariable=self.status_var, wraplength=650).grid(
|
||||
row=3, 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("<Return>", lambda _event: self.save_score())
|
||||
self.load_score()
|
||||
|
||||
def load_score(self, *_args):
|
||||
self.current_path = None
|
||||
self.score_var.set("")
|
||||
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:
|
||||
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.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)
|
||||
|
||||
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}")
|
||||
except (OSError, ValueError) as exc:
|
||||
messagebox.showerror("Modification du score", str(exc))
|
||||
Reference in new issue
Block a user