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

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
+3 -1
View File
@@ -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,
}
+11 -2
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+3 -3
View File
@@ -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,
+5 -2
View File
@@ -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,
}
+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,
+1 -1
View File
@@ -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):
+2 -2
View File
@@ -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:
+2 -3
View File
@@ -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())
+1
View File
@@ -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 -5
View File
@@ -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)
+1
View File
@@ -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)
+1 -1
View File
@@ -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,
+3
View File
@@ -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]
+1
View File
@@ -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)
-10
View File
@@ -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)
+5 -5
View File
@@ -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)