fix: validate import identities and selected materials consistently

This commit is contained in:
2026-10-14 14:57:41 +08:00
parent 1776b1b50c
commit 46c45baaa2
2 changed files with 57 additions and 4 deletions
+36
View File
@@ -0,0 +1,36 @@
import pytest
from pydantic import ValidationError
from wisegrade.api import ImportConfirm, ProjectConfirm
def project(**changes):
return {"relative_dir": "test/student", "student_no": "9000000001", "name": "学生甲", "topic": "机器人项目",
"files": [{"relative_path": "test/student/最终报告.pdf", "category": "final_report", "sha256": "a" * 64}], **changes}
def test_confirm_normalizes_identity_before_validating():
result = ProjectConfirm.model_validate(project(student_no=" 9000000001 ", name=" 学生甲 ", topic=" 机器人项目 "))
assert (result.student_no, result.name, result.topic) == ("9000000001", "学生甲", "机器人项目")
@pytest.mark.parametrize("changes", [
{"name": " "}, {"topic": " "}, {"student_no": "9000000001"}, {"files": []},
{"files": [{"relative_path": "test/student/a.pdf", "category": "unknown", "sha256": "a" * 64}]},
{"files": [{"relative_path": "test/student/a.pdf", "category": "final_report", "sha256": "a" * 64, "status": "ignored"}]},
{"files": [{"relative_path": f"test/student/{i}.pdf", "category": "final_report", "sha256": "a" * 64} for i in range(2)]},
])
def test_confirm_rejects_invalid_identity_or_selection(changes):
with pytest.raises(ValidationError):
ProjectConfirm.model_validate(project(**changes))
def test_empty_import_is_rejected():
with pytest.raises(ValidationError):
ImportConfirm(course_version_id=1, projects=[])
def test_ignored_unknown_material_does_not_block_valid_selection():
data = project()
data["files"].append({"relative_path": "test/student/foreign.pdf", "category": "unknown", "sha256": "b" * 64, "status": "ignored"})
assert len(ProjectConfirm.model_validate(data).files) == 2
+21 -4
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 from pydantic import BaseModel, Field, field_validator, model_validator
from sqlalchemy import func, select from sqlalchemy import func, select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@@ -67,16 +67,33 @@ class FileConfirm(BaseModel):
class ProjectConfirm(BaseModel): class ProjectConfirm(BaseModel):
relative_dir: str relative_dir: str
student_no: str = Field(pattern=r"^\d{5,20}$") student_no: str = Field(pattern=r"^[0-9]{5,20}$")
name: str = Field(min_length=1) name: str = Field(min_length=1)
topic: str = Field(min_length=1) topic: str = Field(min_length=1)
direction: str = "" direction: str = ""
files: list[FileConfirm] files: list[FileConfirm] = Field(min_length=1)
@field_validator("student_no", "name", "topic", "direction", mode="before")
@classmethod
def trim_identity(cls, value):
return value.strip() if isinstance(value, str) else value
@model_validator(mode="after")
def validate_selection(self):
selected = [file for file in self.files if file.status != "ignored"]
if not selected:
raise ValueError("至少选择1份资料")
categories = [file.category for file in selected]
if any(category not in CATEGORIES for category in categories):
raise ValueError("请选择有效材料类型")
if len(set(categories)) != len(categories):
raise ValueError("每类材料只允许选择1份,其余请设为忽略")
return self
class ImportConfirm(BaseModel): class ImportConfirm(BaseModel):
course_version_id: int course_version_id: int
projects: list[ProjectConfirm] projects: list[ProjectConfirm] = Field(min_length=1)
class BatchInput(BaseModel): class BatchInput(BaseModel):