Files
Copies/copienator_gui/giving_names.py
T
2026-09-26 23:01:47 +02:00

229 lines
8.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import os
import re
import tkinter as tk
from collections import Counter
from dataclasses import dataclass
from pathlib import Path
from tkinter import messagebox, ttk
from typing import Callable
from copienator import EvaluationWorkspace
from copienator.platform import open_path, safe_filename
RETURN_FOLDER_PATTERN = re.compile(r"^(?P<name>.+) \((?P<copy_id>\d+)\)$")
@dataclass(frozen=True, slots=True)
class NameIssue:
folder: Path
name: str
copy_id: str
reasons: tuple[str, ...]
def find_name_issues(evaluation: Path) -> list[NameIssue]:
"""Return unknown and duplicated names currently present in A Rendre."""
return_dir = EvaluationWorkspace(evaluation).return_dir
parsed: list[tuple[Path, str, str]] = []
if return_dir.is_dir():
for folder in return_dir.iterdir():
if not folder.is_dir() or folder.is_symlink():
continue
match = RETURN_FOLDER_PATTERN.fullmatch(folder.name)
if match:
parsed.append(
(folder, match.group("name"), match.group("copy_id"))
)
counts = Counter(name.casefold() for _folder, name, _copy_id in parsed)
issues = []
for folder, name, copy_id in parsed:
reasons = []
if name.casefold() == "unknown":
reasons.append("nom inconnu")
if counts[name.casefold()] > 1:
reasons.append(f"nom attribué à {counts[name.casefold()]} copies")
if reasons:
issues.append(NameIssue(folder, name, copy_id, tuple(reasons)))
return sorted(issues, key=lambda issue: (int(issue.copy_id), issue.name.casefold()))
def original_copy_path(evaluation: Path, copy_id: str) -> Path | None:
workspace = EvaluationWorkspace(evaluation)
filename = f"Copie{copy_id}.pdf"
for candidate in (
workspace.original_copies_dir / filename,
workspace.copies_dir / filename,
workspace.root / filename,
):
if candidate.is_file():
return candidate
return None
def rename_return_copy(folder: Path, requested_name: str) -> Path:
"""Rename one return folder and its top-level JPG/PDF, preserving its ID."""
match = RETURN_FOLDER_PATTERN.fullmatch(folder.name)
if not folder.is_dir() or folder.is_symlink() or match is None:
raise ValueError(f"Dossier de copie invalide : {folder}")
if folder.parent.name != "A Rendre":
raise ValueError("Le dossier à renommer doit se trouver dans A Rendre.")
entered_name = requested_name.strip()
if not entered_name:
raise ValueError("Saisissez un nom.")
name = safe_filename(entered_name, fallback="")
if not name:
raise ValueError("Le nom ne contient aucun caractère utilisable.")
copy_id = match.group("copy_id")
destination = folder.with_name(f"{name} ({copy_id})")
if destination != folder and os.path.lexists(destination):
raise FileExistsError(f"Le dossier {destination.name!r} existe déjà.")
assets: dict[str, Path] = {}
for child in folder.iterdir():
suffix = child.suffix.casefold()
if suffix not in {".jpg", ".pdf"}:
continue
if suffix in assets:
raise ValueError(
f"Plusieurs fichiers {suffix} sont présents dans {folder.name}."
)
assets[suffix] = child
renames: list[tuple[Path, Path]] = []
for suffix, source in assets.items():
target = source.with_name(f"{name}{suffix}")
if target != source and os.path.lexists(target):
raise FileExistsError(f"Le fichier {target.name!r} existe déjà.")
if target != source:
renames.append((source, target))
completed: list[tuple[Path, Path]] = []
try:
for source, target in renames:
source.rename(target)
completed.append((source, target))
if destination != folder:
folder.rename(destination)
except OSError:
for source, target in reversed(completed):
if os.path.lexists(target) and not os.path.lexists(source):
target.rename(source)
raise
return destination
class GivingNamesPanel(ttk.LabelFrame):
def __init__(
self,
master: tk.Misc,
evaluation: Path,
*,
opener: Callable[[Path], None] = open_path,
) -> None:
super().__init__(master, text="Vérification des noms", padding=7)
self.evaluation = evaluation
self.opener = opener
self.columnconfigure(0, weight=1)
actions = ttk.Frame(self)
actions.grid(row=0, column=0, sticky="w", pady=(0, 7))
ttk.Button(
actions,
text="Ouvrir A Rendre",
command=self.open_return_folder,
).pack(side="left")
ttk.Button(actions, text="Recharger", command=self.reload).pack(
side="left", padx=6
)
self.issue_frame = ttk.Frame(self)
self.issue_frame.grid(row=1, column=0, sticky="ew")
self.issue_frame.columnconfigure(1, weight=1)
self.reload()
def open_return_folder(self) -> None:
path = EvaluationWorkspace(self.evaluation).return_dir
if not path.is_dir():
messagebox.showerror(
"Dossier introuvable",
"Exécutez d’abord l’attribution des noms pour créer A Rendre.",
)
return
try:
self.opener(path)
except (OSError, RuntimeError) as exc:
messagebox.showerror("Ouverture impossible", str(exc))
def open_original(self, copy_id: str) -> None:
path = original_copy_path(self.evaluation, copy_id)
if path is None:
messagebox.showerror(
"Copie introuvable", f"Copie{copy_id}.pdf est introuvable."
)
return
try:
self.opener(path)
except (OSError, RuntimeError) as exc:
messagebox.showerror("Ouverture impossible", str(exc))
def rename_issue(self, issue: NameIssue, variable: tk.StringVar) -> None:
try:
destination = rename_return_copy(issue.folder, variable.get())
except (OSError, ValueError) as exc:
messagebox.showerror("Renommage impossible", str(exc))
return
self.reload()
messagebox.showinfo("Copie renommée", f"Nouveau dossier : {destination.name}")
def reload(self) -> None:
for child in self.issue_frame.winfo_children():
child.destroy()
issues = find_name_issues(self.evaluation)
if not issues:
return_dir = EvaluationWorkspace(self.evaluation).return_dir
text = (
"Aucun nom inconnu ou dupliqué."
if return_dir.is_dir()
else "A Rendre n’a pas encore été généré."
)
ttk.Label(self.issue_frame, text=text).grid(
row=0, column=0, columnspan=4, sticky="w"
)
return
ttk.Label(self.issue_frame, text="Copie / problème").grid(
row=0, column=0, sticky="w", padx=(0, 6)
)
ttk.Label(self.issue_frame, text="Nouveau nom").grid(
row=0, column=1, sticky="w"
)
for row, issue in enumerate(issues, start=1):
ttk.Label(
self.issue_frame,
text=f"Copie{issue.copy_id} — {', '.join(issue.reasons)}",
).grid(row=row, column=0, sticky="w", padx=(0, 6), pady=2)
variable = tk.StringVar()
ttk.Entry(self.issue_frame, textvariable=variable).grid(
row=row, column=1, sticky="ew", pady=2
)
original = original_copy_path(self.evaluation, issue.copy_id)
ttk.Button(
self.issue_frame,
text="Ouvrir l’originale",
command=lambda ident=issue.copy_id: self.open_original(ident),
state="normal" if original else "disabled",
).grid(row=row, column=2, padx=6, pady=2)
ttk.Button(
self.issue_frame,
text="Renommer",
command=lambda item=issue, value=variable: self.rename_issue(
item, value
),
).grid(row=row, column=3, pady=2)