fix: allow blocked chat import drafts while keeping execution validation strict
This commit is contained in:
@@ -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):
|
||||||
|
|||||||
@@ -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":
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user