Files
ctms/backend/app/crud/subject.py
T

134 lines
4.4 KiB
Python

import uuid
from datetime import date, timedelta
from typing import Sequence
from sqlalchemy import select, update as sa_update
from sqlalchemy.ext.asyncio import AsyncSession
from app.crud import visit as visit_crud
from app.models.study import Study
from app.models.visit import Visit
from app.models.site import Site
from app.models.subject import Subject
from app.schemas.subject import SubjectCreate, SubjectUpdate
async def _validate_site(db: AsyncSession, study_id: uuid.UUID, site_id: uuid.UUID) -> None:
result = await db.execute(select(Site).where(Site.id == site_id))
site = result.scalar_one_or_none()
if not site or site.study_id != study_id:
raise ValueError("分中心不属于当前项目")
async def create_subject(db: AsyncSession, study_id: uuid.UUID, subject_in: SubjectCreate) -> Subject:
await _validate_site(db, study_id, subject_in.site_id)
subject = Subject(
study_id=study_id,
site_id=subject_in.site_id,
subject_no=subject_in.subject_no,
status="SCREENING",
screening_date=subject_in.screening_date,
consent_date=subject_in.consent_date,
enrollment_date=None,
completion_date=None,
drop_reason=None,
)
db.add(subject)
await db.commit()
await db.refresh(subject)
# initial visit: Screening (V0)
await visit_crud.create_visit(
db,
study_id=study_id,
visit_in=None,
subject=subject,
visit_code="V0",
planned_date=subject.screening_date,
)
return subject
async def get_subject(db: AsyncSession, subject_id: uuid.UUID) -> Subject | None:
result = await db.execute(select(Subject).where(Subject.id == subject_id))
return result.scalar_one_or_none()
async def list_subjects(
db: AsyncSession,
study_id: uuid.UUID,
site_id: uuid.UUID | None = None,
status: str | None = None,
subject_no: str | None = None,
) -> Sequence[Subject]:
stmt = select(Subject).where(Subject.study_id == study_id)
if site_id:
stmt = stmt.where(Subject.site_id == site_id)
if status:
stmt = stmt.where(Subject.status == status)
if subject_no:
stmt = stmt.where(Subject.subject_no.ilike(f"%{subject_no}%"))
result = await db.execute(stmt)
return result.scalars().all()
async def generate_default_visits(db: AsyncSession, subject: Subject) -> None:
# Baseline + Follow-up visits based on enrollment_date
if not subject.enrollment_date:
return
result = await db.execute(select(Study).where(Study.id == subject.study_id))
study = result.scalar_one_or_none()
if not study:
return
visit_total = study.visit_total or 3
visit_interval_days = study.visit_interval_days or 30
window_start_offset = study.visit_window_start_offset
window_end_offset = study.visit_window_end_offset
baseline_date = subject.enrollment_date
result = await db.execute(select(Visit.visit_code).where(Visit.subject_id == subject.id))
existing_codes = {row[0] for row in result.all()}
if "V1" not in existing_codes:
window_start = baseline_date + timedelta(days=window_start_offset) if window_start_offset is not None else None
window_end = baseline_date + timedelta(days=window_end_offset) if window_end_offset is not None else None
await visit_crud.create_visit(
db,
study_id=subject.study_id,
visit_in=None,
subject=subject,
visit_code="V1",
planned_date=baseline_date,
window_start=window_start,
window_end=window_end,
)
await visit_crud.create_followup_visits(
db,
study_id=subject.study_id,
subject=subject,
base_date=baseline_date,
visit_total=visit_total,
visit_interval_days=visit_interval_days,
window_start_offset=window_start_offset,
window_end_offset=window_end_offset,
)
async def update_subject(db: AsyncSession, subject: Subject, subject_in: SubjectUpdate) -> Subject:
update_data = subject_in.model_dump(exclude_unset=True)
if update_data:
await db.execute(
sa_update(Subject)
.where(Subject.id == subject.id)
.values(**update_data)
)
await db.commit()
await db.refresh(subject)
return subject
async def delete_subject(db: AsyncSession, subject: Subject) -> None:
await db.delete(subject)
await db.commit()