中文:收口权限与中心/立项配置改造

This commit is contained in:
Cheng Zhou
2026-05-12 10:16:52 +08:00
parent 6e90370a5f
commit 77e842637d
67 changed files with 706 additions and 669 deletions
+31 -35
View File
@@ -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,