Files
ctms/backend/app/crud/study.py
T
2026-05-28 10:56:28 +08:00

220 lines
8.5 KiB
Python

from __future__ import annotations
import uuid
from typing import Sequence
from sqlalchemy import delete as sa_delete, select, update as sa_update
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.study import Study
from app.schemas.study import StudyCreate, StudyUpdate
async def create(db: AsyncSession, study_in: StudyCreate, *, created_by: uuid.UUID | None = None) -> Study:
study = Study(
code=study_in.code.strip(),
name=study_in.name,
project_full_name=study_in.project_full_name,
sponsor=study_in.sponsor,
protocol_no=study_in.protocol_no,
lead_unit=study_in.lead_unit,
principal_investigator=study_in.principal_investigator,
main_pm=study_in.main_pm,
research_analysis=study_in.research_analysis,
research_product=study_in.research_product,
control_product=study_in.control_product,
indication=study_in.indication,
research_population=study_in.research_population,
research_design=study_in.research_design,
plan_start_date=study_in.plan_start_date,
plan_end_date=study_in.plan_end_date,
planned_site_count=study_in.planned_site_count,
planned_enrollment_count=study_in.planned_enrollment_count,
enrollment_monthly_goal_note=study_in.enrollment_monthly_goal_note,
enrollment_stage_breakdown=study_in.enrollment_stage_breakdown,
phase=study_in.phase,
status=study_in.status,
visit_schedule=[item.model_dump() for item in study_in.visit_schedule],
created_by=created_by,
)
db.add(study)
await db.commit()
await db.refresh(study)
return study
async def get(db: AsyncSession, study_id: uuid.UUID) -> Study | None:
result = await db.execute(select(Study).where(Study.id == study_id))
return result.scalar_one_or_none()
async def get_by_code(db: AsyncSession, code: str) -> Study | None:
result = await db.execute(select(Study).where(Study.code == code))
return result.scalar_one_or_none()
async def update(db: AsyncSession, study: Study, study_in: StudyUpdate) -> Study:
update_data = study_in.model_dump(exclude_unset=True)
if update_data:
await db.execute(
sa_update(Study)
.where(Study.id == study.id)
.values(**update_data)
)
await db.commit()
await db.refresh(study)
return study
async def list_studies(db: AsyncSession, skip: int = 0, limit: int = 100) -> Sequence[Study]:
result = await db.execute(select(Study).offset(skip).limit(limit))
return result.scalars().all()
async def list_studies_for_user(
db: AsyncSession,
user_id: uuid.UUID,
skip: int = 0,
limit: int = 100,
) -> Sequence[Study]:
from app.models.study_member import StudyMember # local import to avoid cycles
stmt = (
select(Study)
.join(StudyMember, StudyMember.study_id == Study.id)
.where(StudyMember.user_id == user_id, StudyMember.is_active.is_(True))
.offset(skip)
.limit(limit)
)
result = await db.execute(stmt)
return result.scalars().all()
async def delete(db: AsyncSession, study_id: uuid.UUID) -> None:
"""硬删除项目及其所有关联数据"""
# 导入所有关联的模型
from app.models.study_member import StudyMember
from app.models.audit_log import AuditLog
from app.models.site import Site
from app.models.subject import Subject
from app.models.subject_history import SubjectHistory
from app.models.visit import Visit
from app.models.ae import AdverseEvent
from app.models.finance import FinanceItem
from app.models.contract_fee import ContractFee
from app.models.contract_fee_payment import ContractFeePayment
from app.models.milestone import Milestone
from app.models.document import Document
from app.models.attachment import Attachment
from app.models.drug_shipment import DrugShipment
from app.models.material_equipment import MaterialEquipment
from app.models.training_authorization import TrainingAuthorization
from app.models.startup_feasibility import StartupFeasibility
from app.models.startup_ethics import StartupEthics
from app.models.kickoff_meeting import KickoffMeeting
from app.models.precaution import Precaution
from app.models.faq_category import FaqCategory
from app.models.faq_item import FaqItem
from app.models.faq_reply import FaqReply
from app.models.study_setup_config import StudySetupConfig
from app.models.study_setup_config_version import StudySetupConfigVersion
from app.models.study_monitoring_strategy import StudyMonitoringStrategy
from app.models.study_center_confirm import StudyCenterConfirm
# 按依赖关系顺序删除关联数据
# 1. 删除审计日志
await db.execute(sa_delete(AuditLog).where(AuditLog.study_id == study_id))
# 2. 删除FAQ相关(可能有依赖关系)
await db.execute(sa_delete(FaqReply).where(FaqReply.study_id == study_id))
await db.execute(sa_delete(FaqItem).where(FaqItem.study_id == study_id))
await db.execute(sa_delete(FaqCategory).where(FaqCategory.study_id == study_id))
# 3. 删除注意事项
await db.execute(sa_delete(Precaution).where(Precaution.study_id == study_id))
# 4. 删除启动相关
await db.execute(sa_delete(KickoffMeeting).where(KickoffMeeting.study_id == study_id))
await db.execute(sa_delete(StartupEthics).where(StartupEthics.study_id == study_id))
await db.execute(sa_delete(StartupFeasibility).where(StartupFeasibility.study_id == study_id))
await db.execute(sa_delete(TrainingAuthorization).where(TrainingAuthorization.study_id == study_id))
# 6. 删除药物配送
await db.execute(sa_delete(DrugShipment).where(DrugShipment.study_id == study_id))
await db.execute(sa_delete(MaterialEquipment).where(MaterialEquipment.study_id == study_id))
# 7. 删除附件和文档
await db.execute(sa_delete(Attachment).where(Attachment.study_id == study_id))
await db.execute(sa_delete(Document).where(Document.trial_id == study_id))
# 8. 删除里程碑
await db.execute(sa_delete(Milestone).where(Milestone.study_id == study_id))
# 9. 删除财务相关
await db.execute(
sa_delete(ContractFeePayment).where(
ContractFeePayment.contract_fee_id.in_(
select(ContractFee.id).where(ContractFee.project_id == study_id)
)
)
)
await db.execute(sa_delete(ContractFee).where(ContractFee.project_id == study_id))
await db.execute(sa_delete(FinanceItem).where(FinanceItem.study_id == study_id))
# 10. 删除不良事件
await db.execute(sa_delete(AdverseEvent).where(AdverseEvent.study_id == study_id))
# 11. 删除访视
await db.execute(sa_delete(Visit).where(Visit.study_id == study_id))
# 12. 删除受试者相关
await db.execute(sa_delete(SubjectHistory).where(SubjectHistory.study_id == study_id))
await db.execute(sa_delete(Subject).where(Subject.study_id == study_id))
# 13. 删除立项联动实体(需先于站点删除,避免外键约束)
await db.execute(sa_delete(StudyMonitoringStrategy).where(StudyMonitoringStrategy.study_id == study_id))
await db.execute(sa_delete(StudyCenterConfirm).where(StudyCenterConfirm.study_id == study_id))
# 14. 删除站点
await db.execute(sa_delete(Site).where(Site.study_id == study_id))
# 15. 删除成员
await db.execute(sa_delete(StudyMember).where(StudyMember.study_id == study_id))
# 16. 删除立项配置
await db.execute(sa_delete(StudySetupConfigVersion).where(StudySetupConfigVersion.study_id == study_id))
await db.execute(sa_delete(StudySetupConfig).where(StudySetupConfig.study_id == study_id))
# 17. 最后删除项目本身
await db.execute(sa_delete(Study).where(Study.id == study_id))
await db.commit()
async def lock(db: AsyncSession, study_id: uuid.UUID) -> Study | None:
"""锁定项目"""
await db.execute(
sa_update(Study)
.where(Study.id == study_id)
.values(is_locked=True)
)
await db.commit()
return await get(db, study_id)
async def unlock(db: AsyncSession, study_id: uuid.UUID) -> Study | None:
"""解锁项目"""
await db.execute(
sa_update(Study)
.where(Study.id == study_id)
.values(is_locked=False)
)
await db.commit()
return await get(db, study_id)
async def is_locked(db: AsyncSession, study_id: uuid.UUID) -> bool:
"""检查项目是否已锁定"""
study = await get(db, study_id)
return study.is_locked if study else False