fix: expose per-file cover identities in import scans
This commit is contained in:
@@ -24,6 +24,8 @@ def test_scan_selected_student_folder_prefills_cover_identity(tmp_path, monkeypa
|
|||||||
assert project["status"] == "needs_confirmation"
|
assert project["status"] == "needs_confirmation"
|
||||||
assert len(project["files"]) == 5
|
assert len(project["files"]) == 5
|
||||||
assert project["missing"] == []
|
assert project["missing"] == []
|
||||||
|
assert project["folder_identity"] == {"student_no": "", "name": "", "topic": ""}
|
||||||
|
assert all(file["detected_identity"]["student_no"] == "9000000001" for file in project["files"])
|
||||||
assert scan("test", root=tmp_path)["projects"] == result["projects"]
|
assert scan("test", root=tmp_path)["projects"] == result["projects"]
|
||||||
|
|
||||||
|
|
||||||
@@ -39,6 +41,26 @@ def test_scan_conflicting_covers_need_confirmation(tmp_path, monkeypatch):
|
|||||||
assert project["student_no"] == ""
|
assert project["student_no"] == ""
|
||||||
assert project["warnings"]
|
assert project["warnings"]
|
||||||
assert project["status"] == "needs_confirmation"
|
assert project["status"] == "needs_confirmation"
|
||||||
|
assert "学号" in project["warnings"][0]
|
||||||
|
assert "student_no" not in project["warnings"][0]
|
||||||
|
assert {file["detected_identity"]["student_no"] for file in project["files"]} == {"9000000001", "9000000002"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_scan_unreadable_cover_has_no_stale_identity(tmp_path, monkeypatch):
|
||||||
|
from wisegrade import storage
|
||||||
|
folder = tmp_path / "9000000001_学生甲_机器人系统"
|
||||||
|
folder.mkdir()
|
||||||
|
(folder / "阶段报告1.pdf").write_bytes(b"fixture")
|
||||||
|
(folder / "阶段报告2.pdf").write_bytes(b"broken")
|
||||||
|
def read(path):
|
||||||
|
if path.name.endswith("2.pdf"):
|
||||||
|
raise ValueError("损坏PDF")
|
||||||
|
return "学号:9000000001\n姓名:学生甲\n题目:机器人系统"
|
||||||
|
monkeypatch.setattr(storage, "first_text", read)
|
||||||
|
project = scan(".", root=tmp_path)["projects"][0]
|
||||||
|
assert project["folder_identity"]["student_no"] == "9000000001"
|
||||||
|
assert project["files"][0]["detected_identity"]["student_no"] == "9000000001"
|
||||||
|
assert project["files"][1]["detected_identity"] == {}
|
||||||
|
|
||||||
|
|
||||||
def test_scan_parent_skips_empty_folders(tmp_path, monkeypatch):
|
def test_scan_parent_skips_empty_folders(tmp_path, monkeypatch):
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from .config import get_settings
|
|||||||
CATEGORIES = ("stage1", "stage2", "stage3", "final_report", "presentation")
|
CATEGORIES = ("stage1", "stage2", "stage3", "final_report", "presentation")
|
||||||
SUPPORTED = {".pdf", ".docx", ".pptx"}
|
SUPPORTED = {".pdf", ".docx", ".pptx"}
|
||||||
NAMES = {"stage1": "阶段报告1", "stage2": "阶段报告2", "stage3": "阶段报告3", "final_report": "最终报告", "presentation": "汇报PPT"}
|
NAMES = {"stage1": "阶段报告1", "stage2": "阶段报告2", "stage3": "阶段报告3", "final_report": "最终报告", "presentation": "汇报PPT"}
|
||||||
|
IDENTITY_LABELS = {"student_no": "学号", "name": "姓名", "topic": "选题"}
|
||||||
|
|
||||||
|
|
||||||
def secure_path(relative: str, *, root: Path | None = None, must_exist=True) -> Path:
|
def secure_path(relative: str, *, root: Path | None = None, must_exist=True) -> Path:
|
||||||
@@ -114,6 +115,7 @@ def scan(relative: str, *, root: Path | None = None):
|
|||||||
if item.is_dir() and not item.is_symlink() and has_materials(item)]
|
if item.is_dir() and not item.is_symlink() and has_materials(item)]
|
||||||
for folder in folders:
|
for folder in folders:
|
||||||
info = parse_student_folder(folder.name)
|
info = parse_student_folder(folder.name)
|
||||||
|
folder_identity = {key: info[key] for key in IDENTITY_LABELS}
|
||||||
files = []
|
files = []
|
||||||
extracted = {}
|
extracted = {}
|
||||||
identity_values = {}
|
identity_values = {}
|
||||||
@@ -124,6 +126,7 @@ def scan(relative: str, *, root: Path | None = None):
|
|||||||
if candidate.suffix.lower() not in SUPPORTED:
|
if candidate.suffix.lower() not in SUPPORTED:
|
||||||
continue
|
continue
|
||||||
error = ""
|
error = ""
|
||||||
|
identity = {}
|
||||||
try:
|
try:
|
||||||
text = first_text(candidate)
|
text = first_text(candidate)
|
||||||
category, status = classify(candidate, text)
|
category, status = classify(candidate, text)
|
||||||
@@ -137,6 +140,7 @@ def scan(relative: str, *, root: Path | None = None):
|
|||||||
files.append({"relative_path": candidate.relative_to(base).as_posix(), "category": category, "status": status,
|
files.append({"relative_path": candidate.relative_to(base).as_posix(), "category": category, "status": status,
|
||||||
"size": stat.st_size, "mtime": stat.st_mtime, "sha256": sha256(candidate),
|
"size": stat.st_size, "mtime": stat.st_mtime, "sha256": sha256(candidate),
|
||||||
"mime_type": mimetypes.guess_type(candidate.name)[0] or "application/octet-stream", "error": error,
|
"mime_type": mimetypes.guess_type(candidate.name)[0] or "application/octet-stream", "error": error,
|
||||||
|
"detected_identity": identity,
|
||||||
"first_text": text[:500] if not error else ""})
|
"first_text": text[:500] if not error else ""})
|
||||||
for category in CATEGORIES:
|
for category in CATEGORIES:
|
||||||
matches = [file for file in files if file["category"] == category]
|
matches = [file for file in files if file["category"] == category]
|
||||||
@@ -145,17 +149,18 @@ def scan(relative: str, *, root: Path | None = None):
|
|||||||
file["status"] = "duplicate"
|
file["status"] = "duplicate"
|
||||||
for key, values in identity_values.items():
|
for key, values in identity_values.items():
|
||||||
if len(values) > 1:
|
if len(values) > 1:
|
||||||
warnings.append(f"不同材料封面的{key}不一致,请核对后填写")
|
warnings.append(f"不同材料封面的{IDENTITY_LABELS[key]}不一致,请核对后填写")
|
||||||
else:
|
else:
|
||||||
extracted[key] = next(iter(values))
|
extracted[key] = next(iter(values))
|
||||||
for key, value in extracted.items():
|
for key, value in extracted.items():
|
||||||
if info.get(key) and value != info[key]:
|
if info.get(key) and value != info[key]:
|
||||||
warnings.append(f"封面{key}“{value}”与目录名“{info[key]}”不一致")
|
warnings.append(f"封面{IDENTITY_LABELS[key]}“{value}”与目录名“{info[key]}”不一致")
|
||||||
elif not info.get(key):
|
elif not info.get(key):
|
||||||
info[key] = value
|
info[key] = value
|
||||||
if warnings:
|
if warnings:
|
||||||
info["status"] = "needs_confirmation"
|
info["status"] = "needs_confirmation"
|
||||||
info.update({"relative_dir": folder.relative_to(base).as_posix(), "files": files,
|
info.update({"relative_dir": folder.relative_to(base).as_posix(), "files": files,
|
||||||
|
"folder_identity": folder_identity,
|
||||||
"detected_identity": extracted, "warnings": warnings,
|
"detected_identity": extracted, "warnings": warnings,
|
||||||
"missing": [category for category in CATEGORIES if not any(file["category"] == category for file in files)]})
|
"missing": [category for category in CATEGORIES if not any(file["category"] == category for file in files)]})
|
||||||
projects.append(info)
|
projects.append(info)
|
||||||
|
|||||||
Reference in New Issue
Block a user