中文:收口权限与中心/立项配置改造
This commit is contained in:
@@ -196,7 +196,9 @@ async def update_me(
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="当前密码不正确")
|
||||
update_data = {
|
||||
"full_name": payload.full_name if payload.full_name is not None else current_user.full_name,
|
||||
"department": payload.department if payload.department is not None else current_user.department,
|
||||
"clinical_department": (
|
||||
payload.clinical_department if payload.clinical_department is not None else current_user.clinical_department
|
||||
),
|
||||
"password": payload.password if payload.password else None,
|
||||
"avatar_url": payload.avatar_url if payload.avatar_url is not None else current_user.avatar_url,
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_cra_site_scope, get_current_user, get_db_session, require_study_member
|
||||
from app.crud import member as member_crud
|
||||
from app.models.milestone import Milestone
|
||||
from app.schemas.progress import StudyProgressRead
|
||||
from app.schemas.visit import VisitLostItem
|
||||
@@ -14,6 +15,10 @@ from app.crud import overview as overview_crud
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _role_value(user) -> str:
|
||||
return user.role.value if hasattr(user.role, "value") else str(user.role)
|
||||
|
||||
|
||||
@router.get("/progress", response_model=StudyProgressRead, dependencies=[Depends(require_study_member())])
|
||||
async def get_progress(
|
||||
study_id: uuid.UUID,
|
||||
@@ -68,8 +73,12 @@ async def get_center_summary(
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> list[CenterSummaryItem]:
|
||||
role_value = current_user.role.value if hasattr(current_user.role, "value") else current_user.role
|
||||
if role_value not in {"ADMIN", "PM", "CRA"}:
|
||||
role_value = _role_value(current_user)
|
||||
member_role = None
|
||||
if role_value != "ADMIN":
|
||||
membership = await member_crud.get_member(db, study_id, current_user.id)
|
||||
member_role = membership.role_in_study if membership and membership.is_active else None
|
||||
if role_value != "ADMIN" and member_role not in {"PM", "CRA"}:
|
||||
return []
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
scope_ids = cra_scope[0] if cra_scope else None
|
||||
|
||||
@@ -54,7 +54,7 @@ async def _ensure_project_access(db: AsyncSession, project_id: uuid.UUID, curren
|
||||
membership = await member_crud.get_member(db, project_id, current_user.id)
|
||||
if not membership or not membership.is_active:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="不是项目成员")
|
||||
if write and membership.role_in_study != "PM":
|
||||
if write and membership.role_in_study not in {"ADMIN", "PM"}:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="项目权限不足")
|
||||
return membership
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ async def _ensure_project_access(db: AsyncSession, project_id: uuid.UUID, curren
|
||||
membership = await member_crud.get_member(db, project_id, current_user.id)
|
||||
if not membership or not membership.is_active:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="不是项目成员")
|
||||
if write and membership.role_in_study != "PM":
|
||||
if write and membership.role_in_study not in {"ADMIN", "PM"}:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="项目权限不足")
|
||||
return membership
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ async def _ensure_study_exists(db: AsyncSession, study_id: uuid.UUID):
|
||||
"/",
|
||||
response_model=StudyMemberRead,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
dependencies=[Depends(require_study_roles(["PM"])), Depends(require_study_not_locked())],
|
||||
dependencies=[Depends(require_study_roles(["ADMIN", "PM"])), Depends(require_study_not_locked())],
|
||||
)
|
||||
async def add_member(
|
||||
study_id: uuid.UUID,
|
||||
@@ -115,7 +115,7 @@ async def list_members(
|
||||
@router.patch(
|
||||
"/{member_id}",
|
||||
response_model=StudyMemberRead,
|
||||
dependencies=[Depends(require_study_roles(["PM"])), Depends(require_study_not_locked())],
|
||||
dependencies=[Depends(require_study_roles(["ADMIN", "PM"])), Depends(require_study_not_locked())],
|
||||
)
|
||||
async def update_member(
|
||||
study_id: uuid.UUID,
|
||||
@@ -147,7 +147,7 @@ async def update_member(
|
||||
@router.delete(
|
||||
"/{member_id}",
|
||||
response_model=StudyMemberRead,
|
||||
dependencies=[Depends(require_study_roles(["PM"])), Depends(require_study_not_locked())],
|
||||
dependencies=[Depends(require_study_roles(["ADMIN", "PM"])), Depends(require_study_not_locked())],
|
||||
)
|
||||
async def remove_member(
|
||||
study_id: uuid.UUID,
|
||||
|
||||
@@ -26,7 +26,7 @@ async def _ensure_study_exists(db: AsyncSession, study_id: uuid.UUID):
|
||||
"/",
|
||||
response_model=SiteRead,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
dependencies=[Depends(require_study_roles(["PM"])), Depends(require_study_not_locked())],
|
||||
dependencies=[Depends(require_study_roles(["ADMIN", "PM"])), Depends(require_study_not_locked())],
|
||||
)
|
||||
async def create_site(
|
||||
study_id: uuid.UUID,
|
||||
@@ -91,7 +91,7 @@ async def list_sites(
|
||||
@router.patch(
|
||||
"/{site_id}",
|
||||
response_model=SiteRead,
|
||||
dependencies=[Depends(require_study_roles(["PM"])), Depends(require_study_not_locked())],
|
||||
dependencies=[Depends(require_study_roles(["ADMIN", "PM"])), Depends(require_study_not_locked())],
|
||||
)
|
||||
async def update_site(
|
||||
study_id: uuid.UUID,
|
||||
@@ -108,6 +108,7 @@ async def update_site(
|
||||
"name": site.name,
|
||||
"city": site.city,
|
||||
"pi_name": site.pi_name,
|
||||
"phone": site.phone,
|
||||
"contact": site.contact,
|
||||
"is_active": site.is_active,
|
||||
}
|
||||
@@ -116,6 +117,7 @@ async def update_site(
|
||||
"name": updated.name,
|
||||
"city": updated.city,
|
||||
"pi_name": updated.pi_name,
|
||||
"phone": updated.phone,
|
||||
"contact": updated.contact,
|
||||
"is_active": updated.is_active,
|
||||
}
|
||||
@@ -155,6 +157,7 @@ async def delete_site(
|
||||
"name": site.name,
|
||||
"city": site.city,
|
||||
"pi_name": site.pi_name,
|
||||
"phone": site.phone,
|
||||
"contact": site.contact,
|
||||
"is_active": site.is_active,
|
||||
}
|
||||
|
||||
@@ -42,6 +42,16 @@ from app.utils.pagination import paginate
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _study_read_with_role(study, role_in_study: str | None = None) -> StudyRead:
|
||||
data = StudyRead.model_validate(study)
|
||||
data.role_in_study = role_in_study
|
||||
return data
|
||||
|
||||
|
||||
def _role_value(user) -> str:
|
||||
return user.role.value if hasattr(user.role, "value") else str(user.role)
|
||||
|
||||
def _raise_validation_error(errors: list[dict[str, str]]) -> None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
@@ -196,19 +206,6 @@ def _validate_setup_data(
|
||||
if total_site_target > plan.totalTarget:
|
||||
errors.append({"field": "siteEnrollmentPlans", "message": "中心计划总例数不能超过项目总入组例数"})
|
||||
|
||||
for index, row in enumerate(payload.monitoringStrategies):
|
||||
row_prefix = f"monitoringStrategies[{index}]"
|
||||
if not row.id:
|
||||
errors.append({"field": f"{row_prefix}.id", "message": "ID不能为空"})
|
||||
if _is_empty_row([row.strategyType, row.detail, row.frequency]):
|
||||
continue
|
||||
if row.strategyType not in allowed_strategy_types:
|
||||
errors.append({"field": f"{row_prefix}.strategyType", "message": "监查类型不合法"})
|
||||
if not row.detail.strip():
|
||||
errors.append({"field": f"{row_prefix}.detail", "message": "策略详情不能为空"})
|
||||
if row.frequency not in {"不限", "按触发", "每月1次"} and re.fullmatch(r"\d+次", row.frequency) is None:
|
||||
errors.append({"field": f"{row_prefix}.frequency", "message": "监查次数格式应为“不限”/“按触发”/“每月1次”或“N次”"})
|
||||
|
||||
for index, row in enumerate(payload.centerConfirm):
|
||||
row_prefix = f"centerConfirm[{index}]"
|
||||
if not row.id:
|
||||
@@ -253,7 +250,6 @@ def _build_default_setup_config_from_study(study, sites: list) -> StudySetupConf
|
||||
},
|
||||
siteMilestones=[],
|
||||
siteEnrollmentPlans=[],
|
||||
monitoringStrategies=[],
|
||||
centerConfirm=[],
|
||||
)
|
||||
|
||||
@@ -602,7 +598,6 @@ _SETUP_MODULE_KEYS = (
|
||||
"enrollmentPlan",
|
||||
"siteMilestones",
|
||||
"siteEnrollmentPlans",
|
||||
"monitoringStrategies",
|
||||
"centerConfirm",
|
||||
)
|
||||
|
||||
@@ -611,7 +606,6 @@ _SETUP_MODULE_LABELS = {
|
||||
"enrollmentPlan": "项目入组计划",
|
||||
"siteMilestones": "中心里程碑",
|
||||
"siteEnrollmentPlans": "中心入组计划",
|
||||
"monitoringStrategies": "监查策略",
|
||||
"centerConfirm": "中心确认",
|
||||
}
|
||||
|
||||
@@ -635,11 +629,6 @@ _SETUP_FIELD_LABELS = {
|
||||
"siteName": "中心名称",
|
||||
"target": "计划例数",
|
||||
"note": "备注",
|
||||
"strategyType": "监查类型",
|
||||
"detail": "策略详情",
|
||||
"frequency": "监查频次",
|
||||
"updatedAt": "更新时间",
|
||||
"enabled": "启用状态",
|
||||
"confirmer": "确认人",
|
||||
"confirmStatus": "确认状态",
|
||||
"confirmDate": "确认日期",
|
||||
@@ -676,8 +665,6 @@ def _setup_row_identity(module_key: str, row: Any, index: int) -> str:
|
||||
return str(row.get("milestone") or "").strip() or f"第{index + 1}行"
|
||||
if module_key in {"siteEnrollmentPlans", "centerConfirm"}:
|
||||
return str(row.get("siteName") or row.get("siteId") or "").strip() or f"第{index + 1}行"
|
||||
if module_key == "monitoringStrategies":
|
||||
return str(row.get("strategyType") or "").strip() or f"第{index + 1}行"
|
||||
return f"第{index + 1}行"
|
||||
|
||||
|
||||
@@ -794,30 +781,39 @@ async def list_studies(
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> PaginatedResponse[StudyRead]:
|
||||
if current_user.role == "ADMIN":
|
||||
if _role_value(current_user) == "ADMIN":
|
||||
studies = await study_crud.list_studies(db, skip=skip, limit=limit)
|
||||
total = await study_crud.list_studies(db, skip=0, limit=10_000_000)
|
||||
items = [_study_read_with_role(study) for study in studies]
|
||||
else:
|
||||
studies = await study_crud.list_studies_for_user(db, current_user.id, skip=skip, limit=limit)
|
||||
total = await study_crud.list_studies_for_user(db, current_user.id, skip=0, limit=10_000_000)
|
||||
return paginate(list(studies), total=len(total))
|
||||
items = []
|
||||
for study in studies:
|
||||
member = await member_crud.get_member(db, study.id, current_user.id)
|
||||
items.append(_study_read_with_role(study, member.role_in_study if member else None))
|
||||
return paginate(items, total=len(total))
|
||||
|
||||
|
||||
@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),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> StudyRead:
|
||||
study = await study_crud.get(db, study_id)
|
||||
if not study:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在")
|
||||
return study
|
||||
if _role_value(current_user) == "ADMIN":
|
||||
return _study_read_with_role(study)
|
||||
member = await member_crud.get_member(db, study_id, current_user.id)
|
||||
return _study_read_with_role(study, member.role_in_study if member else None)
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/{study_id}",
|
||||
response_model=StudyRead,
|
||||
dependencies=[Depends(require_study_roles(["PM"]))],
|
||||
dependencies=[Depends(require_study_roles(["ADMIN", "PM"]))],
|
||||
)
|
||||
async def update_study(
|
||||
study_id: uuid.UUID,
|
||||
@@ -994,7 +990,7 @@ async def get_study_setup_config(
|
||||
@router.put(
|
||||
"/{study_id}/setup-config",
|
||||
response_model=StudySetupConfigRead,
|
||||
dependencies=[Depends(require_study_roles(["PM"])), Depends(require_study_not_locked())],
|
||||
dependencies=[Depends(require_study_roles(["ADMIN", "PM"])), Depends(require_study_not_locked())],
|
||||
)
|
||||
async def upsert_study_setup_config(
|
||||
study_id: uuid.UUID,
|
||||
@@ -1072,7 +1068,7 @@ async def upsert_study_setup_config(
|
||||
@router.post(
|
||||
"/{study_id}/setup-config/publish",
|
||||
response_model=StudySetupConfigRead,
|
||||
dependencies=[Depends(require_study_roles(["PM"])), Depends(require_study_not_locked())],
|
||||
dependencies=[Depends(require_study_roles(["ADMIN", "PM"])), Depends(require_study_not_locked())],
|
||||
)
|
||||
async def publish_study_setup_config(
|
||||
study_id: uuid.UUID,
|
||||
@@ -1225,7 +1221,7 @@ async def list_study_setup_config_versions(
|
||||
@router.post(
|
||||
"/{study_id}/setup-config/rollback",
|
||||
response_model=StudySetupConfigRead,
|
||||
dependencies=[Depends(require_study_roles(["PM"])), Depends(require_study_not_locked())],
|
||||
dependencies=[Depends(require_study_roles(["ADMIN", "PM"])), Depends(require_study_not_locked())],
|
||||
)
|
||||
async def rollback_study_setup_config(
|
||||
study_id: uuid.UUID,
|
||||
@@ -1283,7 +1279,7 @@ async def rollback_study_setup_config(
|
||||
@router.post(
|
||||
"/{study_id}/setup-config/draft/checkout-branch",
|
||||
response_model=StudySetupConfigRead,
|
||||
dependencies=[Depends(require_study_roles(["PM"])), Depends(require_study_not_locked())],
|
||||
dependencies=[Depends(require_study_roles(["ADMIN", "PM"])), Depends(require_study_not_locked())],
|
||||
)
|
||||
async def checkout_study_setup_config_branch_draft(
|
||||
study_id: uuid.UUID,
|
||||
@@ -1342,7 +1338,7 @@ async def checkout_study_setup_config_branch_draft(
|
||||
@router.post(
|
||||
"/{study_id}/setup-config/draft/clear",
|
||||
response_model=StudySetupConfigRead,
|
||||
dependencies=[Depends(require_study_roles(["PM"])), Depends(require_study_not_locked())],
|
||||
dependencies=[Depends(require_study_roles(["ADMIN", "PM"])), Depends(require_study_not_locked())],
|
||||
)
|
||||
async def clear_study_setup_config_draft(
|
||||
study_id: uuid.UUID,
|
||||
@@ -1394,7 +1390,7 @@ async def clear_study_setup_config_draft(
|
||||
@router.post(
|
||||
"/{study_id}/setup-config/draft/refill",
|
||||
response_model=StudySetupConfigRead,
|
||||
dependencies=[Depends(require_study_roles(["PM"])), Depends(require_study_not_locked())],
|
||||
dependencies=[Depends(require_study_roles(["ADMIN", "PM"])), Depends(require_study_not_locked())],
|
||||
)
|
||||
async def refill_study_setup_config_draft(
|
||||
study_id: uuid.UUID,
|
||||
@@ -1448,7 +1444,7 @@ async def refill_study_setup_config_draft(
|
||||
@router.post(
|
||||
"/{study_id}/setup-config/merge-main",
|
||||
response_model=StudySetupConfigRead,
|
||||
dependencies=[Depends(require_study_roles(["PM"])), Depends(require_study_not_locked())],
|
||||
dependencies=[Depends(require_study_roles(["ADMIN", "PM"])), Depends(require_study_not_locked())],
|
||||
)
|
||||
async def merge_study_setup_config_to_main(
|
||||
study_id: uuid.UUID,
|
||||
@@ -1543,7 +1539,7 @@ async def merge_study_setup_config_to_main(
|
||||
@router.delete(
|
||||
"/{study_id}/setup-config/versions/{target_version}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(require_study_roles(["PM"])), Depends(require_study_not_locked())],
|
||||
dependencies=[Depends(require_study_roles(["ADMIN", "PM"])), Depends(require_study_not_locked())],
|
||||
)
|
||||
async def delete_study_setup_config_version(
|
||||
study_id: uuid.UUID,
|
||||
|
||||
@@ -7,7 +7,7 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
PROTECTED_ADMIN_EMAIL = "admin@huapont.cn"
|
||||
PROTECTED_ADMIN_DEFAULT_PASSWORD = "admin123"
|
||||
PROTECTED_ADMIN_FULL_NAME = "System Admin"
|
||||
PROTECTED_ADMIN_DEPARTMENT = "SYSTEM"
|
||||
PROTECTED_ADMIN_CLINICAL_DEPARTMENT = "SYSTEM"
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
|
||||
@@ -95,7 +95,7 @@ def require_study_member():
|
||||
return dependency
|
||||
|
||||
|
||||
def require_study_roles(roles: Iterable[str]):
|
||||
def require_study_roles(roles: Iterable[str], *, allow_system_admin: bool = True):
|
||||
roles_set = set(roles)
|
||||
|
||||
async def dependency(
|
||||
@@ -104,7 +104,7 @@ def require_study_roles(roles: Iterable[str]):
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
):
|
||||
role_value = current_user.role.value if hasattr(current_user.role, "value") else current_user.role
|
||||
if role_value == "ADMIN":
|
||||
if allow_system_admin and role_value == "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:
|
||||
|
||||
@@ -32,9 +32,8 @@ ROLE_ACTIONS: dict[str, set[str]] = {
|
||||
|
||||
def is_allowed(action: str, user, membership) -> bool:
|
||||
role_value = user.role.value if hasattr(user.role, "value") else str(user.role)
|
||||
allowed = ROLE_ACTIONS.get(role_value, set())
|
||||
if action in allowed:
|
||||
return True
|
||||
if role_value == "ADMIN":
|
||||
return action in ROLE_ACTIONS["ADMIN"]
|
||||
member_role = getattr(membership, "role_in_study", None)
|
||||
if member_role:
|
||||
return action in ROLE_ACTIONS.get(member_role, set())
|
||||
|
||||
@@ -42,6 +42,7 @@ async def create_site(db: AsyncSession, study_id: uuid.UUID, site_in: SiteCreate
|
||||
name=site_in.name,
|
||||
city=site_in.city,
|
||||
pi_name=site_in.pi_name,
|
||||
phone=site_in.phone,
|
||||
contact=site_in.contact,
|
||||
is_active=site_in.is_active,
|
||||
enrollment_plan_start_date=site_in.enrollment_plan_start_date,
|
||||
|
||||
@@ -5,8 +5,8 @@ from sqlalchemy import delete, func, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.config import (
|
||||
PROTECTED_ADMIN_CLINICAL_DEPARTMENT,
|
||||
PROTECTED_ADMIN_DEFAULT_PASSWORD,
|
||||
PROTECTED_ADMIN_DEPARTMENT,
|
||||
PROTECTED_ADMIN_EMAIL,
|
||||
PROTECTED_ADMIN_FULL_NAME,
|
||||
)
|
||||
@@ -43,7 +43,7 @@ async def create_user(
|
||||
password_hash=hash_password(user_in.password),
|
||||
full_name=user_in.full_name,
|
||||
role=UserRole(user_in.role),
|
||||
department=user_in.department,
|
||||
clinical_department=user_in.clinical_department,
|
||||
status=status_value,
|
||||
)
|
||||
db.add(user)
|
||||
@@ -70,8 +70,8 @@ async def update_user(db: AsyncSession, user: User, user_in: UserUpdate) -> User
|
||||
update_data["password_hash"] = hash_password(user_in.password)
|
||||
if user_in.full_name is not None:
|
||||
update_data["full_name"] = user_in.full_name
|
||||
if user_in.department is not None:
|
||||
update_data["department"] = user_in.department
|
||||
if user_in.clinical_department is not None:
|
||||
update_data["clinical_department"] = user_in.clinical_department
|
||||
|
||||
if update_data:
|
||||
await db.execute(update(User).where(User.id == user.id).values(**update_data))
|
||||
@@ -114,7 +114,7 @@ async def ensure_admin_exists(db: AsyncSession, *, default_password: str = PROTE
|
||||
password_hash=hash_password(default_password),
|
||||
full_name=PROTECTED_ADMIN_FULL_NAME,
|
||||
role=UserRole.ADMIN,
|
||||
department=PROTECTED_ADMIN_DEPARTMENT,
|
||||
clinical_department=PROTECTED_ADMIN_CLINICAL_DEPARTMENT,
|
||||
status=UserStatus.ACTIVE,
|
||||
)
|
||||
db.add(new_admin)
|
||||
|
||||
@@ -16,6 +16,7 @@ class Site(Base):
|
||||
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)
|
||||
phone: 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")
|
||||
enrollment_target: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
|
||||
@@ -33,7 +33,7 @@ class User(Base):
|
||||
password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
full_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
role: Mapped[UserRole] = mapped_column(Enum(UserRole, name="user_role"), nullable=False)
|
||||
department: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
clinical_department: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
status: Mapped[UserStatus] = mapped_column(
|
||||
Enum(UserStatus, name="user_status"),
|
||||
nullable=False,
|
||||
|
||||
@@ -9,6 +9,7 @@ class SiteCreate(BaseModel):
|
||||
name: str = Field(min_length=1)
|
||||
city: Optional[str] = None
|
||||
pi_name: Optional[str] = None
|
||||
phone: Optional[str] = None
|
||||
contact: Optional[str] = None
|
||||
is_active: bool = True
|
||||
enrollment_plan_start_date: Optional[date] = None
|
||||
@@ -20,6 +21,7 @@ class SiteUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
city: Optional[str] = None
|
||||
pi_name: Optional[str] = None
|
||||
phone: Optional[str] = None
|
||||
contact: Optional[str] = None
|
||||
is_active: Optional[bool] = None
|
||||
enrollment_plan_start_date: Optional[date] = None
|
||||
@@ -33,6 +35,7 @@ class SiteRead(BaseModel):
|
||||
name: str
|
||||
city: Optional[str]
|
||||
pi_name: Optional[str]
|
||||
phone: Optional[str]
|
||||
contact: Optional[str]
|
||||
is_active: bool
|
||||
enrollment_target: Optional[int]
|
||||
|
||||
@@ -126,5 +126,6 @@ class StudyRead(BaseModel):
|
||||
visit_schedule: list[VisitScheduleItem]
|
||||
created_by: Optional[uuid.UUID]
|
||||
created_at: datetime
|
||||
role_in_study: Optional[str] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@@ -44,15 +44,6 @@ class SiteEnrollmentPlanItem(BaseModel):
|
||||
stageBreakdown: str = ""
|
||||
|
||||
|
||||
class MonitoringStrategyItem(BaseModel):
|
||||
id: str
|
||||
strategyType: str = ""
|
||||
detail: str = ""
|
||||
frequency: str = ""
|
||||
updatedAt: str = ""
|
||||
enabled: bool = True
|
||||
|
||||
|
||||
class CenterConfirmItem(BaseModel):
|
||||
id: str
|
||||
siteId: str = ""
|
||||
@@ -105,7 +96,6 @@ class StudySetupConfigData(BaseModel):
|
||||
enrollmentPlan: EnrollmentPlanItem = Field(default_factory=EnrollmentPlanItem)
|
||||
siteMilestones: list[SiteMilestoneItem] = Field(default_factory=list)
|
||||
siteEnrollmentPlans: list[SiteEnrollmentPlanItem] = Field(default_factory=list)
|
||||
monitoringStrategies: list[MonitoringStrategyItem] = Field(default_factory=list)
|
||||
centerConfirm: list[CenterConfirmItem] = Field(default_factory=list)
|
||||
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ class UserRegisterRequest(_PasswordValidator):
|
||||
email: EmailStr
|
||||
full_name: str = Field(min_length=1)
|
||||
role: RegisterRole
|
||||
department: str = Field(min_length=1)
|
||||
clinical_department: str = Field(min_length=1)
|
||||
|
||||
|
||||
class UserCreate(_PasswordValidator):
|
||||
@@ -48,7 +48,7 @@ class UserCreate(_PasswordValidator):
|
||||
email: EmailStr
|
||||
full_name: str = Field(min_length=1)
|
||||
role: UserRole
|
||||
department: str = Field(min_length=1)
|
||||
clinical_department: str = Field(min_length=1)
|
||||
status: UserStatus = "ACTIVE"
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ class UserRead(BaseModel):
|
||||
username: str
|
||||
full_name: str
|
||||
role: UserRole
|
||||
department: str
|
||||
clinical_department: str
|
||||
status: UserStatus
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
@@ -73,7 +73,7 @@ class UserUpdate(_PasswordValidator):
|
||||
email: Optional[EmailStr] = None
|
||||
full_name: Optional[str] = None
|
||||
role: Optional[UserRole] = None
|
||||
department: Optional[str] = None
|
||||
clinical_department: Optional[str] = None
|
||||
status: Optional[UserStatus] = None
|
||||
password: Optional[str] = None
|
||||
is_active: Optional[bool] = None
|
||||
@@ -95,7 +95,7 @@ class AdminUserListResponse(BaseModel):
|
||||
|
||||
class UserSelfUpdate(_PasswordValidator):
|
||||
full_name: Optional[str] = None
|
||||
department: Optional[str] = None
|
||||
clinical_department: Optional[str] = None
|
||||
password: Optional[str] = None
|
||||
current_password: Optional[str] = None
|
||||
avatar_url: Optional[str] = None
|
||||
|
||||
@@ -10,7 +10,6 @@ from app.models.milestone import Milestone
|
||||
from app.models.site import Site
|
||||
from app.models.study import Study
|
||||
from app.models.study_center_confirm import StudyCenterConfirm
|
||||
from app.models.study_monitoring_strategy import StudyMonitoringStrategy
|
||||
from app.models.user import User
|
||||
from app.schemas.study_setup_config import StudySetupConfigData
|
||||
|
||||
@@ -207,34 +206,6 @@ async def _replace_site_milestones(
|
||||
)
|
||||
|
||||
|
||||
async def _replace_monitoring_strategies(
|
||||
db: AsyncSession,
|
||||
*,
|
||||
study_id: uuid.UUID,
|
||||
setup_data: StudySetupConfigData,
|
||||
result: SetupProjectionResult,
|
||||
) -> None:
|
||||
await db.execute(delete(StudyMonitoringStrategy).where(StudyMonitoringStrategy.study_id == study_id))
|
||||
for row in setup_data.monitoringStrategies:
|
||||
strategy_type = _normalize_text(row.strategyType)
|
||||
detail = _normalize_text(row.detail)
|
||||
frequency = _normalize_text(row.frequency)
|
||||
if not any([strategy_type, detail, frequency]):
|
||||
continue
|
||||
if not strategy_type or not detail or not frequency:
|
||||
result.warnings.append(f"monitoring_strategy_incomplete:{row.id}")
|
||||
continue
|
||||
db.add(
|
||||
StudyMonitoringStrategy(
|
||||
study_id=study_id,
|
||||
source_setup_item_id=_normalize_text(row.id),
|
||||
strategy_type=strategy_type,
|
||||
detail=detail,
|
||||
frequency=frequency,
|
||||
enabled=bool(row.enabled),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def _replace_center_confirms(
|
||||
db: AsyncSession,
|
||||
@@ -384,7 +355,6 @@ async def apply_setup_projection_on_publish(
|
||||
owner_lookup=owner_lookup,
|
||||
result=result,
|
||||
)
|
||||
await _replace_monitoring_strategies(db, study_id=study_id, setup_data=setup_data, result=result)
|
||||
await _replace_center_confirms(db, study_id=study_id, setup_data=setup_data, site_map=site_map, result=result)
|
||||
|
||||
result.site_skipped_count = len(result.skipped_items)
|
||||
|
||||
Reference in New Issue
Block a user