补齐业务路由与页面权限控制

This commit is contained in:
Cheng Zhou
2026-05-28 10:56:28 +08:00
parent fa961f1391
commit 8611bd66f8
39 changed files with 458 additions and 250 deletions
+66 -17
View File
@@ -48,7 +48,7 @@ def _is_overdue(ae: AERead) -> bool:
"/",
response_model=AERead,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(require_api_permission("risk_issues:create")), Depends(require_study_not_locked())],
dependencies=[Depends(require_api_permission("subject_aes:create")), Depends(require_study_not_locked())],
)
async def create_ae(
study_id: uuid.UUID,
@@ -86,20 +86,15 @@ async def create_ae(
return data
@router.get(
"/",
response_model=list[AERead],
dependencies=[Depends(require_api_permission("risk_issues:read"))],
)
async def list_ae(
async def _list_ae_records(
study_id: uuid.UUID,
status_filter: str | None = None,
seriousness: str | None = None,
site_id: uuid.UUID | None = None,
subject_id: uuid.UUID | None = None,
overdue: bool | None = None,
db: AsyncSession = Depends(get_db_session),
current_user=Depends(get_current_user),
status_filter: str | None,
seriousness: str | None,
site_id: uuid.UUID | None,
subject_id: uuid.UUID | None,
overdue: bool | None,
db: AsyncSession,
current_user,
) -> list[AERead]:
await _ensure_study_exists(db, study_id)
cra_scope = await get_cra_site_scope(db, study_id, current_user)
@@ -124,10 +119,64 @@ async def list_ae(
return result
@router.get(
"/summary",
response_model=list[AERead],
dependencies=[Depends(require_api_permission("subject_aes:list"))],
)
async def list_risk_issue_ae(
study_id: uuid.UUID,
status_filter: str | None = None,
seriousness: str | None = None,
site_id: uuid.UUID | None = None,
subject_id: uuid.UUID | None = None,
overdue: bool | None = None,
db: AsyncSession = Depends(get_db_session),
current_user=Depends(get_current_user),
) -> list[AERead]:
return await _list_ae_records(
study_id,
status_filter,
seriousness,
site_id,
subject_id,
overdue,
db,
current_user,
)
@router.get(
"/",
response_model=list[AERead],
dependencies=[Depends(require_api_permission("subject_aes:list"))],
)
async def list_ae(
study_id: uuid.UUID,
status_filter: str | None = None,
seriousness: str | None = None,
site_id: uuid.UUID | None = None,
subject_id: uuid.UUID | None = None,
overdue: bool | None = None,
db: AsyncSession = Depends(get_db_session),
current_user=Depends(get_current_user),
) -> list[AERead]:
return await _list_ae_records(
study_id,
status_filter,
seriousness,
site_id,
subject_id,
overdue,
db,
current_user,
)
@router.get(
"/{ae_id}",
response_model=AERead,
dependencies=[Depends(require_api_permission("risk_issues:read"))],
dependencies=[Depends(require_api_permission("subject_aes:read"))],
)
async def get_ae(
study_id: uuid.UUID,
@@ -155,7 +204,7 @@ async def get_ae(
@router.patch(
"/{ae_id}",
response_model=AERead,
dependencies=[Depends(require_api_permission("risk_issues:update")), Depends(require_study_not_locked())],
dependencies=[Depends(require_api_permission("subject_aes:update")), Depends(require_study_not_locked())],
)
async def update_ae(
study_id: uuid.UUID,
@@ -210,7 +259,7 @@ async def update_ae(
@router.delete(
"/{ae_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(require_api_permission("risk_issues:delete")), Depends(require_study_not_locked())],
dependencies=[Depends(require_api_permission("subject_aes:delete")), Depends(require_study_not_locked())],
)
async def delete_ae(
study_id: uuid.UUID,
+2 -2
View File
@@ -70,7 +70,7 @@ async def list_lost_visits(
@router.get("/center-summary", response_model=list[CenterSummaryItem], dependencies=[
Depends(require_study_member()),
Depends(require_api_permission("dashboard:read"))
Depends(require_api_permission("project_overview:read"))
])
async def get_center_summary(
study_id: uuid.UUID,
@@ -91,7 +91,7 @@ async def get_center_summary(
stage_order = [
("institution_initiation_status", "机构立项"),
("ethics_status", "伦理审批"),
("ethics_status", "伦理记录"),
("contract_sign_status", "合同签署"),
("startup_status", "启动"),
("enrollment_status", "入组"),
+2 -2
View File
@@ -19,9 +19,9 @@ PROJECT_ROLE_RANK = {
"ADMIN": 100,
"PM": 80,
"PV": 50,
"MEDICAL_REVIEW": 50,
"QA": 50,
"CRA": 40,
"IMP": 40,
"CTA": 40,
}
@@ -332,7 +332,7 @@ def _normalize_file_rows(filename: str, content: bytes) -> list[dict[str, object
@router.get(
"/issues",
response_model=list[MonitoringVisitIssueRead],
dependencies=[Depends(require_api_permission("monitoring_audit:read"))],
dependencies=[Depends(require_api_permission("monitoring_issues:list"))],
)
async def list_monitoring_visit_issues(
study_id: uuid.UUID,
@@ -382,7 +382,7 @@ async def list_monitoring_visit_issues(
"/issues",
response_model=MonitoringVisitIssueRead,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(require_api_permission("monitoring_audit:create")), Depends(require_study_not_locked())],
dependencies=[Depends(require_api_permission("monitoring_issues:create")), Depends(require_study_not_locked())],
)
async def create_monitoring_visit_issue(
study_id: uuid.UUID,
@@ -421,7 +421,7 @@ async def create_monitoring_visit_issue(
@router.get(
"/issues/export",
response_class=StreamingResponse,
dependencies=[Depends(require_api_permission("monitoring_audit:read"))],
dependencies=[Depends(require_api_permission("monitoring_issues:list"))],
)
async def export_monitoring_visit_issues(
study_id: uuid.UUID,
@@ -553,7 +553,7 @@ async def export_monitoring_visit_issues(
@router.get(
"/issues/{issue_id}",
response_model=MonitoringVisitIssueRead,
dependencies=[Depends(require_api_permission("monitoring_audit:read"))],
dependencies=[Depends(require_api_permission("monitoring_issues:read"))],
)
async def get_monitoring_visit_issue(
study_id: uuid.UUID,
@@ -570,7 +570,7 @@ async def get_monitoring_visit_issue(
@router.patch(
"/issues/{issue_id}",
response_model=MonitoringVisitIssueRead,
dependencies=[Depends(require_api_permission("monitoring_audit:update")), Depends(require_study_not_locked())],
dependencies=[Depends(require_api_permission("monitoring_issues:update")), Depends(require_study_not_locked())],
)
async def update_monitoring_visit_issue(
study_id: uuid.UUID,
@@ -625,7 +625,7 @@ async def update_monitoring_visit_issue(
@router.delete(
"/issues/{issue_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(require_api_permission("monitoring_audit:delete")), Depends(require_study_not_locked())],
dependencies=[Depends(require_api_permission("monitoring_issues:delete")), Depends(require_study_not_locked())],
)
async def delete_monitoring_visit_issue(
study_id: uuid.UUID,
@@ -655,7 +655,7 @@ async def delete_monitoring_visit_issue(
@router.post(
"/issues/import",
response_model=MonitoringVisitIssueImportSummary,
dependencies=[Depends(require_api_permission("monitoring_audit:create")), Depends(require_study_not_locked())],
dependencies=[Depends(require_api_permission("monitoring_issues:create")), Depends(require_study_not_locked())],
)
async def import_monitoring_visit_issues(
study_id: uuid.UUID,
+1 -1
View File
@@ -25,7 +25,7 @@ async def get_project_overview(
返回项目各中心的进度情况,包括:
- 机构立项状态
- 伦理审批状态
- 伦理记录状态
- 合同签署状态
- 启动会状态
- 入组进度
+3 -4
View File
@@ -1,6 +1,6 @@
from fastapi import APIRouter
from app.api.v1 import auth, users, admin_users, studies, sites, members, attachments, audit_logs, dashboard, subjects, visits, aes, finance_dashboard, finance_contracts, fees_contracts, fees_attachments, drug_shipments, material_equipments, project_milestones, startup, knowledge_notes, subject_histories, subject_pds, study_subject_pds, faq_categories, faqs, documents, overview, notifications, monitoring_visit_issues, api_permissions, permission_monitoring, permission_templates, system_permissions, study_active_roles
from app.api.v1 import auth, users, admin_users, studies, sites, members, attachments, audit_logs, dashboard, subjects, visits, aes, finance_dashboard, fees_contracts, drug_shipments, material_equipments, project_milestones, startup, precautions, subject_histories, subject_pds, study_subject_pds, faq_categories, faqs, documents, etmf, overview, notifications, monitoring_visit_issues, api_permissions, permission_monitoring, permission_templates, system_permissions, study_active_roles
api_router = APIRouter()
@@ -22,14 +22,12 @@ api_router.include_router(subjects.router, prefix="/studies/{study_id}/subjects"
api_router.include_router(visits.router, prefix="/studies/{study_id}/subjects/{subject_id}/visits", tags=["visits"])
api_router.include_router(aes.router, prefix="/studies/{study_id}/aes", tags=["aes"])
api_router.include_router(finance_dashboard.router, prefix="/studies/{study_id}/finance", tags=["finance"])
api_router.include_router(finance_contracts.router, prefix="/studies/{study_id}/finance", tags=["finance-contracts"])
api_router.include_router(fees_contracts.router, prefix="/fees", tags=["fees-contracts"])
api_router.include_router(fees_attachments.router, prefix="/fees", tags=["fees-attachments"])
api_router.include_router(drug_shipments.router, prefix="/studies/{study_id}/drug", tags=["drug-shipments"])
api_router.include_router(material_equipments.router, prefix="/studies/{study_id}/materials", tags=["material-equipments"])
api_router.include_router(project_milestones.router, prefix="/studies/{study_id}/project", tags=["project-milestones"])
api_router.include_router(startup.router, prefix="/studies/{study_id}/startup", tags=["startup"])
api_router.include_router(knowledge_notes.router, prefix="/studies/{study_id}/knowledge", tags=["knowledge"])
api_router.include_router(precautions.router, prefix="/studies/{study_id}/shared-library", tags=["precautions"])
api_router.include_router(monitoring_visit_issues.router, prefix="/studies/{study_id}/monitoring", tags=["monitoring-visit-issues"])
api_router.include_router(subject_histories.router, prefix="/studies/{study_id}/subjects/{subject_id}", tags=["subject-histories"])
api_router.include_router(subject_pds.router, prefix="/studies/{study_id}/subjects/{subject_id}", tags=["subject-pds"])
@@ -37,6 +35,7 @@ api_router.include_router(study_subject_pds.router, prefix="/studies/{study_id}"
api_router.include_router(faq_categories.router, prefix="/faqs/categories", tags=["faq-categories"])
api_router.include_router(faqs.router, prefix="/faqs/items", tags=["faqs"])
api_router.include_router(documents.router, prefix="", tags=["documents"])
api_router.include_router(etmf.router, prefix="/etmf", tags=["etmf"])
api_router.include_router(permission_monitoring.router)
api_router.include_router(permission_templates.router)
api_router.include_router(permission_templates.study_router, prefix="/studies/{study_id}", tags=["permission-templates"])
+1 -1
View File
@@ -79,7 +79,7 @@ async def create_site(
module="sites",
action="read",
description="查询中心列表",
default_roles=["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"],
default_roles=["PM", "CRA", "PV", "QA", "CTA"],
)
async def list_sites(
study_id: uuid.UUID,
+10 -10
View File
@@ -53,7 +53,7 @@ async def _ensure_site_active(db: AsyncSession, site_id: uuid.UUID | None):
"/feasibility",
response_model=StartupFeasibilityRead,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(require_api_permission("ethics:create"))],
dependencies=[Depends(require_api_permission("startup_initiation:create"))],
)
async def create_feasibility(
study_id: uuid.UUID,
@@ -83,7 +83,7 @@ async def create_feasibility(
@router.get(
"/feasibility",
response_model=list[StartupFeasibilityRead],
dependencies=[Depends(require_api_permission("ethics:read"))],
dependencies=[Depends(require_api_permission("startup_initiation:read"))],
)
async def list_feasibilities(
study_id: uuid.UUID,
@@ -102,7 +102,7 @@ async def list_feasibilities(
@router.get(
"/feasibility/{record_id}",
response_model=StartupFeasibilityRead,
dependencies=[Depends(require_api_permission("ethics:read"))],
dependencies=[Depends(require_api_permission("startup_initiation:read"))],
)
async def get_feasibility(
study_id: uuid.UUID,
@@ -123,7 +123,7 @@ async def get_feasibility(
@router.patch(
"/feasibility/{record_id}",
response_model=StartupFeasibilityRead,
dependencies=[Depends(require_api_permission("ethics:update"))],
dependencies=[Depends(require_api_permission("startup_initiation:update"))],
)
async def update_feasibility(
study_id: uuid.UUID,
@@ -157,7 +157,7 @@ async def update_feasibility(
@router.delete(
"/feasibility/{record_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(require_api_permission("ethics:delete"))],
dependencies=[Depends(require_api_permission("startup_initiation:delete"))],
)
async def delete_feasibility(
study_id: uuid.UUID,
@@ -190,7 +190,7 @@ async def delete_feasibility(
"/ethics",
response_model=StartupEthicsRead,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(require_api_permission("ethics:create"))],
dependencies=[Depends(require_api_permission("startup_ethics:create"))],
)
async def create_ethics(
study_id: uuid.UUID,
@@ -220,7 +220,7 @@ async def create_ethics(
@router.get(
"/ethics",
response_model=list[StartupEthicsRead],
dependencies=[Depends(require_api_permission("ethics:read"))],
dependencies=[Depends(require_api_permission("startup_ethics:read"))],
)
async def list_ethics(
study_id: uuid.UUID,
@@ -239,7 +239,7 @@ async def list_ethics(
@router.get(
"/ethics/{record_id}",
response_model=StartupEthicsRead,
dependencies=[Depends(require_api_permission("ethics:read"))],
dependencies=[Depends(require_api_permission("startup_ethics:read"))],
)
async def get_ethics(
study_id: uuid.UUID,
@@ -260,7 +260,7 @@ async def get_ethics(
@router.patch(
"/ethics/{record_id}",
response_model=StartupEthicsRead,
dependencies=[Depends(require_api_permission("ethics:update"))],
dependencies=[Depends(require_api_permission("startup_ethics:update"))],
)
async def update_ethics(
study_id: uuid.UUID,
@@ -294,7 +294,7 @@ async def update_ethics(
@router.delete(
"/ethics/{record_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(require_api_permission("ethics:delete"))],
dependencies=[Depends(require_api_permission("startup_ethics:delete"))],
)
async def delete_ethics(
study_id: uuid.UUID,
+2 -2
View File
@@ -3,7 +3,7 @@ import uuid
from fastapi import APIRouter, Depends, HTTPException, status
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.core.deps import get_cra_site_scope, get_current_user, get_db_session, require_api_permission
from app.crud import study as study_crud
from app.crud import subject_pd as subject_pd_crud
from app.schemas.subject_pd import SubjectPdSummaryRead
@@ -21,7 +21,7 @@ async def _ensure_study_exists(db: AsyncSession, study_id: uuid.UUID):
@router.get(
"/subject-pds",
response_model=list[SubjectPdSummaryRead],
dependencies=[Depends(require_study_member())],
dependencies=[Depends(require_api_permission("subject_pds:list"))],
)
async def list_study_subject_pds(
study_id: uuid.UUID,
+1 -1
View File
@@ -66,7 +66,7 @@ async def create_history(
@router.get(
"/histories",
response_model=list[SubjectHistoryRead],
dependencies=[Depends(require_api_permission("subject_histories:read"))],
dependencies=[Depends(require_api_permission("subject_histories:list"))],
)
async def list_histories(
study_id: uuid.UUID,
+1 -1
View File
@@ -64,7 +64,7 @@ def _normalize_choice(value: str | None, allowed: set[str], field_name: str) ->
@router.get(
"/pds",
response_model=list[SubjectPdRead],
dependencies=[Depends(require_api_permission("subject_pds:read"))],
dependencies=[Depends(require_api_permission("subject_pds:list"))],
)
async def list_subject_pds(
study_id: uuid.UUID,
+1 -1
View File
@@ -33,7 +33,7 @@ async def _ensure_subject_active(db: AsyncSession, subject) -> None:
@router.get(
"/",
response_model=list[VisitRead],
dependencies=[Depends(require_api_permission("visits:read"))],
dependencies=[Depends(require_api_permission("visits:list"))],
)
async def list_visits(
study_id: uuid.UUID,