146 lines
5.3 KiB
Python
146 lines
5.3 KiB
Python
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<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")
|
|
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
|