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)
|
||||
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