fix: allow blocked chat import drafts while keeping execution validation strict

This commit is contained in:
2026-10-17 20:50:25 +08:00
parent 4acc7563b0
commit a5cb40f83d
3 changed files with 21 additions and 4 deletions
+6
View File
@@ -214,6 +214,12 @@ def test_duplicate_scan_requires_teacher_file_choice(environment, db):
generated_action = db.get(PendingAction, generated["pending_action"]["id"]) generated_action = db.get(PendingAction, generated["pending_action"]["id"])
assert generated_action.preview["requires_edit"] is True assert generated_action.preview["requires_edit"] is True
assert all(file["status"] == "ignored" for file in generated_action.payload["projects"][0]["files"]) assert all(file["status"] == "ignored" for file in generated_action.payload["projects"][0]["files"])
with pytest.raises(HTTPException) as blocked:
confirm(db, conversation, generated_action.id)
assert blocked.value.status_code == 409
with pytest.raises(HTTPException) as invalid_selection:
prepare(db, conversation, "import", generated_action.payload, teacher_edited=True)
assert invalid_selection.value.status_code == 400
proposed = prepare(db, conversation, "import", payload) proposed = prepare(db, conversation, "import", payload)
assert proposed.preview["requires_edit"] is True assert proposed.preview["requires_edit"] is True
with pytest.raises(HTTPException): with pytest.raises(HTTPException):
+9 -2
View File
@@ -6,6 +6,7 @@ from pathlib import Path
from uuid import uuid4 from uuid import uuid4
from fastapi import HTTPException from fastapi import HTTPException
from pydantic import ValidationError
from sqlalchemy import select from sqlalchemy import select
from . import services from . import services
@@ -149,7 +150,12 @@ def prepare(db, conversation: Conversation, kind: str, payload: dict, *, teacher
raise HTTPException(400, "整批 Excel 导出须在未限定学生项目的会话中准备") raise HTTPException(400, "整批 Excel 导出须在未限定学生项目的会话中准备")
if kind == "import": if kind == "import":
from .api import ImportConfirm from .api import ImportConfirm
data = ImportConfirm.model_validate(payload) try:
data = ImportConfirm.model_validate(payload, context={
"allow_unselected_import_draft": not teacher_edited})
except ValidationError as exc:
messages = [error["msg"] for error in exc.errors(include_input=False, include_context=False)]
raise HTTPException(400, "导入资料尚未满足确认条件:" + ";".join(messages)) from exc
payload = data.model_dump() payload = data.model_dump()
require_scope(db, conversation, version_id=data.course_version_id) require_scope(db, conversation, version_id=data.course_version_id)
if teacher_edited and any(item.student_no == "00000" or item.name == "待确认" or item.topic == "待确认" for item in data.projects): if teacher_edited and any(item.student_no == "00000" or item.name == "待确认" or item.topic == "待确认" for item in data.projects):
@@ -164,9 +170,10 @@ def prepare(db, conversation: Conversation, kind: str, payload: dict, *, teacher
scanned.append(match) scanned.append(match)
if match["status"] != "recognized" or match["warnings"] or any(f["status"] != "recognized" for f in match["files"]): if match["status"] != "recognized" or match["warnings"] or any(f["status"] != "recognized" for f in match["files"]):
ambiguous.append(item.relative_dir) ambiguous.append(item.relative_dir)
unselected = any(not any(file.status != "ignored" for file in item.files) for item in data.projects)
preview = {"course_version_id": data.course_version_id, "projects": scanned, preview = {"course_version_id": data.course_version_id, "projects": scanned,
"selection": payload["projects"], "ambiguous": ambiguous, "selection": payload["projects"], "ambiguous": ambiguous,
"requires_edit": bool(ambiguous) and not teacher_edited} "requires_edit": (bool(ambiguous) or unselected) and not teacher_edited}
elif kind == "course_ingest": elif kind == "course_ingest":
require_scope(db, conversation, version_id=payload["course_version_id"]) require_scope(db, conversation, version_id=payload["course_version_id"])
preview = {"course_version_id": payload["course_version_id"], "templates": preview = {"course_version_id": payload["course_version_id"], "templates":
+6 -2
View File
@@ -3,7 +3,7 @@ from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from pydantic import BaseModel, Field, field_validator, model_validator from pydantic import BaseModel, Field, ValidationInfo, field_validator, model_validator
from sqlalchemy import func, select from sqlalchemy import func, select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@@ -79,9 +79,13 @@ class ProjectConfirm(BaseModel):
return value.strip() if isinstance(value, str) else value return value.strip() if isinstance(value, str) else value
@model_validator(mode="after") @model_validator(mode="after")
def validate_selection(self): def validate_selection(self, info: ValidationInfo):
selected = [file for file in self.files if file.status != "ignored"] selected = [file for file in self.files if file.status != "ignored"]
if not selected: if not selected:
# Only the chat preview builder supplies this context. HTTP imports,
# edited previews and final execution always use strict validation.
if (info.context or {}).get("allow_unselected_import_draft"):
return self
raise ValueError("至少选择1份资料") raise ValueError("至少选择1份资料")
categories = [file.category for file in selected] categories = [file.category for file in selected]
if any(category not in CATEGORIES for category in categories): if any(category not in CATEGORIES for category in categories):