Step 7:AE + 风险/问题管理(PV)

This commit is contained in:
Cheng Zhou
2025-12-16 17:55:53 +08:00
parent c77b20f932
commit f19faf1a1a
63 changed files with 707 additions and 1 deletions
+113
View File
@@ -0,0 +1,113 @@
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.models.ae import AdverseEvent
from app.models.site import Site
from app.models.subject import Subject
from app.schemas.ae import AECreate, AEUpdate
def _calc_due_date(onset: date | None, seriousness: str) -> date | None:
if onset is None:
return None
delta = 1 if seriousness == "SERIOUS" 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("Site not found in study")
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("Subject not found in study")
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,
severity=ae_in.severity,
causality=ae_in.causality,
action_taken=None,
outcome=None,
reported_to_sponsor=False,
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,
) -> Sequence[AdverseEvent]:
stmt = select(AdverseEvent).where(AdverseEvent.study_id == study_id)
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, ae: AdverseEvent, ae_in: AEUpdate) -> AdverseEvent:
update_data = ae_in.model_dump(exclude_unset=True)
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
+99
View File
@@ -0,0 +1,99 @@
import uuid
from datetime import date, datetime, timezone
from typing import Sequence
from sqlalchemy import select, update as sa_update
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.issue import Issue
from app.models.site import Site
from app.models.subject import Subject
from app.schemas.issue import IssueCreate, IssueUpdate
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("Site not found in study")
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("Subject not found in study")
async def create_issue(
db: AsyncSession,
study_id: uuid.UUID,
issue_in: IssueCreate,
*,
created_by: uuid.UUID,
) -> Issue:
await _validate_site_subject(db, study_id, issue_in.site_id, issue_in.subject_id)
issue = Issue(
study_id=study_id,
site_id=issue_in.site_id,
subject_id=issue_in.subject_id,
title=issue_in.title,
description=issue_in.description,
category=issue_in.category,
level=issue_in.level,
owner_id=issue_in.owner_id,
due_date=issue_in.due_date,
status="OPEN",
capa=None,
closed_at=None,
created_by=created_by,
)
db.add(issue)
await db.commit()
await db.refresh(issue)
return issue
async def get_issue(db: AsyncSession, issue_id: uuid.UUID) -> Issue | None:
result = await db.execute(select(Issue).where(Issue.id == issue_id))
return result.scalar_one_or_none()
async def list_issues(
db: AsyncSession,
study_id: uuid.UUID,
status: str | None = None,
level: str | None = None,
category: str | None = None,
overdue: bool | None = None,
) -> Sequence[Issue]:
stmt = select(Issue).where(Issue.study_id == study_id)
if status:
stmt = stmt.where(Issue.status == status)
if level:
stmt = stmt.where(Issue.level == level)
if category:
stmt = stmt.where(Issue.category == category)
if overdue is True:
stmt = stmt.where(Issue.due_date < date.today(), Issue.status != "CLOSED")
if overdue is False:
stmt = stmt.where((Issue.due_date >= date.today()) | (Issue.due_date.is_(None)) | (Issue.status == "CLOSED"))
result = await db.execute(stmt)
return result.scalars().all()
async def update_issue(db: AsyncSession, issue: Issue, issue_in: IssueUpdate) -> Issue:
update_data = issue_in.model_dump(exclude_unset=True)
if "status" in update_data:
if update_data["status"] == "CLOSED":
update_data["closed_at"] = datetime.now(timezone.utc)
else:
update_data["closed_at"] = None
if update_data:
await db.execute(
sa_update(Issue)
.where(Issue.id == issue.id)
.values(**update_data)
)
await db.commit()
await db.refresh(issue)
return issue