Files
Copies/copienator_gui/refaire.py
T

245 lines
8.5 KiB
Python

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