308 lines
9.3 KiB
Python
308 lines
9.3 KiB
Python
from datetime import date
|
|
from types import SimpleNamespace
|
|
|
|
from fastapi import HTTPException
|
|
|
|
from app.api.v1.studies import (
|
|
_apply_project_publish_snapshot_to_study,
|
|
_build_default_setup_config_from_study,
|
|
_resolve_setup_project_snapshot,
|
|
_validate_setup_data,
|
|
)
|
|
from app.schemas.study_setup_config import ProjectPublishSnapshot, StudySetupConfigData
|
|
|
|
|
|
def test_setup_config_data_keeps_project_info_draft():
|
|
draft = StudySetupConfigData.model_validate(
|
|
{
|
|
"projectInfo": {
|
|
"code": "PRJ-002",
|
|
"name": "草稿项目",
|
|
"plan_start_date": "2026-06-01",
|
|
"plan_end_date": "2026-12-31",
|
|
"planned_enrollment_count": 120,
|
|
"status": "ACTIVE",
|
|
"visit_schedule": [
|
|
{
|
|
"visit_code": "V1",
|
|
"baseline_offset_days": 7,
|
|
"window_before_days": 1,
|
|
"window_after_days": 2,
|
|
}
|
|
],
|
|
}
|
|
}
|
|
)
|
|
|
|
assert draft.projectInfo.code == "PRJ-002"
|
|
assert draft.projectInfo.name == "草稿项目"
|
|
assert draft.projectInfo.planned_enrollment_count == 120
|
|
assert draft.projectInfo.visit_schedule[0].visit_code == "V1"
|
|
|
|
|
|
def test_draft_schema_accepts_incomplete_project_snapshot_rows():
|
|
draft = StudySetupConfigData.model_validate(
|
|
{
|
|
"projectInfo": {
|
|
"code": "PRJ-002",
|
|
"name": "草稿项目",
|
|
"visit_schedule": [
|
|
{
|
|
"visit_code": "",
|
|
"baseline_offset_days": 0,
|
|
"window_before_days": 0,
|
|
"window_after_days": 0,
|
|
}
|
|
],
|
|
},
|
|
"projectMilestones": [
|
|
{
|
|
"id": "row-1",
|
|
"name": "",
|
|
"planDate": "",
|
|
"startDate": "",
|
|
"endDate": "",
|
|
"durationDays": 1,
|
|
"owner": "",
|
|
"remark": "",
|
|
"status": "",
|
|
}
|
|
],
|
|
"centerConfirm": [
|
|
{
|
|
"id": "row-1",
|
|
"siteId": "",
|
|
"siteName": "",
|
|
"confirmer": "",
|
|
"confirmStatus": "",
|
|
"confirmDate": "",
|
|
"note": "",
|
|
}
|
|
],
|
|
}
|
|
)
|
|
|
|
assert draft.projectInfo.visit_schedule[0].visit_code == ""
|
|
assert draft.projectMilestones[0].status == ""
|
|
assert draft.centerConfirm[0].confirmStatus == ""
|
|
|
|
|
|
def test_draft_save_allows_incomplete_setup_steps():
|
|
draft = StudySetupConfigData.model_validate(
|
|
{
|
|
"projectInfo": {
|
|
"code": "PRJ-002",
|
|
"name": "草稿项目",
|
|
"plan_start_date": "2026-05-01",
|
|
"plan_end_date": "2027-05-31",
|
|
},
|
|
"enrollmentPlan": {
|
|
"totalTarget": 180,
|
|
"startDate": "",
|
|
"endDate": "",
|
|
"monthlyGoalNote": "",
|
|
"stageBreakdown": "",
|
|
},
|
|
}
|
|
)
|
|
|
|
_validate_setup_data(
|
|
draft,
|
|
{},
|
|
project_plan_start=date(2026, 5, 1),
|
|
project_plan_end=date(2027, 5, 31),
|
|
strict_required=False,
|
|
)
|
|
|
|
|
|
def test_publish_validation_still_requires_complete_setup_steps():
|
|
draft = StudySetupConfigData.model_validate(
|
|
{
|
|
"projectInfo": {
|
|
"code": "PRJ-002",
|
|
"name": "草稿项目",
|
|
"plan_start_date": "2026-05-01",
|
|
"plan_end_date": "2027-05-31",
|
|
},
|
|
"enrollmentPlan": {
|
|
"totalTarget": 180,
|
|
"startDate": "",
|
|
"endDate": "",
|
|
"monthlyGoalNote": "",
|
|
"stageBreakdown": "",
|
|
},
|
|
}
|
|
)
|
|
|
|
try:
|
|
_validate_setup_data(
|
|
draft,
|
|
{},
|
|
project_plan_start=date(2026, 5, 1),
|
|
project_plan_end=date(2027, 5, 31),
|
|
)
|
|
except HTTPException as exc:
|
|
assert exc.status_code == 422
|
|
else:
|
|
raise AssertionError("publish validation should reject incomplete setup steps")
|
|
|
|
|
|
def test_project_info_is_part_of_setup_config_payload():
|
|
draft = StudySetupConfigData.model_validate(
|
|
{
|
|
"projectInfo": {
|
|
"code": "PRJ-VERSION",
|
|
"name": "版本内项目信息",
|
|
"project_full_name": "版本内项目全称",
|
|
"plan_start_date": "2026-06-01",
|
|
"plan_end_date": "2026-12-31",
|
|
"planned_site_count": 8,
|
|
"planned_enrollment_count": 120,
|
|
"status": "ACTIVE",
|
|
},
|
|
"enrollmentPlan": {
|
|
"totalTarget": 120,
|
|
"startDate": "2026-06-01",
|
|
"endDate": "2026-12-31",
|
|
"monthlyGoalNote": "",
|
|
"stageBreakdown": "",
|
|
},
|
|
}
|
|
)
|
|
|
|
payload = draft.model_dump(mode="json")
|
|
|
|
assert payload["projectInfo"]["code"] == "PRJ-VERSION"
|
|
assert payload["projectInfo"]["planned_enrollment_count"] == 120
|
|
assert payload["enrollmentPlan"]["totalTarget"] == 120
|
|
|
|
|
|
def test_default_setup_config_includes_project_info_snapshot():
|
|
study = SimpleNamespace(
|
|
code="PRJ-DEFAULT",
|
|
name="默认项目",
|
|
project_full_name="默认项目全称",
|
|
sponsor="申办方",
|
|
protocol_no="PROTO-DEFAULT",
|
|
lead_unit="组长单位",
|
|
principal_investigator="PI",
|
|
main_pm="PM",
|
|
research_analysis="III期",
|
|
research_product="研究药物",
|
|
control_product="对照药物",
|
|
indication="适应症",
|
|
research_population="研究人群",
|
|
research_design="随机双盲",
|
|
plan_start_date=date(2026, 6, 1),
|
|
plan_end_date=date(2026, 12, 31),
|
|
planned_site_count=8,
|
|
planned_enrollment_count=120,
|
|
enrollment_monthly_goal_note="",
|
|
enrollment_stage_breakdown="",
|
|
status="ACTIVE",
|
|
visit_schedule=[
|
|
{
|
|
"visit_code": "V1",
|
|
"baseline_offset_days": 7,
|
|
"window_before_days": 1,
|
|
"window_after_days": 2,
|
|
}
|
|
],
|
|
)
|
|
|
|
draft = _build_default_setup_config_from_study(study, [])
|
|
|
|
assert draft.projectInfo.code == "PRJ-DEFAULT"
|
|
assert draft.projectInfo.name == "默认项目"
|
|
assert draft.projectInfo.planned_enrollment_count == 120
|
|
assert draft.projectInfo.visit_schedule[0].visit_code == "V1"
|
|
|
|
|
|
def test_empty_project_info_does_not_fallback_to_study_snapshot():
|
|
study = SimpleNamespace(
|
|
code="PRJ-MASTER",
|
|
name="主表项目",
|
|
project_full_name="",
|
|
sponsor="",
|
|
protocol_no="",
|
|
lead_unit="",
|
|
principal_investigator="",
|
|
main_pm="",
|
|
research_analysis="",
|
|
research_product="",
|
|
control_product="",
|
|
indication="",
|
|
research_population="",
|
|
research_design="",
|
|
plan_start_date=None,
|
|
plan_end_date=None,
|
|
planned_site_count=None,
|
|
planned_enrollment_count=None,
|
|
status="DRAFT",
|
|
visit_schedule=[],
|
|
)
|
|
draft = StudySetupConfigData()
|
|
|
|
snapshot = _resolve_setup_project_snapshot(draft, study)
|
|
|
|
assert snapshot.code == ""
|
|
assert snapshot.name == ""
|
|
|
|
|
|
def test_apply_project_publish_snapshot_updates_formal_study_fields():
|
|
study = SimpleNamespace(
|
|
code="PRJ-001",
|
|
name="原项目",
|
|
project_full_name=None,
|
|
sponsor=None,
|
|
protocol_no=None,
|
|
lead_unit=None,
|
|
principal_investigator=None,
|
|
main_pm=None,
|
|
research_analysis=None,
|
|
research_product=None,
|
|
control_product=None,
|
|
indication=None,
|
|
research_population=None,
|
|
research_design=None,
|
|
plan_start_date=None,
|
|
plan_end_date=None,
|
|
planned_site_count=None,
|
|
planned_enrollment_count=10,
|
|
status="DRAFT",
|
|
visit_schedule=[],
|
|
)
|
|
snapshot = ProjectPublishSnapshot(
|
|
code="PRJ-002",
|
|
name="发布项目",
|
|
project_full_name="发布项目全称",
|
|
sponsor="申办方",
|
|
plan_start_date="2026-06-01",
|
|
plan_end_date="2026-12-31",
|
|
planned_site_count=8,
|
|
planned_enrollment_count=120,
|
|
status="ACTIVE",
|
|
visit_schedule=[
|
|
{
|
|
"visit_code": "V1",
|
|
"baseline_offset_days": 7,
|
|
"window_before_days": 1,
|
|
"window_after_days": 2,
|
|
}
|
|
],
|
|
)
|
|
|
|
changed = _apply_project_publish_snapshot_to_study(study, snapshot)
|
|
|
|
assert changed is True
|
|
assert study.code == "PRJ-002"
|
|
assert study.name == "发布项目"
|
|
assert study.project_full_name == "发布项目全称"
|
|
assert study.sponsor == "申办方"
|
|
assert study.plan_start_date == date(2026, 6, 1)
|
|
assert study.plan_end_date == date(2026, 12, 31)
|
|
assert study.planned_site_count == 8
|
|
assert study.planned_enrollment_count == 120
|
|
assert study.status == "ACTIVE"
|
|
assert study.visit_schedule[0]["visit_code"] == "V1"
|