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
+164
View File
@@ -0,0 +1,164 @@
import uuid
from datetime import date
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
from app.crud import ae as ae_crud
from app.crud import audit as audit_crud
from app.crud import member as member_crud
from app.crud import study as study_crud
from app.schemas.ae import AECreate, AERead, AEUpdate
router = APIRouter()
ALLOWED_CREATE_ROLES = {"PM", "CRA", "PV"}
ALLOWED_UPDATE_ROLES = {"PM", "PV"}
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
async def _get_member_role(db: AsyncSession, study_id: uuid.UUID, user_id: uuid.UUID) -> str | None:
member = await member_crud.get_member(db, study_id, user_id)
return member.role_in_study if member else None
def _is_overdue(ae: AERead) -> bool:
return bool(ae.report_due_date and date.today() > ae.report_due_date and ae.status != "CLOSED")
@router.post(
"/",
response_model=AERead,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(require_study_member())],
)
async def create_ae(
study_id: uuid.UUID,
ae_in: AECreate,
db: AsyncSession = Depends(get_db_session),
current_user=Depends(get_current_user),
) -> AERead:
await _ensure_study_exists(db, study_id)
member_role = await _get_member_role(db, study_id, current_user.id)
if current_user.role != "ADMIN" and member_role not in ALLOWED_CREATE_ROLES:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions")
try:
ae = await ae_crud.create_ae(db, study_id, ae_in, created_by=current_user.id)
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="ae",
entity_id=ae.id,
action="CREATE_AE",
detail=f"AE {ae.id} created",
operator_id=current_user.id,
operator_role=current_user.role,
)
data = AERead.model_validate(ae)
data.is_overdue = _is_overdue(data)
return data
@router.get(
"/",
response_model=list[AERead],
dependencies=[Depends(require_study_member())],
)
async def list_ae(
study_id: uuid.UUID,
status_filter: str | None = None,
seriousness: str | None = None,
site_id: uuid.UUID | None = None,
subject_id: uuid.UUID | None = None,
overdue: bool | None = None,
db: AsyncSession = Depends(get_db_session),
) -> list[AERead]:
await _ensure_study_exists(db, study_id)
aes = await ae_crud.list_ae(db, study_id, status=status_filter, seriousness=seriousness, site_id=site_id, subject_id=subject_id, overdue=overdue)
result: list[AERead] = []
for item in aes:
obj = AERead.model_validate(item)
obj.is_overdue = _is_overdue(obj)
result.append(obj)
return result
@router.get(
"/{ae_id}",
response_model=AERead,
dependencies=[Depends(require_study_member())],
)
async def get_ae(
study_id: uuid.UUID,
ae_id: uuid.UUID,
db: AsyncSession = Depends(get_db_session),
) -> AERead:
await _ensure_study_exists(db, study_id)
ae = await ae_crud.get_ae(db, ae_id)
if not ae or ae.study_id != study_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="AE not found")
data = AERead.model_validate(ae)
data.is_overdue = _is_overdue(data)
return data
@router.patch(
"/{ae_id}",
response_model=AERead,
dependencies=[Depends(require_study_member())],
)
async def update_ae(
study_id: uuid.UUID,
ae_id: uuid.UUID,
ae_in: AEUpdate,
db: AsyncSession = Depends(get_db_session),
current_user=Depends(get_current_user),
) -> AERead:
await _ensure_study_exists(db, study_id)
ae = await ae_crud.get_ae(db, ae_id)
if not ae or ae.study_id != study_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="AE not found")
member_role = await _get_member_role(db, study_id, current_user.id)
if current_user.role == "ADMIN":
pass
elif member_role in ALLOWED_UPDATE_ROLES:
pass
elif member_role == "CRA":
if ae.created_by != current_user.id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Only creator can update AE")
if ae_in.status == "CLOSED":
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="CRA cannot close AE")
else:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions")
old_status = ae.status
updated = await ae_crud.update_ae(db, ae, ae_in)
detail = None
action = "UPDATE_AE"
if ae_in.status and ae_in.status != old_status:
detail = f"AE {ae_id} status {old_status} -> {ae_in.status}"
action = "AE_STATUS_CHANGE"
await audit_crud.log_action(
db,
study_id=study_id,
entity_type="ae",
entity_id=ae_id,
action=action,
detail=detail or "AE updated",
operator_id=current_user.id,
operator_role=current_user.role,
)
data = AERead.model_validate(updated)
data.is_overdue = _is_overdue(data)
return data
+156
View File
@@ -0,0 +1,156 @@
import uuid
from datetime import date
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
from app.crud import audit as audit_crud
from app.crud import issue as issue_crud
from app.crud import member as member_crud
from app.crud import study as study_crud
from app.schemas.issue import IssueCreate, IssueRead, IssueUpdate
router = APIRouter()
ALLOWED_EDIT_ROLES = {"PM", "PV"}
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
async def _get_member_role(db: AsyncSession, study_id: uuid.UUID, user_id: uuid.UUID) -> str | None:
member = await member_crud.get_member(db, study_id, user_id)
return member.role_in_study if member else None
def _is_overdue(issue: IssueRead) -> bool:
return bool(issue.due_date and date.today() > issue.due_date and issue.status != "CLOSED")
@router.post(
"/",
response_model=IssueRead,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(require_study_member())],
)
async def create_issue(
study_id: uuid.UUID,
issue_in: IssueCreate,
db: AsyncSession = Depends(get_db_session),
current_user=Depends(get_current_user),
) -> IssueRead:
await _ensure_study_exists(db, study_id)
member_role = await _get_member_role(db, study_id, current_user.id)
if current_user.role != "ADMIN" and member_role not in ALLOWED_EDIT_ROLES:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions")
try:
issue = await issue_crud.create_issue(db, study_id, issue_in, created_by=current_user.id)
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="issue",
entity_id=issue.id,
action="CREATE_ISSUE",
detail=f"Issue {issue.id} created",
operator_id=current_user.id,
operator_role=current_user.role,
)
data = IssueRead.model_validate(issue)
data.is_overdue = _is_overdue(data)
return data
@router.get(
"/",
response_model=list[IssueRead],
dependencies=[Depends(require_study_member())],
)
async def list_issues(
study_id: uuid.UUID,
status_filter: str | None = None,
level: str | None = None,
category: str | None = None,
overdue: bool | None = None,
db: AsyncSession = Depends(get_db_session),
) -> list[IssueRead]:
await _ensure_study_exists(db, study_id)
issues = await issue_crud.list_issues(db, study_id, status=status_filter, level=level, category=category, overdue=overdue)
result: list[IssueRead] = []
for item in issues:
obj = IssueRead.model_validate(item)
obj.is_overdue = _is_overdue(obj)
result.append(obj)
return result
@router.get(
"/{issue_id}",
response_model=IssueRead,
dependencies=[Depends(require_study_member())],
)
async def get_issue(
study_id: uuid.UUID,
issue_id: uuid.UUID,
db: AsyncSession = Depends(get_db_session),
) -> IssueRead:
await _ensure_study_exists(db, study_id)
issue = await issue_crud.get_issue(db, issue_id)
if not issue or issue.study_id != study_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Issue not found")
data = IssueRead.model_validate(issue)
data.is_overdue = _is_overdue(data)
return data
@router.patch(
"/{issue_id}",
response_model=IssueRead,
dependencies=[Depends(require_study_member())],
)
async def update_issue(
study_id: uuid.UUID,
issue_id: uuid.UUID,
issue_in: IssueUpdate,
db: AsyncSession = Depends(get_db_session),
current_user=Depends(get_current_user),
) -> IssueRead:
await _ensure_study_exists(db, study_id)
issue = await issue_crud.get_issue(db, issue_id)
if not issue or issue.study_id != study_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Issue not found")
member_role = await _get_member_role(db, study_id, current_user.id)
if current_user.role != "ADMIN" and member_role not in ALLOWED_EDIT_ROLES:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions")
old_status = issue.status
old_level = issue.level
updated = await issue_crud.update_issue(db, issue, issue_in)
detail = None
action = "UPDATE_ISSUE"
if issue_in.status and issue_in.status != old_status:
detail = f"Issue {issue_id} status {old_status} -> {issue_in.status}"
action = "ISSUE_STATUS_CHANGE"
elif issue_in.level and issue_in.level != old_level:
detail = f"Issue {issue_id} level {old_level} -> {issue_in.level}"
await audit_crud.log_action(
db,
study_id=study_id,
entity_type="issue",
entity_id=issue_id,
action=action,
detail=detail or "Issue updated",
operator_id=current_user.id,
operator_role=current_user.role,
)
data = IssueRead.model_validate(updated)
data.is_overdue = _is_overdue(data)
return data
+3 -1
View File
@@ -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, subjects, visits from app.api.v1 import auth, users, studies, sites, members, comments, attachments, audit_logs, milestones, tasks, dashboard, subjects, visits, aes, issues
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"])
@@ -16,3 +16,5 @@ api_router.include_router(tasks.router, prefix="/studies/{study_id}/tasks", tags
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(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"]) api_router.include_router(visits.router, prefix="/studies/{study_id}/subjects/{subject_id}/visits", tags=["visits"])
api_router.include_router(aes.router, prefix="/studies/{study_id}/aes", tags=["aes"])
api_router.include_router(issues.router, prefix="/studies/{study_id}/issues", tags=["issues"])
+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
+2
View File
@@ -12,3 +12,5 @@ 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.subject import Subject # noqa: F401
from app.models.visit import Visit # noqa: F401 from app.models.visit import Visit # noqa: F401
from app.models.ae import AdverseEvent # noqa: F401
from app.models.issue import Issue # noqa: F401
+35
View File
@@ -0,0 +1,35 @@
import uuid
from datetime import datetime, date
from sqlalchemy import Boolean, 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 AdverseEvent(Base):
__tablename__ = "adverse_events"
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 | None] = mapped_column(UUID(as_uuid=True), ForeignKey("sites.id"), index=True, nullable=True)
subject_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("subjects.id"), index=True, nullable=True)
visit_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), nullable=True)
term: Mapped[str] = mapped_column(String(255), nullable=False)
onset_date: Mapped[date | None] = mapped_column(Date, nullable=True)
resolution_date: Mapped[date | None] = mapped_column(Date, nullable=True)
seriousness: Mapped[str] = mapped_column(String(50), nullable=False)
severity: Mapped[str] = mapped_column(String(50), nullable=False)
causality: Mapped[str | None] = mapped_column(String(50), nullable=True)
action_taken: Mapped[str | None] = mapped_column(Text, nullable=True)
outcome: Mapped[str | None] = mapped_column(String(50), nullable=True)
reported_to_sponsor: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
report_due_date: Mapped[date | None] = mapped_column(Date, nullable=True)
status: Mapped[str] = mapped_column(String(20), nullable=False, default="NEW")
description: Mapped[str | None] = mapped_column(Text, nullable=True)
created_by: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
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()
)
+31
View File
@@ -0,0 +1,31 @@
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 Issue(Base):
__tablename__ = "issues"
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 | None] = mapped_column(UUID(as_uuid=True), ForeignKey("sites.id"), index=True, nullable=True)
subject_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("subjects.id"), index=True, nullable=True)
title: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
category: Mapped[str] = mapped_column(String(50), nullable=False)
level: Mapped[str] = mapped_column(String(20), nullable=False)
owner_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
due_date: Mapped[date | None] = mapped_column(Date, nullable=True)
status: Mapped[str] = mapped_column(String(20), nullable=False, default="OPEN")
capa: Mapped[str | None] = mapped_column(Text, nullable=True)
closed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
created_by: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
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()
)
+55
View File
@@ -0,0 +1,55 @@
import uuid
from datetime import date, datetime
from typing import Optional
from pydantic import BaseModel, ConfigDict
class AECreate(BaseModel):
site_id: Optional[uuid.UUID] = None
subject_id: Optional[uuid.UUID] = None
visit_id: Optional[uuid.UUID] = None
term: str
onset_date: Optional[date] = None
seriousness: str
severity: str
causality: Optional[str] = None
description: Optional[str] = None
class AEUpdate(BaseModel):
resolution_date: Optional[date] = None
seriousness: Optional[str] = None
severity: Optional[str] = None
causality: Optional[str] = None
action_taken: Optional[str] = None
outcome: Optional[str] = None
reported_to_sponsor: Optional[bool] = None
status: Optional[str] = None
description: Optional[str] = None
class AERead(BaseModel):
id: uuid.UUID
study_id: uuid.UUID
site_id: Optional[uuid.UUID]
subject_id: Optional[uuid.UUID]
visit_id: Optional[uuid.UUID]
term: str
onset_date: Optional[date]
resolution_date: Optional[date]
seriousness: str
severity: str
causality: Optional[str]
action_taken: Optional[str]
outcome: Optional[str]
reported_to_sponsor: bool
report_due_date: Optional[date]
status: str
description: Optional[str]
created_by: uuid.UUID
created_at: datetime
updated_at: datetime
is_overdue: bool | None = None
model_config = ConfigDict(from_attributes=True)
+49
View File
@@ -0,0 +1,49 @@
import uuid
from datetime import date, datetime
from typing import Optional
from pydantic import BaseModel, ConfigDict
class IssueCreate(BaseModel):
title: str
category: str
level: str
due_date: Optional[date] = None
owner_id: Optional[uuid.UUID] = None
site_id: Optional[uuid.UUID] = None
subject_id: Optional[uuid.UUID] = None
description: Optional[str] = None
class IssueUpdate(BaseModel):
title: Optional[str] = None
category: Optional[str] = None
level: Optional[str] = None
due_date: Optional[date] = None
owner_id: Optional[uuid.UUID] = None
status: Optional[str] = None
capa: Optional[str] = None
description: Optional[str] = None
class IssueRead(BaseModel):
id: uuid.UUID
study_id: uuid.UUID
site_id: Optional[uuid.UUID]
subject_id: Optional[uuid.UUID]
title: str
description: Optional[str]
category: str
level: str
owner_id: Optional[uuid.UUID]
due_date: Optional[date]
status: str
capa: Optional[str]
closed_at: Optional[datetime]
created_by: uuid.UUID
created_at: datetime
updated_at: datetime
is_overdue: bool | None = None
model_config = ConfigDict(from_attributes=True)
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
View File
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.