Files
ctms/backend/app/schemas/study.py
T
2026-05-12 10:16:52 +08:00

132 lines
4.5 KiB
Python

import uuid
from datetime import date, datetime
from typing import Literal, Optional
from pydantic import BaseModel, ConfigDict, Field, model_validator
StudyStatus = Literal["DRAFT", "ACTIVE", "CLOSED"]
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 VisitScheduleMixin(BaseModel):
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 StudyCreate(VisitScheduleMixin):
name: str = Field(min_length=1)
code: str = Field(min_length=1)
project_full_name: Optional[str] = None
sponsor: Optional[str] = None
protocol_no: Optional[str] = None
lead_unit: Optional[str] = None
principal_investigator: Optional[str] = None
main_pm: Optional[str] = None
research_analysis: Optional[str] = None
research_product: Optional[str] = None
control_product: Optional[str] = None
indication: Optional[str] = None
research_population: Optional[str] = None
research_design: Optional[str] = None
plan_start_date: Optional[date] = None
plan_end_date: Optional[date] = None
planned_site_count: Optional[int] = None
planned_enrollment_count: Optional[int] = None
enrollment_monthly_goal_note: Optional[str] = None
enrollment_stage_breakdown: Optional[str] = None
phase: Optional[str] = None
status: StudyStatus = "DRAFT"
class StudyUpdate(BaseModel):
code: Optional[str] = None
name: Optional[str] = None
project_full_name: Optional[str] = None
sponsor: Optional[str] = None
protocol_no: Optional[str] = None
lead_unit: Optional[str] = None
principal_investigator: Optional[str] = None
main_pm: Optional[str] = None
research_analysis: Optional[str] = None
research_product: Optional[str] = None
control_product: Optional[str] = None
indication: Optional[str] = None
research_population: Optional[str] = None
research_design: Optional[str] = None
plan_start_date: Optional[date] = None
plan_end_date: Optional[date] = None
planned_site_count: Optional[int] = None
planned_enrollment_count: Optional[int] = None
enrollment_monthly_goal_note: Optional[str] = None
enrollment_stage_breakdown: Optional[str] = None
phase: Optional[str] = None
status: Optional[StudyStatus] = None
is_locked: Optional[bool] = None
visit_schedule: Optional[list[VisitScheduleItem]] = None
@model_validator(mode="after")
def validate_visit_schedule(self):
if self.visit_schedule is None:
return 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 StudyRead(BaseModel):
id: uuid.UUID
code: str
name: str
project_full_name: Optional[str]
sponsor: Optional[str]
protocol_no: Optional[str]
lead_unit: Optional[str]
principal_investigator: Optional[str]
main_pm: Optional[str]
research_analysis: Optional[str]
research_product: Optional[str]
control_product: Optional[str]
indication: Optional[str]
research_population: Optional[str]
research_design: Optional[str]
plan_start_date: Optional[date]
plan_end_date: Optional[date]
planned_site_count: Optional[int]
planned_enrollment_count: Optional[int]
enrollment_monthly_goal_note: Optional[str]
enrollment_stage_breakdown: Optional[str]
phase: Optional[str]
status: StudyStatus
is_locked: bool
visit_schedule: list[VisitScheduleItem]
created_by: Optional[uuid.UUID]
created_at: datetime
role_in_study: Optional[str] = None
model_config = ConfigDict(from_attributes=True)