from __future__ import annotations import os import re from collections import Counter from dataclasses import dataclass from pathlib import Path from copienator import EvaluationWorkspace from copienator.platform import safe_filename RETURN_FOLDER_PATTERN = re.compile(r"^(?P.+) \((?P\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") elif 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 def return_folders(root: Path) -> dict[int, Path]: result = {} for folder in (root / "A Rendre").glob("*"): match = RETURN_FOLDER_PATTERN.fullmatch(folder.name) if not folder.is_dir() or folder.is_symlink() or not match: continue key = int(match["copy_id"]) if key in result: raise ValueError(f"Plusieurs dossiers A Rendre pour la copie {key}.") result[key] = folder return result def set_copy_name(root: Path, copy_id: str, name: str) -> None: from copienator import atomic_update_json name = name.strip() if not name or not safe_filename(name, fallback=""): raise ValueError("Saisissez un nom utilisable.") metadata = root / "Copies" / f"Copie{copy_id}.json" if not metadata.is_file(): raise ValueError("Métadonnées de copie absentes.") folder = return_folders(root).get(int(copy_id)) renamed = rename_return_copy(folder, name) if folder else None try: atomic_update_json(metadata, lambda data: data.update(name=name), default_factory=dict) except Exception: if renamed and renamed != folder: rename_return_copy(renamed, RETURN_FOLDER_PATTERN.fullmatch(folder.name)["name"]) raise