Step 3:Study / Site / StudyMember + 项目权限
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_db_session, require_study_member, require_study_roles
|
||||
from app.crud import member as member_crud
|
||||
from app.crud import study as study_crud
|
||||
from app.schemas.member import StudyMemberCreate, StudyMemberRead, StudyMemberUpdate
|
||||
|
||||
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=StudyMemberRead,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
dependencies=[Depends(require_study_roles(["PM"]))],
|
||||
)
|
||||
async def add_member(
|
||||
study_id: uuid.UUID,
|
||||
member_in: StudyMemberCreate,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> StudyMemberRead:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
existing = await member_crud.get_member(db, study_id, member_in.user_id)
|
||||
if existing:
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Member already exists")
|
||||
member = await member_crud.add_member(db, study_id, member_in)
|
||||
return member
|
||||
|
||||
|
||||
@router.get(
|
||||
"/",
|
||||
response_model=list[StudyMemberRead],
|
||||
dependencies=[Depends(require_study_member())],
|
||||
)
|
||||
async def list_members(
|
||||
study_id: uuid.UUID,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> list[StudyMemberRead]:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
members = await member_crud.list_members(db, study_id, skip=skip, limit=limit)
|
||||
return list(members)
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/{member_id}",
|
||||
response_model=StudyMemberRead,
|
||||
dependencies=[Depends(require_study_roles(["PM"]))],
|
||||
)
|
||||
async def update_member(
|
||||
study_id: uuid.UUID,
|
||||
member_id: uuid.UUID,
|
||||
member_in: StudyMemberUpdate,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> StudyMemberRead:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
member = await member_crud.get_member_by_id(db, member_id)
|
||||
if not member or member.study_id != study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Member not found")
|
||||
updated = await member_crud.update_member(db, member, member_in)
|
||||
return updated
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/{member_id}",
|
||||
response_model=StudyMemberRead,
|
||||
dependencies=[Depends(require_study_roles(["PM"]))],
|
||||
)
|
||||
async def remove_member(
|
||||
study_id: uuid.UUID,
|
||||
member_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> StudyMemberRead:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
member = await member_crud.get_member_by_id(db, member_id)
|
||||
if not member or member.study_id != study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Member not found")
|
||||
removed = await member_crud.remove_member(db, member)
|
||||
return removed
|
||||
@@ -1,7 +1,10 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.v1 import auth, users
|
||||
from app.api.v1 import auth, users, studies, sites, members
|
||||
|
||||
api_router = APIRouter()
|
||||
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
|
||||
api_router.include_router(users.router, prefix="/users", tags=["users"])
|
||||
api_router.include_router(studies.router, prefix="/studies", tags=["studies"])
|
||||
api_router.include_router(sites.router, prefix="/studies/{study_id}/sites", tags=["sites"])
|
||||
api_router.include_router(members.router, prefix="/studies/{study_id}/members", tags=["study-members"])
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_db_session, require_study_member, require_study_roles
|
||||
from app.crud import site as site_crud
|
||||
from app.crud import study as study_crud
|
||||
from app.schemas.site import SiteCreate, SiteRead, SiteUpdate
|
||||
|
||||
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=SiteRead,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
dependencies=[Depends(require_study_roles(["PM"]))],
|
||||
)
|
||||
async def create_site(
|
||||
study_id: uuid.UUID,
|
||||
site_in: SiteCreate,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> SiteRead:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
site = await site_crud.create_site(db, study_id, site_in)
|
||||
return site
|
||||
|
||||
|
||||
@router.get(
|
||||
"/",
|
||||
response_model=list[SiteRead],
|
||||
dependencies=[Depends(require_study_member())],
|
||||
)
|
||||
async def list_sites(
|
||||
study_id: uuid.UUID,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> list[SiteRead]:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
sites = await site_crud.list_by_study(db, study_id, skip=skip, limit=limit)
|
||||
return list(sites)
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/{site_id}",
|
||||
response_model=SiteRead,
|
||||
dependencies=[Depends(require_study_roles(["PM"]))],
|
||||
)
|
||||
async def update_site(
|
||||
study_id: uuid.UUID,
|
||||
site_id: uuid.UUID,
|
||||
site_in: SiteUpdate,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> SiteRead:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
site = await site_crud.get_site(db, site_id)
|
||||
if not site or site.study_id != study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Site not found")
|
||||
updated = await site_crud.update_site(db, site, site_in)
|
||||
return updated
|
||||
@@ -0,0 +1,65 @@
|
||||
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_roles, require_study_member, require_study_roles
|
||||
from app.crud import study as study_crud
|
||||
from app.schemas.study import StudyCreate, StudyRead, StudyUpdate
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/", response_model=StudyRead, status_code=status.HTTP_201_CREATED, dependencies=[Depends(require_roles(["ADMIN"]))])
|
||||
async def create_study(
|
||||
study_in: StudyCreate,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> StudyRead:
|
||||
existing = await study_crud.get_by_code(db, study_in.code)
|
||||
if existing:
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Study code already exists")
|
||||
study = await study_crud.create(db, study_in, created_by=current_user.id)
|
||||
return study
|
||||
|
||||
|
||||
@router.get("/", response_model=list[StudyRead])
|
||||
async def list_studies(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> list[StudyRead]:
|
||||
if current_user.role == "ADMIN":
|
||||
studies = await study_crud.list_studies(db, skip=skip, limit=limit)
|
||||
else:
|
||||
studies = await study_crud.list_studies_for_user(db, current_user.id, skip=skip, limit=limit)
|
||||
return list(studies)
|
||||
|
||||
|
||||
@router.get("/{study_id}", response_model=StudyRead, dependencies=[Depends(require_study_member())])
|
||||
async def get_study(
|
||||
study_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> StudyRead:
|
||||
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.patch(
|
||||
"/{study_id}",
|
||||
response_model=StudyRead,
|
||||
dependencies=[Depends(require_study_roles(["PM"]))],
|
||||
)
|
||||
async def update_study(
|
||||
study_id: uuid.UUID,
|
||||
study_in: StudyUpdate,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> StudyRead:
|
||||
study = await study_crud.get(db, study_id)
|
||||
if not study:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Study not found")
|
||||
updated = await study_crud.update(db, study, study_in)
|
||||
return updated
|
||||
@@ -7,6 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.security import decode_token, oauth2_scheme
|
||||
from app.crud import user as user_crud
|
||||
from app.crud import member as member_crud
|
||||
from app.db.session import SessionLocal
|
||||
from app.schemas.user import TokenPayload
|
||||
|
||||
@@ -52,3 +53,53 @@ def require_roles(roles: Iterable[str]) -> Callable:
|
||||
return current_user
|
||||
|
||||
return dependency
|
||||
|
||||
|
||||
async def get_study_member(
|
||||
study_id: uuid.UUID,
|
||||
current_user=Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
):
|
||||
if current_user.role == "ADMIN":
|
||||
return None
|
||||
return await member_crud.get_member(db, study_id, current_user.id)
|
||||
|
||||
|
||||
def require_study_member():
|
||||
async def dependency(
|
||||
study_id: uuid.UUID,
|
||||
current_user=Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
):
|
||||
if current_user.role == "ADMIN":
|
||||
return current_user
|
||||
membership = await member_crud.get_member(db, study_id, current_user.id)
|
||||
if not membership or not membership.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not a member of this study",
|
||||
)
|
||||
return current_user
|
||||
|
||||
return dependency
|
||||
|
||||
|
||||
def require_study_roles(roles: Iterable[str]):
|
||||
roles_set = set(roles)
|
||||
|
||||
async def dependency(
|
||||
study_id: uuid.UUID,
|
||||
current_user=Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
):
|
||||
if current_user.role == "ADMIN":
|
||||
return current_user
|
||||
membership = await member_crud.get_member(db, study_id, current_user.id)
|
||||
if not membership or not membership.is_active or membership.role_in_study not in roles_set:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Insufficient study permissions",
|
||||
)
|
||||
return current_user
|
||||
|
||||
return dependency
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import uuid
|
||||
from typing import Sequence
|
||||
|
||||
from sqlalchemy import select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.study_member import StudyMember
|
||||
from app.schemas.member import StudyMemberCreate, StudyMemberUpdate
|
||||
|
||||
|
||||
async def get_member(db: AsyncSession, study_id: uuid.UUID, user_id: uuid.UUID) -> StudyMember | None:
|
||||
result = await db.execute(
|
||||
select(StudyMember).where(StudyMember.study_id == study_id, StudyMember.user_id == user_id)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def get_member_by_id(db: AsyncSession, member_id: uuid.UUID) -> StudyMember | None:
|
||||
result = await db.execute(select(StudyMember).where(StudyMember.id == member_id))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def add_member(db: AsyncSession, study_id: uuid.UUID, member_in: StudyMemberCreate) -> StudyMember:
|
||||
member = StudyMember(
|
||||
study_id=study_id,
|
||||
user_id=member_in.user_id,
|
||||
role_in_study=member_in.role_in_study,
|
||||
is_active=member_in.is_active,
|
||||
)
|
||||
db.add(member)
|
||||
await db.commit()
|
||||
await db.refresh(member)
|
||||
return member
|
||||
|
||||
|
||||
async def list_members(db: AsyncSession, study_id: uuid.UUID, skip: int = 0, limit: int = 100) -> Sequence[StudyMember]:
|
||||
result = await db.execute(
|
||||
select(StudyMember).where(StudyMember.study_id == study_id).offset(skip).limit(limit)
|
||||
)
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
async def update_member(db: AsyncSession, member: StudyMember, member_in: StudyMemberUpdate) -> StudyMember:
|
||||
update_data = {k: v for k, v in member_in.model_dump(exclude_unset=True).items() if v is not None}
|
||||
if update_data:
|
||||
await db.execute(
|
||||
update(StudyMember)
|
||||
.where(StudyMember.id == member.id)
|
||||
.values(**update_data)
|
||||
)
|
||||
await db.commit()
|
||||
await db.refresh(member)
|
||||
return member
|
||||
|
||||
|
||||
async def remove_member(db: AsyncSession, member: StudyMember) -> StudyMember:
|
||||
await db.execute(
|
||||
update(StudyMember)
|
||||
.where(StudyMember.id == member.id)
|
||||
.values(is_active=False)
|
||||
)
|
||||
await db.commit()
|
||||
await db.refresh(member)
|
||||
return member
|
||||
@@ -0,0 +1,48 @@
|
||||
import uuid
|
||||
from typing import Sequence
|
||||
|
||||
from sqlalchemy import select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.site import Site
|
||||
from app.schemas.site import SiteCreate, SiteUpdate
|
||||
|
||||
|
||||
async def create_site(db: AsyncSession, study_id: uuid.UUID, site_in: SiteCreate) -> Site:
|
||||
site = Site(
|
||||
study_id=study_id,
|
||||
name=site_in.name,
|
||||
city=site_in.city,
|
||||
pi_name=site_in.pi_name,
|
||||
contact=site_in.contact,
|
||||
is_active=site_in.is_active,
|
||||
)
|
||||
db.add(site)
|
||||
await db.commit()
|
||||
await db.refresh(site)
|
||||
return site
|
||||
|
||||
|
||||
async def get_site(db: AsyncSession, site_id: uuid.UUID) -> Site | None:
|
||||
result = await db.execute(select(Site).where(Site.id == site_id))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def update_site(db: AsyncSession, site: Site, site_in: SiteUpdate) -> Site:
|
||||
update_data = site_in.model_dump(exclude_unset=True)
|
||||
if update_data:
|
||||
await db.execute(
|
||||
update(Site)
|
||||
.where(Site.id == site.id)
|
||||
.values(**update_data)
|
||||
)
|
||||
await db.commit()
|
||||
await db.refresh(site)
|
||||
return site
|
||||
|
||||
|
||||
async def list_by_study(db: AsyncSession, study_id: uuid.UUID, skip: int = 0, limit: int = 100) -> Sequence[Site]:
|
||||
result = await db.execute(
|
||||
select(Site).where(Site.study_id == study_id).offset(skip).limit(limit)
|
||||
)
|
||||
return result.scalars().all()
|
||||
@@ -0,0 +1,71 @@
|
||||
import uuid
|
||||
from typing import Sequence
|
||||
|
||||
from sqlalchemy import select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.study import Study
|
||||
from app.schemas.study import StudyCreate, StudyUpdate
|
||||
|
||||
|
||||
async def create(db: AsyncSession, study_in: StudyCreate, *, created_by: uuid.UUID | None = None) -> Study:
|
||||
study = Study(
|
||||
code=study_in.code,
|
||||
name=study_in.name,
|
||||
sponsor=study_in.sponsor,
|
||||
protocol_no=study_in.protocol_no,
|
||||
phase=study_in.phase,
|
||||
status=study_in.status,
|
||||
created_by=created_by,
|
||||
)
|
||||
db.add(study)
|
||||
await db.commit()
|
||||
await db.refresh(study)
|
||||
return study
|
||||
|
||||
|
||||
async def get(db: AsyncSession, study_id: uuid.UUID) -> Study | None:
|
||||
result = await db.execute(select(Study).where(Study.id == study_id))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def get_by_code(db: AsyncSession, code: str) -> Study | None:
|
||||
result = await db.execute(select(Study).where(Study.code == code))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def update(db: AsyncSession, study: Study, study_in: StudyUpdate) -> Study:
|
||||
update_data = study_in.model_dump(exclude_unset=True)
|
||||
if update_data:
|
||||
await db.execute(
|
||||
update(Study)
|
||||
.where(Study.id == study.id)
|
||||
.values(**update_data)
|
||||
)
|
||||
await db.commit()
|
||||
await db.refresh(study)
|
||||
return study
|
||||
|
||||
|
||||
async def list_studies(db: AsyncSession, skip: int = 0, limit: int = 100) -> Sequence[Study]:
|
||||
result = await db.execute(select(Study).offset(skip).limit(limit))
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
async def list_studies_for_user(
|
||||
db: AsyncSession,
|
||||
user_id: uuid.UUID,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
) -> Sequence[Study]:
|
||||
from app.models.study_member import StudyMember # local import to avoid cycles
|
||||
|
||||
stmt = (
|
||||
select(Study)
|
||||
.join(StudyMember, StudyMember.study_id == Study.id)
|
||||
.where(StudyMember.user_id == user_id, StudyMember.is_active.is_(True))
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalars().all()
|
||||
@@ -1,5 +1,7 @@
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
from app.db.base_class import Base
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
# Import models to ensure metadata is loaded for create_all
|
||||
from app.models.user import User # noqa: F401
|
||||
from app.models.study import Study # noqa: F401
|
||||
from app.models.site import Site # noqa: F401
|
||||
from app.models.study_member import StudyMember # noqa: F401
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
@@ -0,0 +1,21 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, ForeignKey, String, func
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.base_class import Base
|
||||
|
||||
|
||||
class Site(Base):
|
||||
__tablename__ = "sites"
|
||||
|
||||
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)
|
||||
name: Mapped[str] = mapped_column(String(200), nullable=False)
|
||||
city: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||
pi_name: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||
contact: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default="true")
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
|
||||
@@ -0,0 +1,22 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String, func
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.base_class import Base
|
||||
|
||||
|
||||
class Study(Base):
|
||||
__tablename__ = "studies"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
code: Mapped[str] = mapped_column(String(50), unique=True, nullable=False, index=True)
|
||||
name: Mapped[str] = mapped_column(String(200), nullable=False)
|
||||
sponsor: Mapped[str | None] = mapped_column(String(200), nullable=True)
|
||||
protocol_no: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||
phase: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
||||
status: Mapped[str] = mapped_column(String(20), nullable=False, default="DRAFT")
|
||||
created_by: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
|
||||
@@ -0,0 +1,20 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, ForeignKey, String, UniqueConstraint, func
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.base_class import Base
|
||||
|
||||
|
||||
class StudyMember(Base):
|
||||
__tablename__ = "study_members"
|
||||
__table_args__ = (UniqueConstraint("study_id", "user_id", name="uq_study_member"),)
|
||||
|
||||
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, index=True)
|
||||
user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
|
||||
role_in_study: Mapped[str] = mapped_column(String(20), nullable=False)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default="true")
|
||||
added_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
|
||||
@@ -5,7 +5,7 @@ from sqlalchemy import Boolean, DateTime, String, func
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.base import Base
|
||||
from app.db.base_class import Base
|
||||
|
||||
|
||||
class User(Base):
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Literal, Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
StudyRole = Literal["PM", "CRA", "PV", "IMP"]
|
||||
|
||||
|
||||
class StudyMemberCreate(BaseModel):
|
||||
user_id: uuid.UUID
|
||||
role_in_study: StudyRole
|
||||
is_active: bool = True
|
||||
|
||||
|
||||
class StudyMemberUpdate(BaseModel):
|
||||
role_in_study: Optional[StudyRole] = None
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
|
||||
class StudyMemberRead(BaseModel):
|
||||
id: uuid.UUID
|
||||
study_id: uuid.UUID
|
||||
user_id: uuid.UUID
|
||||
role_in_study: StudyRole
|
||||
is_active: bool
|
||||
added_at: datetime
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
@@ -0,0 +1,34 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class SiteCreate(BaseModel):
|
||||
name: str = Field(min_length=1)
|
||||
city: Optional[str] = None
|
||||
pi_name: Optional[str] = None
|
||||
contact: Optional[str] = None
|
||||
is_active: bool = True
|
||||
|
||||
|
||||
class SiteUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
city: Optional[str] = None
|
||||
pi_name: Optional[str] = None
|
||||
contact: Optional[str] = None
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
|
||||
class SiteRead(BaseModel):
|
||||
id: uuid.UUID
|
||||
study_id: uuid.UUID
|
||||
name: str
|
||||
city: Optional[str]
|
||||
pi_name: Optional[str]
|
||||
contact: Optional[str]
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
@@ -0,0 +1,39 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Literal, Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
StudyStatus = Literal["DRAFT", "ACTIVE", "CLOSED"]
|
||||
|
||||
|
||||
class StudyCreate(BaseModel):
|
||||
code: str = Field(min_length=1)
|
||||
name: str = Field(min_length=1)
|
||||
sponsor: Optional[str] = None
|
||||
protocol_no: Optional[str] = None
|
||||
phase: Optional[str] = None
|
||||
status: StudyStatus = "DRAFT"
|
||||
|
||||
|
||||
class StudyUpdate(BaseModel):
|
||||
code: Optional[str] = None
|
||||
name: Optional[str] = None
|
||||
sponsor: Optional[str] = None
|
||||
protocol_no: Optional[str] = None
|
||||
phase: Optional[str] = None
|
||||
status: Optional[StudyStatus] = None
|
||||
|
||||
|
||||
class StudyRead(BaseModel):
|
||||
id: uuid.UUID
|
||||
code: str
|
||||
name: str
|
||||
sponsor: Optional[str]
|
||||
protocol_no: Optional[str]
|
||||
phase: Optional[str]
|
||||
status: StudyStatus
|
||||
created_by: Optional[uuid.UUID]
|
||||
created_at: datetime
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
Reference in New Issue
Block a user