完善项目导入、模板与分割工作区交互
- 增强 DICOM/视频项目导入与演示数据:DICOM 按文件名自然顺序处理,导入后展示上传与解析任务进度,恢复演示出厂设置保留演示视频和演示 DICOM 项目,并补充 demo media seed 逻辑。 - 完善项目管理:项目支持重命名、删除、复制,删除使用站内确认弹窗,复制支持新项目重置和全内容复制,DICOM 项目不显示生成帧入口。 - 完善 GT Mask 与导出链路:只支持 8-bit maskid 图导入,非法/全背景图明确拒绝,尺寸自动适配,高精度 polygon 回显;统一导出默认当前帧,GT_label 使用 uint8 和真实 maskid,待分类 maskid 0 与背景一致。 - 完善分割工作区交互:新增画笔和橡皮擦并支持尺寸控制,移除创建点/线段入口,工具栏按类别分隔,AI 智能分割使用明确 AI 图标,取消黄色 seed point,清空/删除传播 mask 后同步清理空帧时间轴状态。 - 完善传播与时间轴:自动传播使用 SAM 2.1 权重任务,参考帧无遮罩时提示,传播历史按同一蓝色系递进变暗,删除/清空传播链时保留人工或独立 AI 标注来源。 - 完善模板库:新增头颈部 CT 分割默认模板,所有模板保留 maskid 0 待分类,支持鼠标复制模板、拖拽层级、JSON 批量导入预览、删除 label 和站内删除确认。 - 完善用户与高风险确认:用户改密码、删除用户、恢复演示出厂设置和清空人工/AI 标注帧均改为站内确认交互,避免浏览器原生 prompt/confirm。 - 补充前后端测试与文档:更新项目、模板、GT 导入、导出、传播、DICOM、用户管理等测试,并同步 README、AGENTS 和 doc 下实现/契约/测试计划文档。
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
from models import Annotation, AuditLog, Frame, Mask, ProcessingTask, Project, Template, User
|
||||
from routers.auth import create_access_token, hash_password
|
||||
from statuses import PROJECT_STATUS_PENDING
|
||||
from statuses import PROJECT_STATUS_READY
|
||||
|
||||
|
||||
def test_admin_user_management_and_audit_logs(client, db_session):
|
||||
@@ -83,17 +83,34 @@ def test_admin_cannot_delete_self_or_user_with_projects(client, db_session):
|
||||
assert response.status_code == 409
|
||||
|
||||
|
||||
def test_demo_factory_reset_leaves_admin_and_unparsed_demo_video(client, db_session, monkeypatch, tmp_path):
|
||||
def test_demo_factory_reset_leaves_admin_and_parsed_demo_dicom(client, db_session, monkeypatch, tmp_path):
|
||||
video_path = tmp_path / "Data_MyVideo_1.mp4"
|
||||
video_path.write_bytes(b"demo-video")
|
||||
monkeypatch.setattr("routers.admin.settings.demo_video_path", str(video_path))
|
||||
dicom_dir = tmp_path / "dicom"
|
||||
dicom_dir.mkdir()
|
||||
for name in ["10.dcm", "2.dcm", "1.dcm"]:
|
||||
(dicom_dir / name).write_bytes(name.encode())
|
||||
monkeypatch.setattr("routers.admin.settings.demo_dicom_dir", str(dicom_dir))
|
||||
|
||||
parsed_frame_paths = []
|
||||
for idx in range(3):
|
||||
frame_path = tmp_path / f"frame_{idx:06d}.jpg"
|
||||
frame_path.write_bytes(b"frame")
|
||||
parsed_frame_paths.append(str(frame_path))
|
||||
|
||||
uploaded = []
|
||||
monkeypatch.setattr("routers.admin.upload_file", lambda object_name, data, content_type, length: uploaded.append({
|
||||
monkeypatch.setattr("services.demo_media.upload_file", lambda object_name, data, content_type, length: uploaded.append({
|
||||
"object_name": object_name,
|
||||
"data": data,
|
||||
"content_type": content_type,
|
||||
"length": length,
|
||||
}))
|
||||
monkeypatch.setattr("services.demo_media.parse_dicom", lambda dicom_dir_arg, output_dir: parsed_frame_paths)
|
||||
monkeypatch.setattr(
|
||||
"services.demo_media.upload_frames_to_minio",
|
||||
lambda frame_files, project_id: [f"projects/{project_id}/frames/frame_{idx:06d}.jpg" for idx, _ in enumerate(frame_files)],
|
||||
)
|
||||
|
||||
extra_user = User(username="doctor", password_hash=hash_password("secret123"), role="annotator", is_active=1)
|
||||
db_session.add(extra_user)
|
||||
@@ -113,7 +130,15 @@ def test_demo_factory_reset_leaves_admin_and_unparsed_demo_video(client, db_sess
|
||||
z_index=1,
|
||||
owner_user_id=extra_user.id,
|
||||
)
|
||||
db_session.add_all([task, private_template])
|
||||
system_template = Template(
|
||||
name="头颈部CT分割",
|
||||
description="头颈部CT分割",
|
||||
color="#ef4444",
|
||||
z_index=10,
|
||||
owner_user_id=None,
|
||||
mapping_rules={"classes": [{"name": "肿瘤/结节 (Tumor/Nodule)", "color": "#ff0000", "maskId": 1}], "rules": []},
|
||||
)
|
||||
db_session.add_all([task, private_template, system_template])
|
||||
db_session.commit()
|
||||
db_session.refresh(frame)
|
||||
annotation = Annotation(project_id=old_project.id, frame_id=frame.id, mask_data={"label": "old"})
|
||||
@@ -130,24 +155,36 @@ def test_demo_factory_reset_leaves_admin_and_unparsed_demo_video(client, db_sess
|
||||
data = response.json()
|
||||
assert data["message"] == "演示环境已恢复出厂设置"
|
||||
assert data["admin_user"]["username"] == "admin"
|
||||
assert data["project"]["name"] == "Data_MyVideo_1"
|
||||
assert data["project"]["status"] == PROJECT_STATUS_PENDING
|
||||
assert data["project"]["frame_count"] == 0
|
||||
assert data["project"]["video_path"] == f"uploads/{data['project']['id']}/Data_MyVideo_1.mp4"
|
||||
assert uploaded == [{
|
||||
"object_name": data["project"]["video_path"],
|
||||
"data": b"demo-video",
|
||||
"content_type": "video/mp4",
|
||||
"length": len(b"demo-video"),
|
||||
}]
|
||||
assert data["project"]["name"] == "演示DICOM序列"
|
||||
assert data["project"]["status"] == PROJECT_STATUS_READY
|
||||
assert data["project"]["source_type"] == "dicom"
|
||||
assert data["project"]["frame_count"] == 3
|
||||
assert data["project"]["video_path"] == f"uploads/{data['project']['id']}/dicom"
|
||||
assert [project["name"] for project in data["projects"]] == ["Data_MyVideo_1", "演示DICOM序列"]
|
||||
assert data["projects"][0]["status"] == "pending"
|
||||
assert data["projects"][0]["source_type"] == "video"
|
||||
assert data["projects"][0]["frame_count"] == 0
|
||||
assert data["projects"][1]["status"] == PROJECT_STATUS_READY
|
||||
assert data["projects"][1]["source_type"] == "dicom"
|
||||
assert data["projects"][1]["frame_count"] == 3
|
||||
assert [item["object_name"] for item in uploaded] == [
|
||||
f"uploads/{data['projects'][0]['id']}/Data_MyVideo_1.mp4",
|
||||
f"uploads/{data['project']['id']}/dicom/1.dcm",
|
||||
f"uploads/{data['project']['id']}/dicom/2.dcm",
|
||||
f"uploads/{data['project']['id']}/dicom/10.dcm",
|
||||
]
|
||||
assert [item["content_type"] for item in uploaded] == ["video/mp4", "application/dicom", "application/dicom", "application/dicom"]
|
||||
|
||||
assert [user.username for user in db_session.query(User).all()] == ["admin"]
|
||||
assert db_session.query(Project).count() == 1
|
||||
assert db_session.query(Frame).count() == 0
|
||||
assert db_session.query(Project).count() == 2
|
||||
assert db_session.query(Frame).count() == 3
|
||||
assert [frame.source_frame_number for frame in db_session.query(Frame).order_by(Frame.frame_index).all()] == [0, 1, 2]
|
||||
assert db_session.query(Annotation).count() == 0
|
||||
assert db_session.query(Mask).count() == 0
|
||||
assert db_session.query(ProcessingTask).count() == 0
|
||||
assert db_session.query(Template).filter(Template.owner_user_id.is_not(None)).count() == 0
|
||||
preserved_templates = db_session.query(Template).filter(Template.owner_user_id.is_(None)).all()
|
||||
assert [template.name for template in preserved_templates] == ["头颈部CT分割"]
|
||||
assert db_session.query(AuditLog).count() == 1
|
||||
assert db_session.query(AuditLog).first().action == "admin.demo_factory_reset"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user