158 lines
5.8 KiB
Python
158 lines
5.8 KiB
Python
import uuid
|
|
from datetime import date, timedelta
|
|
from typing import Sequence
|
|
|
|
from sqlalchemy import or_, select, update as sa_update
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.models.ae import AdverseEvent
|
|
from app.models.site import Site
|
|
from app.models.subject import Subject
|
|
from app.schemas.ae import AECreate, AEUpdate
|
|
|
|
|
|
def _seriousness_rank(value: str | None) -> int | None:
|
|
if not value:
|
|
return None
|
|
normalized = str(value).strip().upper()
|
|
roman_map = {"I": 1, "II": 2, "III": 3, "IV": 4, "V": 5}
|
|
legacy_map = {"SERIOUS": 5, "NON_SERIOUS": 1}
|
|
return roman_map.get(normalized) or legacy_map.get(normalized)
|
|
|
|
|
|
def _calc_due_date(onset: date | None, seriousness: str) -> date | None:
|
|
if onset is None:
|
|
return None
|
|
rank = _seriousness_rank(seriousness)
|
|
delta = 1 if rank and rank >= 3 else 7
|
|
return onset + timedelta(days=delta)
|
|
|
|
|
|
async def _validate_site_subject(db: AsyncSession, study_id: uuid.UUID, site_id: uuid.UUID | None, subject_id: uuid.UUID | None):
|
|
if site_id:
|
|
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("分中心不属于当前项目")
|
|
if not site.is_active:
|
|
raise ValueError("中心已停用")
|
|
if subject_id:
|
|
result = await db.execute(select(Subject).where(Subject.id == subject_id))
|
|
subj = result.scalar_one_or_none()
|
|
if not subj or subj.study_id != study_id:
|
|
raise ValueError("参与者不属于当前项目")
|
|
result = await db.execute(select(Site).where(Site.id == subj.site_id))
|
|
subj_site = result.scalar_one_or_none()
|
|
if subj_site and not subj_site.is_active:
|
|
raise ValueError("中心已停用")
|
|
|
|
|
|
async def create_ae(
|
|
db: AsyncSession,
|
|
study_id: uuid.UUID,
|
|
ae_in: AECreate,
|
|
*,
|
|
created_by: uuid.UUID,
|
|
) -> AdverseEvent:
|
|
await _validate_site_subject(db, study_id, ae_in.site_id, ae_in.subject_id)
|
|
due_date = _calc_due_date(ae_in.onset_date, ae_in.seriousness)
|
|
ae = AdverseEvent(
|
|
study_id=study_id,
|
|
site_id=ae_in.site_id,
|
|
subject_id=ae_in.subject_id,
|
|
visit_id=ae_in.visit_id,
|
|
term=ae_in.term,
|
|
onset_date=ae_in.onset_date,
|
|
resolution_date=None,
|
|
seriousness=ae_in.seriousness.value,
|
|
severity=ae_in.seriousness.value,
|
|
causality=ae_in.causality,
|
|
action_taken=None,
|
|
outcome=ae_in.outcome,
|
|
reported_to_sponsor=False,
|
|
is_sae=bool(ae_in.is_sae or ae_in.is_susar),
|
|
is_susar=bool(ae_in.is_susar),
|
|
report_due_date=due_date,
|
|
status="NEW",
|
|
description=ae_in.description,
|
|
created_by=created_by,
|
|
)
|
|
db.add(ae)
|
|
await db.commit()
|
|
await db.refresh(ae)
|
|
return ae
|
|
|
|
|
|
async def get_ae(db: AsyncSession, ae_id: uuid.UUID) -> AdverseEvent | None:
|
|
result = await db.execute(select(AdverseEvent).where(AdverseEvent.id == ae_id))
|
|
return result.scalar_one_or_none()
|
|
|
|
|
|
async def list_ae(
|
|
db: AsyncSession,
|
|
study_id: uuid.UUID,
|
|
status: str | None = None,
|
|
seriousness: str | None = None,
|
|
site_id: uuid.UUID | None = None,
|
|
subject_id: uuid.UUID | None = None,
|
|
overdue: bool | None = None,
|
|
site_ids: set[uuid.UUID] | None = None,
|
|
) -> Sequence[AdverseEvent]:
|
|
stmt = select(AdverseEvent).where(AdverseEvent.study_id == study_id)
|
|
if site_ids is not None:
|
|
if not site_ids:
|
|
return []
|
|
subject_site_ids = select(Subject.id).where(Subject.site_id.in_(site_ids))
|
|
stmt = stmt.where(
|
|
or_(
|
|
AdverseEvent.site_id.in_(site_ids),
|
|
AdverseEvent.subject_id.in_(subject_site_ids),
|
|
)
|
|
)
|
|
if status:
|
|
stmt = stmt.where(AdverseEvent.status == status)
|
|
if seriousness:
|
|
stmt = stmt.where(AdverseEvent.seriousness == seriousness)
|
|
if site_id:
|
|
stmt = stmt.where(AdverseEvent.site_id == site_id)
|
|
if subject_id:
|
|
stmt = stmt.where(AdverseEvent.subject_id == subject_id)
|
|
if overdue is True:
|
|
stmt = stmt.where(AdverseEvent.report_due_date < date.today(), AdverseEvent.status != "CLOSED")
|
|
if overdue is False:
|
|
stmt = stmt.where((AdverseEvent.report_due_date >= date.today()) | (AdverseEvent.report_due_date.is_(None)) | (AdverseEvent.status == "CLOSED"))
|
|
result = await db.execute(stmt)
|
|
return result.scalars().all()
|
|
|
|
|
|
async def update_ae(db: AsyncSession, study_id: uuid.UUID, ae: AdverseEvent, ae_in: AEUpdate) -> AdverseEvent:
|
|
update_data = ae_in.model_dump(exclude_unset=True)
|
|
if "is_susar" in update_data and update_data["is_susar"]:
|
|
update_data["is_sae"] = True
|
|
if "is_sae" in update_data and update_data["is_sae"] is False:
|
|
update_data["is_susar"] = False
|
|
if "seriousness" in update_data:
|
|
severity_value = update_data["seriousness"].value if hasattr(update_data["seriousness"], "value") else str(update_data["seriousness"])
|
|
update_data["seriousness"] = severity_value
|
|
update_data["severity"] = severity_value
|
|
if "subject_id" in update_data:
|
|
await _validate_site_subject(db, study_id, None, update_data["subject_id"])
|
|
if "onset_date" in update_data or "seriousness" in update_data:
|
|
onset = update_data.get("onset_date", ae.onset_date)
|
|
seriousness = update_data.get("seriousness", ae.seriousness)
|
|
update_data["report_due_date"] = _calc_due_date(onset, seriousness)
|
|
if update_data:
|
|
await db.execute(
|
|
sa_update(AdverseEvent)
|
|
.where(AdverseEvent.id == ae.id)
|
|
.values(**update_data)
|
|
)
|
|
await db.commit()
|
|
await db.refresh(ae)
|
|
return ae
|
|
|
|
|
|
async def delete_ae(db: AsyncSession, ae: AdverseEvent) -> None:
|
|
await db.delete(ae)
|
|
await db.commit()
|