feat: derive subject status from visits
This commit is contained in:
@@ -16,6 +16,55 @@ from app.models.subject import Subject
|
||||
from app.schemas.subject import SubjectCreate, SubjectUpdate
|
||||
|
||||
|
||||
def _visit_code(visit) -> str:
|
||||
return str(getattr(visit, "visit_code", "") or "").strip()
|
||||
|
||||
|
||||
def _is_cancelled_visit(visit) -> bool:
|
||||
return str(getattr(visit, "status", "") or "").strip().upper() == "CANCELLED"
|
||||
|
||||
|
||||
def _standard_visit_codes(visit_schedule: list[dict] | None) -> set[str]:
|
||||
return {
|
||||
str(item.get("visit_code") or "").strip()
|
||||
for item in visit_schedule or []
|
||||
if str(item.get("visit_code") or "").strip()
|
||||
and str(item.get("visit_code") or "").strip() not in visit_crud.NON_TREATMENT_VISIT_CODES
|
||||
}
|
||||
|
||||
|
||||
def derive_subject_status(subject, visits: Sequence[Visit], visit_schedule: list[dict] | None = None) -> str:
|
||||
if any(_visit_code(visit) == "提前终止" and getattr(visit, "actual_date", None) for visit in visits):
|
||||
return "DROPPED"
|
||||
|
||||
standard_codes = _standard_visit_codes(visit_schedule)
|
||||
completion_visits = [
|
||||
visit
|
||||
for visit in visits
|
||||
if _visit_code(visit) not in visit_crud.NON_TREATMENT_VISIT_CODES
|
||||
and not _is_cancelled_visit(visit)
|
||||
and getattr(visit, "planned_date", None) is not None
|
||||
]
|
||||
if standard_codes:
|
||||
standard_visits = [visit for visit in completion_visits if _visit_code(visit) in standard_codes]
|
||||
if standard_visits:
|
||||
latest_planned_date = max(getattr(visit, "planned_date") for visit in completion_visits)
|
||||
latest_visits = [
|
||||
visit for visit in completion_visits if getattr(visit, "planned_date", None) == latest_planned_date
|
||||
]
|
||||
if latest_visits and all(getattr(visit, "actual_date", None) for visit in latest_visits):
|
||||
return "COMPLETED"
|
||||
elif completion_visits:
|
||||
latest_planned_date = max(getattr(visit, "planned_date") for visit in completion_visits)
|
||||
latest_visits = [visit for visit in completion_visits if getattr(visit, "planned_date", None) == latest_planned_date]
|
||||
if latest_visits and all(getattr(visit, "actual_date", None) for visit in latest_visits):
|
||||
return "COMPLETED"
|
||||
|
||||
if getattr(subject, "enrollment_date", None):
|
||||
return "ENROLLED"
|
||||
return "SCREENING"
|
||||
|
||||
|
||||
def _validate_subject_date_chain(
|
||||
*,
|
||||
screening_date: date | None,
|
||||
@@ -80,6 +129,7 @@ async def create_subject(db: AsyncSession, study_id: uuid.UUID, subject_in: Subj
|
||||
await db.refresh(subject)
|
||||
|
||||
await sync_visits_from_baseline(db, subject)
|
||||
await sync_subject_status(db, subject)
|
||||
return subject
|
||||
|
||||
|
||||
@@ -121,12 +171,30 @@ async def sync_visits_from_baseline(db: AsyncSession, subject: Subject) -> None:
|
||||
)
|
||||
|
||||
|
||||
async def sync_subject_status(db: AsyncSession, subject: Subject) -> Subject:
|
||||
visits_result = await db.execute(select(Visit).where(Visit.subject_id == subject.id))
|
||||
visits = list(visits_result.scalars().all())
|
||||
study_result = await db.execute(select(Study.visit_schedule).where(Study.id == subject.study_id))
|
||||
visit_schedule = study_result.scalar_one_or_none() or []
|
||||
next_status = derive_subject_status(subject, visits, visit_schedule)
|
||||
if subject.status != next_status:
|
||||
await db.execute(
|
||||
sa_update(Subject)
|
||||
.where(Subject.id == subject.id)
|
||||
.values(status=next_status)
|
||||
)
|
||||
await db.commit()
|
||||
await db.refresh(subject)
|
||||
return subject
|
||||
|
||||
|
||||
async def generate_default_visits(db: AsyncSession, subject: Subject) -> None:
|
||||
await sync_visits_from_baseline(db, subject)
|
||||
|
||||
|
||||
async def update_subject(db: AsyncSession, subject: Subject, subject_in: SubjectUpdate) -> Subject:
|
||||
update_data = subject_in.model_dump(exclude_unset=True)
|
||||
update_data.pop("status", None)
|
||||
next_screening_date = subject.screening_date
|
||||
next_consent_date = update_data.get("consent_date", subject.consent_date)
|
||||
next_enrollment_date = update_data.get("enrollment_date", subject.enrollment_date)
|
||||
@@ -146,7 +214,7 @@ async def update_subject(db: AsyncSession, subject: Subject, subject_in: Subject
|
||||
)
|
||||
await db.commit()
|
||||
await db.refresh(subject)
|
||||
return subject
|
||||
return await sync_subject_status(db, subject)
|
||||
|
||||
|
||||
def should_generate_visits_after_subject_update(
|
||||
|
||||
Reference in New Issue
Block a user