feat: refine subject visits and project workflows
Add early termination visit workflow with ordering, non-applicable visit handling, visit window display, and medication adherence support. Extend monitoring visit issue template fields, site scoping, setup draft project info handling, login security UI, attachment behavior, and related tests/migrations.
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
from datetime import date
|
||||
from types import SimpleNamespace
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
from app.api.v1.studies import _apply_project_publish_snapshot_to_study, _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_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"
|
||||
Reference in New Issue
Block a user