fix: validate import identities and selected materials consistently
This commit is contained in:
@@ -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
|
||||||
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user