Step 6:受试者(Subject)+ 访视(Visit)+ 随访进度
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from app.api.v1 import auth, users, studies, sites, members, comments, attachments, audit_logs, milestones, tasks, dashboard
|
from app.api.v1 import auth, users, studies, sites, members, comments, attachments, audit_logs, milestones, tasks, dashboard, subjects, visits
|
||||||
|
|
||||||
api_router = APIRouter()
|
api_router = APIRouter()
|
||||||
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
|
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
|
||||||
@@ -14,3 +14,5 @@ api_router.include_router(audit_logs.router, prefix="/studies/{study_id}/audit-l
|
|||||||
api_router.include_router(milestones.router, prefix="/studies/{study_id}/milestones", tags=["milestones"])
|
api_router.include_router(milestones.router, prefix="/studies/{study_id}/milestones", tags=["milestones"])
|
||||||
api_router.include_router(tasks.router, prefix="/studies/{study_id}/tasks", tags=["tasks"])
|
api_router.include_router(tasks.router, prefix="/studies/{study_id}/tasks", tags=["tasks"])
|
||||||
api_router.include_router(dashboard.router, prefix="/studies/{study_id}/dashboard", tags=["dashboard"])
|
api_router.include_router(dashboard.router, prefix="/studies/{study_id}/dashboard", tags=["dashboard"])
|
||||||
|
api_router.include_router(subjects.router, prefix="/studies/{study_id}/subjects", tags=["subjects"])
|
||||||
|
api_router.include_router(visits.router, prefix="/studies/{study_id}/subjects/{subject_id}/visits", tags=["visits"])
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import uuid
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.core.deps import get_current_user, get_db_session, require_study_member, require_study_roles
|
||||||
|
from app.crud import audit as audit_crud
|
||||||
|
from app.crud import subject as subject_crud
|
||||||
|
from app.crud import study as study_crud
|
||||||
|
from app.schemas.subject import SubjectCreate, SubjectRead, SubjectUpdate
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
async def _ensure_study_exists(db: AsyncSession, study_id: uuid.UUID):
|
||||||
|
study = await study_crud.get(db, study_id)
|
||||||
|
if not study:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Study not found")
|
||||||
|
return study
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/",
|
||||||
|
response_model=SubjectRead,
|
||||||
|
status_code=status.HTTP_201_CREATED,
|
||||||
|
dependencies=[Depends(require_study_roles(["PM", "CRA"]))],
|
||||||
|
)
|
||||||
|
async def create_subject(
|
||||||
|
study_id: uuid.UUID,
|
||||||
|
subject_in: SubjectCreate,
|
||||||
|
db: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
) -> SubjectRead:
|
||||||
|
await _ensure_study_exists(db, study_id)
|
||||||
|
try:
|
||||||
|
subject = await subject_crud.create_subject(db, study_id, subject_in)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
||||||
|
await audit_crud.log_action(
|
||||||
|
db,
|
||||||
|
study_id=study_id,
|
||||||
|
entity_type="subject",
|
||||||
|
entity_id=subject.id,
|
||||||
|
action="CREATE_SUBJECT",
|
||||||
|
detail=f"Subject {subject.subject_no} created",
|
||||||
|
operator_id=current_user.id,
|
||||||
|
operator_role=current_user.role,
|
||||||
|
)
|
||||||
|
return subject
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/",
|
||||||
|
response_model=list[SubjectRead],
|
||||||
|
dependencies=[Depends(require_study_member())],
|
||||||
|
)
|
||||||
|
async def list_subjects(
|
||||||
|
study_id: uuid.UUID,
|
||||||
|
site_id: uuid.UUID | None = None,
|
||||||
|
status_filter: str | None = None,
|
||||||
|
db: AsyncSession = Depends(get_db_session),
|
||||||
|
) -> list[SubjectRead]:
|
||||||
|
await _ensure_study_exists(db, study_id)
|
||||||
|
subjects = await subject_crud.list_subjects(db, study_id, site_id=site_id, status=status_filter)
|
||||||
|
return list(subjects)
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch(
|
||||||
|
"/{subject_id}",
|
||||||
|
response_model=SubjectRead,
|
||||||
|
dependencies=[Depends(require_study_roles(["PM", "CRA"]))],
|
||||||
|
)
|
||||||
|
async def update_subject(
|
||||||
|
study_id: uuid.UUID,
|
||||||
|
subject_id: uuid.UUID,
|
||||||
|
subject_in: SubjectUpdate,
|
||||||
|
db: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
) -> SubjectRead:
|
||||||
|
await _ensure_study_exists(db, study_id)
|
||||||
|
subject = await subject_crud.get_subject(db, subject_id)
|
||||||
|
if not subject or subject.study_id != study_id:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Subject not found")
|
||||||
|
old_status = subject.status
|
||||||
|
updated = await subject_crud.update_subject(db, subject, subject_in)
|
||||||
|
# auto-generate visits when enrolled
|
||||||
|
if subject_in.status and subject_in.status == "ENROLLED" and (subject_in.enrollment_date or updated.enrollment_date):
|
||||||
|
await subject_crud.generate_default_visits(db, updated)
|
||||||
|
detail = None
|
||||||
|
if subject_in.status and subject_in.status != old_status:
|
||||||
|
detail = f"subject {updated.subject_no} status {old_status} -> {subject_in.status}"
|
||||||
|
await audit_crud.log_action(
|
||||||
|
db,
|
||||||
|
study_id=study_id,
|
||||||
|
entity_type="subject",
|
||||||
|
entity_id=subject_id,
|
||||||
|
action="SUBJECT_STATUS_CHANGE" if detail else "UPDATE_SUBJECT",
|
||||||
|
detail=detail or "Subject updated",
|
||||||
|
operator_id=current_user.id,
|
||||||
|
operator_role=current_user.role,
|
||||||
|
)
|
||||||
|
return updated
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import uuid
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.core.deps import get_current_user, get_db_session, require_study_member, require_study_roles
|
||||||
|
from app.crud import audit as audit_crud
|
||||||
|
from app.crud import subject as subject_crud
|
||||||
|
from app.crud import visit as visit_crud
|
||||||
|
from app.schemas.visit import VisitRead, VisitUpdate
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
async def _ensure_subject(db: AsyncSession, study_id: uuid.UUID, subject_id: uuid.UUID):
|
||||||
|
subject = await subject_crud.get_subject(db, subject_id)
|
||||||
|
if not subject or subject.study_id != study_id:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Subject not found")
|
||||||
|
return subject
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/",
|
||||||
|
response_model=list[VisitRead],
|
||||||
|
dependencies=[Depends(require_study_member())],
|
||||||
|
)
|
||||||
|
async def list_visits(
|
||||||
|
study_id: uuid.UUID,
|
||||||
|
subject_id: uuid.UUID,
|
||||||
|
db: AsyncSession = Depends(get_db_session),
|
||||||
|
) -> list[VisitRead]:
|
||||||
|
await _ensure_subject(db, study_id, subject_id)
|
||||||
|
visits = await visit_crud.list_visits(db, subject_id)
|
||||||
|
return list(visits)
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch(
|
||||||
|
"/{visit_id}",
|
||||||
|
response_model=VisitRead,
|
||||||
|
dependencies=[Depends(require_study_roles(["PM", "CRA"]))],
|
||||||
|
)
|
||||||
|
async def update_visit(
|
||||||
|
study_id: uuid.UUID,
|
||||||
|
subject_id: uuid.UUID,
|
||||||
|
visit_id: uuid.UUID,
|
||||||
|
visit_in: VisitUpdate,
|
||||||
|
db: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
) -> VisitRead:
|
||||||
|
await _ensure_subject(db, study_id, subject_id)
|
||||||
|
visit = await visit_crud.get_visit(db, visit_id)
|
||||||
|
if not visit or visit.subject_id != subject_id or visit.study_id != study_id:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Visit not found")
|
||||||
|
if visit_in.status == "DONE" and not visit_in.actual_date:
|
||||||
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="actual_date required when DONE")
|
||||||
|
updated = await visit_crud.update_visit(db, visit, visit_in)
|
||||||
|
detail = None
|
||||||
|
if visit_in.status:
|
||||||
|
detail = f"visit {visit.visit_code} {visit.status} -> {visit_in.status}"
|
||||||
|
await audit_crud.log_action(
|
||||||
|
db,
|
||||||
|
study_id=study_id,
|
||||||
|
entity_type="visit",
|
||||||
|
entity_id=visit_id,
|
||||||
|
action="VISIT_STATUS_CHANGE" if detail else "UPDATE_VISIT",
|
||||||
|
detail=detail or "Visit updated",
|
||||||
|
operator_id=current_user.id,
|
||||||
|
operator_role=current_user.role,
|
||||||
|
)
|
||||||
|
return updated
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
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.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("Site not found in study")
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
||||||
|
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",
|
||||||
|
visit_name="Screening",
|
||||||
|
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,
|
||||||
|
) -> 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)
|
||||||
|
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
|
||||||
|
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:
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import uuid
|
||||||
|
from typing import Sequence
|
||||||
|
|
||||||
|
from sqlalchemy import select, update as sa_update
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.models.subject import Subject
|
||||||
|
from app.models.visit import Visit
|
||||||
|
from app.schemas.visit import VisitCreate, VisitUpdate
|
||||||
|
|
||||||
|
|
||||||
|
async def create_visit(
|
||||||
|
db: AsyncSession,
|
||||||
|
*,
|
||||||
|
study_id: uuid.UUID,
|
||||||
|
visit_in: VisitCreate | None,
|
||||||
|
subject: Subject,
|
||||||
|
visit_code: str,
|
||||||
|
visit_name: str,
|
||||||
|
planned_date,
|
||||||
|
) -> 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,
|
||||||
|
notes=None,
|
||||||
|
)
|
||||||
|
db.add(visit)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(visit)
|
||||||
|
return visit
|
||||||
|
|
||||||
|
|
||||||
|
async def list_visits(db: AsyncSession, subject_id: uuid.UUID) -> Sequence[Visit]:
|
||||||
|
result = await db.execute(select(Visit).where(Visit.subject_id == subject_id).order_by(Visit.planned_date))
|
||||||
|
return result.scalars().all()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_visit(db: AsyncSession, visit_id: uuid.UUID) -> Visit | None:
|
||||||
|
result = await db.execute(select(Visit).where(Visit.id == visit_id))
|
||||||
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
|
||||||
|
async def update_visit(db: AsyncSession, visit: Visit, visit_in: VisitUpdate) -> Visit:
|
||||||
|
update_data = visit_in.model_dump(exclude_unset=True)
|
||||||
|
if update_data:
|
||||||
|
await db.execute(
|
||||||
|
sa_update(Visit)
|
||||||
|
.where(Visit.id == visit.id)
|
||||||
|
.values(**update_data)
|
||||||
|
)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(visit)
|
||||||
|
return visit
|
||||||
@@ -10,3 +10,5 @@ from app.models.attachment import Attachment # noqa: F401
|
|||||||
from app.models.audit_log import AuditLog # noqa: F401
|
from app.models.audit_log import AuditLog # noqa: F401
|
||||||
from app.models.milestone import Milestone # noqa: F401
|
from app.models.milestone import Milestone # noqa: F401
|
||||||
from app.models.task import Task # noqa: F401
|
from app.models.task import Task # noqa: F401
|
||||||
|
from app.models.subject import Subject # noqa: F401
|
||||||
|
from app.models.visit import Visit # noqa: F401
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import uuid
|
||||||
|
from datetime import datetime, date
|
||||||
|
|
||||||
|
from sqlalchemy import Date, DateTime, ForeignKey, String, Text, UniqueConstraint, func
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from app.db.base_class import Base
|
||||||
|
|
||||||
|
|
||||||
|
class Subject(Base):
|
||||||
|
__tablename__ = "subjects"
|
||||||
|
__table_args__ = (UniqueConstraint("study_id", "subject_no", name="uq_subject_study_no"),)
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||||
|
study_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("studies.id"), index=True, nullable=False)
|
||||||
|
site_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("sites.id"), index=True, nullable=False)
|
||||||
|
subject_no: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||||
|
status: Mapped[str] = mapped_column(String(20), nullable=False, default="SCREENING")
|
||||||
|
screening_date: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||||||
|
enrollment_date: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||||||
|
completion_date: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||||||
|
drop_reason: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
|
||||||
|
updated_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
|
||||||
|
)
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import uuid
|
||||||
|
from datetime import datetime, date
|
||||||
|
|
||||||
|
from sqlalchemy import Date, DateTime, ForeignKey, String, Text, func
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from app.db.base_class import Base
|
||||||
|
|
||||||
|
|
||||||
|
class Visit(Base):
|
||||||
|
__tablename__ = "visits"
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||||
|
study_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("studies.id"), nullable=False)
|
||||||
|
subject_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("subjects.id"), index=True, nullable=False)
|
||||||
|
visit_code: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||||
|
visit_name: Mapped[str] = mapped_column(String(200), nullable=False)
|
||||||
|
planned_date: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||||||
|
actual_date: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||||||
|
status: Mapped[str] = mapped_column(String(20), nullable=False, default="PLANNED")
|
||||||
|
window_start: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||||||
|
window_end: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||||||
|
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
|
||||||
|
updated_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
|
||||||
|
)
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import uuid
|
||||||
|
from datetime import date, datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
|
||||||
|
|
||||||
|
class SubjectCreate(BaseModel):
|
||||||
|
site_id: uuid.UUID
|
||||||
|
subject_no: str
|
||||||
|
screening_date: Optional[date] = None
|
||||||
|
|
||||||
|
|
||||||
|
class SubjectUpdate(BaseModel):
|
||||||
|
status: Optional[str] = None
|
||||||
|
enrollment_date: Optional[date] = None
|
||||||
|
completion_date: Optional[date] = None
|
||||||
|
drop_reason: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class SubjectRead(BaseModel):
|
||||||
|
id: uuid.UUID
|
||||||
|
study_id: uuid.UUID
|
||||||
|
site_id: uuid.UUID
|
||||||
|
subject_no: str
|
||||||
|
status: str
|
||||||
|
screening_date: Optional[date]
|
||||||
|
enrollment_date: Optional[date]
|
||||||
|
completion_date: Optional[date]
|
||||||
|
drop_reason: Optional[str]
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import uuid
|
||||||
|
from datetime import date, datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
|
||||||
|
|
||||||
|
class VisitCreate(BaseModel):
|
||||||
|
subject_id: uuid.UUID
|
||||||
|
visit_code: str
|
||||||
|
visit_name: str
|
||||||
|
planned_date: Optional[date] = None
|
||||||
|
window_start: Optional[date] = None
|
||||||
|
window_end: Optional[date] = None
|
||||||
|
|
||||||
|
|
||||||
|
class VisitUpdate(BaseModel):
|
||||||
|
actual_date: Optional[date] = None
|
||||||
|
status: Optional[str] = None
|
||||||
|
notes: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class VisitRead(BaseModel):
|
||||||
|
id: uuid.UUID
|
||||||
|
study_id: uuid.UUID
|
||||||
|
subject_id: uuid.UUID
|
||||||
|
visit_code: str
|
||||||
|
visit_name: str
|
||||||
|
planned_date: Optional[date]
|
||||||
|
actual_date: Optional[date]
|
||||||
|
status: str
|
||||||
|
window_start: Optional[date]
|
||||||
|
window_end: Optional[date]
|
||||||
|
notes: Optional[str]
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user