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:
@@ -1,14 +1,9 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
|
||||
|
||||
ConfirmStatus = Literal["待确认", "已确认", "退回"]
|
||||
MilestoneStatus = Literal["未开始", "进行中", "已完成", "延期"]
|
||||
|
||||
|
||||
class ProjectMilestoneItem(BaseModel):
|
||||
id: str
|
||||
name: str = ""
|
||||
@@ -18,7 +13,7 @@ class ProjectMilestoneItem(BaseModel):
|
||||
durationDays: int = 1
|
||||
owner: str = ""
|
||||
remark: str = ""
|
||||
status: MilestoneStatus = "未开始"
|
||||
status: str = "未开始"
|
||||
|
||||
|
||||
class EnrollmentPlanItem(BaseModel):
|
||||
@@ -35,7 +30,7 @@ class SiteMilestoneItem(BaseModel):
|
||||
planDate: str = ""
|
||||
owner: str = ""
|
||||
remark: str = ""
|
||||
status: MilestoneStatus = "未开始"
|
||||
status: str = "未开始"
|
||||
|
||||
|
||||
class SiteEnrollmentPlanItem(BaseModel):
|
||||
@@ -63,12 +58,49 @@ class CenterConfirmItem(BaseModel):
|
||||
siteId: str = ""
|
||||
siteName: str = ""
|
||||
confirmer: str = ""
|
||||
confirmStatus: ConfirmStatus = "待确认"
|
||||
confirmStatus: str = "待确认"
|
||||
confirmDate: str = ""
|
||||
note: str = ""
|
||||
|
||||
|
||||
class VisitScheduleItem(BaseModel):
|
||||
visit_code: str = ""
|
||||
baseline_offset_days: int = 0
|
||||
window_before_days: int = 0
|
||||
window_after_days: int = 0
|
||||
|
||||
|
||||
class ProjectPublishSnapshot(BaseModel):
|
||||
code: str = ""
|
||||
name: str = ""
|
||||
project_full_name: str = ""
|
||||
sponsor: str = ""
|
||||
protocol_no: str = ""
|
||||
lead_unit: str = ""
|
||||
principal_investigator: str = ""
|
||||
main_pm: str = ""
|
||||
research_analysis: str = ""
|
||||
research_product: str = ""
|
||||
control_product: str = ""
|
||||
indication: str = ""
|
||||
research_population: str = ""
|
||||
research_design: str = ""
|
||||
plan_start_date: str = ""
|
||||
plan_end_date: str = ""
|
||||
planned_site_count: int | None = None
|
||||
planned_enrollment_count: int | None = None
|
||||
status: str = ""
|
||||
visit_schedule: list[VisitScheduleItem] = Field(default_factory=list)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def normalize_visit_schedule(self):
|
||||
for item in self.visit_schedule:
|
||||
item.visit_code = item.visit_code.strip()
|
||||
return self
|
||||
|
||||
|
||||
class StudySetupConfigData(BaseModel):
|
||||
projectInfo: ProjectPublishSnapshot = Field(default_factory=ProjectPublishSnapshot)
|
||||
projectMilestones: list[ProjectMilestoneItem] = Field(default_factory=list)
|
||||
enrollmentPlan: EnrollmentPlanItem = Field(default_factory=EnrollmentPlanItem)
|
||||
siteMilestones: list[SiteMilestoneItem] = Field(default_factory=list)
|
||||
@@ -143,49 +175,6 @@ class SetupProjectionSummary(BaseModel):
|
||||
skipped_items: list[SetupProjectionSkippedItem] = Field(default_factory=list)
|
||||
|
||||
|
||||
class VisitScheduleItem(BaseModel):
|
||||
visit_code: str = Field(min_length=1, max_length=50)
|
||||
baseline_offset_days: int = Field(ge=0, le=3650)
|
||||
window_before_days: int = Field(ge=0, le=365)
|
||||
window_after_days: int = Field(ge=0, le=365)
|
||||
|
||||
|
||||
class ProjectPublishSnapshot(BaseModel):
|
||||
code: str = ""
|
||||
name: str = ""
|
||||
project_full_name: str = ""
|
||||
sponsor: str = ""
|
||||
protocol_no: str = ""
|
||||
lead_unit: str = ""
|
||||
principal_investigator: str = ""
|
||||
main_pm: str = ""
|
||||
research_analysis: str = ""
|
||||
research_product: str = ""
|
||||
control_product: str = ""
|
||||
indication: str = ""
|
||||
research_population: str = ""
|
||||
research_design: str = ""
|
||||
plan_start_date: str = ""
|
||||
plan_end_date: str = ""
|
||||
planned_site_count: int | None = None
|
||||
planned_enrollment_count: int | None = None
|
||||
status: str = ""
|
||||
visit_schedule: list[VisitScheduleItem] = Field(default_factory=list)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_visit_schedule(self):
|
||||
codes: set[str] = set()
|
||||
for index, item in enumerate(self.visit_schedule):
|
||||
code = item.visit_code.strip()
|
||||
if not code:
|
||||
raise ValueError(f"第 {index + 1} 行访视编号不能为空")
|
||||
if code in codes:
|
||||
raise ValueError(f"访视编号重复:{code}")
|
||||
codes.add(code)
|
||||
item.visit_code = code
|
||||
return self
|
||||
|
||||
|
||||
class StudySetupConfigRead(BaseModel):
|
||||
id: uuid.UUID
|
||||
study_id: uuid.UUID
|
||||
|
||||
Reference in New Issue
Block a user