Initial commit for finalisation web interface
This commit is contained in:
1 parent
b295f871a7
commit
c36642e8de
33 files changed
+2745
-138
No files matched your search
@@ -0,0 +1,145 @@
|
||||
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
|
||||
Reference in new issue
Block a user