项目概览、参与者管理初步优化(未接入真实数据)

This commit is contained in:
Cheng Zhou
2026-01-13 10:10:28 +08:00
parent 1338fa7e2b
commit b3cd8e03f2
37 changed files with 1785 additions and 118 deletions
+1 -1
View File
@@ -28,7 +28,7 @@ async def _validate_site_subject(db: AsyncSession, study_id: uuid.UUID, site_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("受试者不属于当前项目")
raise ValueError("参与者不属于当前项目")
async def create_ae(
+4
View File
@@ -16,6 +16,10 @@ async def create(db: AsyncSession, study_in: StudyCreate, *, created_by: uuid.UU
protocol_no=study_in.protocol_no,
phase=study_in.phase,
status=study_in.status,
visit_interval_days=study_in.visit_interval_days,
visit_total=study_in.visit_total,
visit_window_start_offset=study_in.visit_window_start_offset,
visit_window_end_offset=study_in.visit_window_end_offset,
created_by=created_by,
)
db.add(study)
+33 -12
View File
@@ -6,6 +6,8 @@ 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
@@ -26,6 +28,7 @@ async def create_subject(db: AsyncSession, study_id: uuid.UUID, subject_in: Subj
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,
@@ -41,7 +44,6 @@ async def create_subject(db: AsyncSession, study_id: uuid.UUID, subject_in: Subj
visit_in=None,
subject=subject,
visit_code="V0",
visit_name="Screening",
planned_date=subject.screening_date,
)
return subject
@@ -74,25 +76,44 @@ 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
follow1 = baseline_date + timedelta(days=30)
follow2 = baseline_date + timedelta(days=60)
visits_data = [
("V1", "Baseline", baseline_date),
("FU1", "Follow-up 1", follow1),
("FU2", "Follow-up 2", follow2),
]
for code, name, plan_date in visits_data:
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=code,
visit_name=name,
planned_date=plan_date,
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)
+1 -1
View File
@@ -13,7 +13,7 @@ async def _ensure_subject(db: AsyncSession, study_id: uuid.UUID, subject_id: uui
result = await db.execute(select(Subject).where(Subject.id == subject_id))
subject = result.scalar_one_or_none()
if not subject or subject.study_id != study_id:
raise ValueError("受试者不属于当前项目")
raise ValueError("参与者不属于当前项目")
async def create_history(
+52 -4
View File
@@ -1,4 +1,5 @@
import uuid
from datetime import date, timedelta
from typing import Sequence
from sqlalchemy import select, update as sa_update
@@ -16,19 +17,19 @@ async def create_visit(
visit_in: VisitCreate | None,
subject: Subject,
visit_code: str,
visit_name: str,
planned_date,
window_start: date | None = None,
window_end: date | None = None,
) -> Visit:
visit = Visit(
study_id=study_id,
subject_id=subject.id,
visit_code=visit_code,
visit_name=visit_name,
planned_date=planned_date,
actual_date=None,
status="PLANNED",
window_start=visit_in.window_start if visit_in else None,
window_end=visit_in.window_end if visit_in else None,
window_start=window_start if window_start is not None else (visit_in.window_start if visit_in else None),
window_end=window_end if window_end is not None else (visit_in.window_end if visit_in else None),
notes=None,
)
db.add(visit)
@@ -49,6 +50,12 @@ async def get_visit(db: AsyncSession, visit_id: uuid.UUID) -> Visit | None:
async def update_visit(db: AsyncSession, visit: Visit, visit_in: VisitUpdate) -> Visit:
update_data = visit_in.model_dump(exclude_unset=True)
if "actual_date" in update_data and update_data["actual_date"] is not None and "status" not in update_data:
actual_date = update_data["actual_date"]
if (visit.window_start and actual_date < visit.window_start) or (visit.window_end and actual_date > visit.window_end):
update_data["status"] = "OVERDUE"
else:
update_data["status"] = "DONE"
if update_data:
await db.execute(
sa_update(Visit)
@@ -63,3 +70,44 @@ async def update_visit(db: AsyncSession, visit: Visit, visit_in: VisitUpdate) ->
async def delete_visit(db: AsyncSession, visit: Visit) -> None:
await db.delete(visit)
await db.commit()
async def create_followup_visits(
db: AsyncSession,
*,
study_id: uuid.UUID,
subject: Subject,
base_date: date,
visit_total: int | None,
visit_interval_days: int | None,
window_start_offset: int | None,
window_end_offset: int | None,
) -> Sequence[Visit]:
if not visit_total or not visit_interval_days or visit_total < 2:
return []
result = await db.execute(select(Visit.visit_code).where(Visit.subject_id == subject.id))
existing_codes = {row[0] for row in result.all()}
created: list[Visit] = []
for index in range(2, visit_total + 1):
code = f"V{index}"
if code in existing_codes:
continue
planned_date = base_date + timedelta(days=visit_interval_days * (index - 1))
window_start = (
planned_date + timedelta(days=window_start_offset) if window_start_offset is not None else None
)
window_end = planned_date + timedelta(days=window_end_offset) if window_end_offset is not None else None
created.append(
await create_visit(
db,
study_id=study_id,
visit_in=None,
subject=subject,
visit_code=code,
planned_date=planned_date,
window_start=window_start,
window_end=window_end,
)
)
return created