未知(继上次中断)
This commit is contained in:
@@ -5,7 +5,7 @@ from datetime import date
|
||||
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_study_member, require_study_not_locked
|
||||
from app.core.deps import get_cra_site_scope, get_current_user, get_db_session, require_study_member, require_study_not_locked
|
||||
from app.crud import ae as ae_crud
|
||||
from app.crud import audit as audit_crud
|
||||
from app.crud import member as member_crud
|
||||
@@ -65,6 +65,14 @@ async def create_ae(
|
||||
current_user=Depends(get_current_user),
|
||||
) -> AERead:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
if cra_scope:
|
||||
if ae_in.site_id and ae_in.site_id not in cra_scope[0]:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
|
||||
if ae_in.subject_id:
|
||||
subj = await subject_crud.get_subject(db, ae_in.subject_id)
|
||||
if subj and subj.site_id not in cra_scope[0]:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
|
||||
if ae_in.onset_date and ae_in.onset_date > date.today():
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="发生日期不能晚于今天")
|
||||
member_role = await _get_member_role(db, study_id, current_user.id)
|
||||
@@ -102,9 +110,23 @@ async def list_ae(
|
||||
subject_id: uuid.UUID | None = None,
|
||||
overdue: bool | None = None,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> list[AERead]:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
aes = await ae_crud.list_ae(db, study_id, status=status_filter, seriousness=seriousness, site_id=site_id, subject_id=subject_id, overdue=overdue)
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
site_ids = cra_scope[0] if cra_scope else None
|
||||
if site_id and site_ids is not None and site_id not in site_ids:
|
||||
return []
|
||||
aes = await ae_crud.list_ae(
|
||||
db,
|
||||
study_id,
|
||||
status=status_filter,
|
||||
seriousness=seriousness,
|
||||
site_id=site_id,
|
||||
subject_id=subject_id,
|
||||
overdue=overdue,
|
||||
site_ids=site_ids,
|
||||
)
|
||||
result: list[AERead] = []
|
||||
for item in aes:
|
||||
obj = AERead.model_validate(item)
|
||||
@@ -122,6 +144,7 @@ async def get_ae(
|
||||
study_id: uuid.UUID,
|
||||
ae_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> AERead:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
ae = await ae_crud.get_ae(db, ae_id)
|
||||
@@ -129,6 +152,14 @@ async def get_ae(
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="AE 不存在")
|
||||
data = AERead.model_validate(ae)
|
||||
data.is_overdue = _is_overdue(data)
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
if cra_scope:
|
||||
if data.site_id and data.site_id not in cra_scope[0]:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
|
||||
if data.subject_id:
|
||||
subj = await subject_crud.get_subject(db, data.subject_id)
|
||||
if subj and subj.site_id not in cra_scope[0]:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
|
||||
return data
|
||||
|
||||
|
||||
@@ -169,7 +200,7 @@ async def update_ae(
|
||||
detail_before = {
|
||||
"event": ae.term,
|
||||
"onset_date": ae.onset_date,
|
||||
"severity": ae.severity,
|
||||
"seriousness": ae.seriousness,
|
||||
"status": ae.status,
|
||||
"description": ae.description,
|
||||
}
|
||||
@@ -177,7 +208,7 @@ async def update_ae(
|
||||
detail_after = {
|
||||
"event": updated.term,
|
||||
"onset_date": updated.onset_date,
|
||||
"severity": updated.severity,
|
||||
"seriousness": updated.seriousness,
|
||||
"status": updated.status,
|
||||
"description": updated.description,
|
||||
}
|
||||
|
||||
@@ -3,9 +3,13 @@ from fastapi import APIRouter, Depends
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_db_session, require_study_member
|
||||
from app.core.deps import get_cra_site_scope, get_current_user, get_db_session, require_study_member
|
||||
from app.models.milestone import Milestone
|
||||
from app.schemas.progress import StudyProgressRead
|
||||
from app.schemas.visit import VisitLostItem
|
||||
from app.crud import visit as visit_crud
|
||||
from app.schemas.dashboard import CenterSummaryItem
|
||||
from app.crud import overview as overview_crud
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -30,3 +34,87 @@ async def get_progress(
|
||||
milestones_done=milestone_done,
|
||||
completion_rate=completion_rate,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/lost-visits", response_model=list[VisitLostItem], dependencies=[Depends(require_study_member())])
|
||||
async def list_lost_visits(
|
||||
study_id: uuid.UUID,
|
||||
limit: int = 20,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> list[VisitLostItem]:
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
site_ids = cra_scope[0] if cra_scope else None
|
||||
rows = await visit_crud.list_lost_visits(db, study_id, site_ids=site_ids, limit=limit)
|
||||
items: list[VisitLostItem] = []
|
||||
for visit, subject_no, site_id in rows:
|
||||
items.append(
|
||||
VisitLostItem(
|
||||
visit_id=visit.id,
|
||||
subject_id=visit.subject_id,
|
||||
subject_no=subject_no,
|
||||
site_id=site_id,
|
||||
visit_code=visit.visit_code,
|
||||
status=visit.status,
|
||||
updated_at=visit.updated_at,
|
||||
)
|
||||
)
|
||||
return items
|
||||
|
||||
|
||||
@router.get("/center-summary", response_model=list[CenterSummaryItem], dependencies=[Depends(require_study_member())])
|
||||
async def get_center_summary(
|
||||
study_id: uuid.UUID,
|
||||
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"}:
|
||||
return []
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
scope_ids = cra_scope[0] if cra_scope else None
|
||||
scope_id_strs = {str(cid) for cid in scope_ids} if scope_ids is not None else None
|
||||
overview = await overview_crud.get_project_overview(db, study_id)
|
||||
centers = overview.get("centers") or []
|
||||
|
||||
stage_order = [
|
||||
("institution_initiation_status", "机构立项"),
|
||||
("ethics_status", "伦理审批"),
|
||||
("contract_sign_status", "合同签署"),
|
||||
("startup_status", "启动"),
|
||||
("enrollment_status", "入组"),
|
||||
("inspection_status", "末次稽查"),
|
||||
("closeout_status", "关中心"),
|
||||
]
|
||||
|
||||
summary_list: list[CenterSummaryItem] = []
|
||||
for center in centers:
|
||||
center_id = center.get("center_id")
|
||||
if scope_id_strs is not None and center_id not in scope_id_strs:
|
||||
continue
|
||||
|
||||
stage_label = "未开始"
|
||||
stage_status = "NOT_STARTED"
|
||||
for key, label in stage_order:
|
||||
status = (center.get(key) or "NOT_STARTED").upper()
|
||||
if status in ("IN_PROGRESS", "BLOCKED"):
|
||||
stage_label = label
|
||||
stage_status = status
|
||||
break
|
||||
if status == "COMPLETED":
|
||||
stage_label = label
|
||||
stage_status = status
|
||||
continue
|
||||
|
||||
summary_list.append(
|
||||
CenterSummaryItem(
|
||||
id=center_id,
|
||||
name=center.get("center_name") or "",
|
||||
stage=stage_label,
|
||||
stage_status=stage_status,
|
||||
actual_enrolled=center.get("enrollment_actual") or 0,
|
||||
planned_enrolled=center.get("enrollment_target") or 0,
|
||||
)
|
||||
)
|
||||
|
||||
return summary_list
|
||||
|
||||
@@ -6,13 +6,12 @@ from fastapi.responses import FileResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_current_user, get_db_session
|
||||
from app.crud import document_version as version_crud
|
||||
from app.schemas.acknowledgement import AcknowledgementCreate, AcknowledgementRead
|
||||
from app.schemas.common import PaginatedResponse
|
||||
from app.schemas.distribution import DistributionCreate, DistributionRead
|
||||
from app.schemas.document import DocumentCreate, DocumentDetail, DocumentSummary
|
||||
from app.schemas.document_version import DocumentVersionRead, VersionActionRequest, VersionSubmitRequest
|
||||
from app.schemas.workflow_template import WorkflowTemplateRead
|
||||
from app.schemas.document_version import DocumentVersionRead
|
||||
from app.models.user import UserRole
|
||||
from app.services import document_service
|
||||
from app.utils.pagination import paginate
|
||||
|
||||
@@ -74,6 +73,9 @@ async def delete_document(
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> DocumentSummary:
|
||||
role_value = current_user.role.value if hasattr(current_user.role, "value") else current_user.role
|
||||
if role_value != UserRole.ADMIN.value:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="仅管理员可删除文档")
|
||||
doc = await document_service.delete_document(db, document_id, current_user)
|
||||
return DocumentSummary.model_validate(doc)
|
||||
|
||||
@@ -86,6 +88,7 @@ async def delete_document(
|
||||
async def create_version(
|
||||
document_id: uuid.UUID,
|
||||
version_no: str = Form(...),
|
||||
version_date: str = Form(...),
|
||||
change_summary: Optional[str] = Form(None),
|
||||
parent_version_id: Optional[uuid.UUID] = Form(None),
|
||||
file: UploadFile = File(...),
|
||||
@@ -96,6 +99,7 @@ async def create_version(
|
||||
db,
|
||||
document_id,
|
||||
version_no=version_no,
|
||||
version_date=version_date,
|
||||
change_summary=change_summary,
|
||||
parent_version_id=parent_version_id,
|
||||
file=file,
|
||||
@@ -104,58 +108,6 @@ async def create_version(
|
||||
return DocumentVersionRead.model_validate(version)
|
||||
|
||||
|
||||
@router.post("/versions/{version_id}/submit", response_model=DocumentVersionRead)
|
||||
async def submit_version(
|
||||
version_id: uuid.UUID,
|
||||
payload: VersionSubmitRequest,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> DocumentVersionRead:
|
||||
await document_service.submit_version(db, version_id, payload.template_id, payload.comment, current_user)
|
||||
version = await version_crud.get(db, version_id)
|
||||
if not version:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="版本不存在")
|
||||
return DocumentVersionRead.model_validate(version)
|
||||
|
||||
|
||||
@router.post("/versions/{version_id}/approve", response_model=DocumentVersionRead)
|
||||
async def approve_version(
|
||||
version_id: uuid.UUID,
|
||||
payload: VersionActionRequest,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> DocumentVersionRead:
|
||||
await document_service.approve_version(db, version_id, payload.comment, current_user)
|
||||
version = await version_crud.get(db, version_id)
|
||||
if not version:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="版本不存在")
|
||||
return DocumentVersionRead.model_validate(version)
|
||||
|
||||
|
||||
@router.post("/versions/{version_id}/reject", response_model=DocumentVersionRead)
|
||||
async def reject_version(
|
||||
version_id: uuid.UUID,
|
||||
payload: VersionActionRequest,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> DocumentVersionRead:
|
||||
await document_service.reject_version(db, version_id, payload.comment, current_user)
|
||||
version = await version_crud.get(db, version_id)
|
||||
if not version:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="版本不存在")
|
||||
return DocumentVersionRead.model_validate(version)
|
||||
|
||||
|
||||
@router.post("/versions/{version_id}/make-effective", response_model=DocumentVersionRead)
|
||||
async def make_version_effective(
|
||||
version_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> DocumentVersionRead:
|
||||
version = await document_service.make_version_effective(db, version_id, current_user)
|
||||
return DocumentVersionRead.model_validate(version)
|
||||
|
||||
|
||||
@router.get("/versions/{version_id}/download", response_class=FileResponse)
|
||||
async def download_version(
|
||||
version_id: uuid.UUID,
|
||||
@@ -165,14 +117,16 @@ async def download_version(
|
||||
return await document_service.get_version_download_response(db, version_id, current_user)
|
||||
|
||||
|
||||
@router.get("/workflow-templates", response_model=list[WorkflowTemplateRead])
|
||||
async def list_workflow_templates(
|
||||
trial_id: uuid.UUID | None = None,
|
||||
@router.delete("/versions/{version_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_version(
|
||||
version_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> list[WorkflowTemplateRead]:
|
||||
templates = await document_service.list_workflow_templates(db, trial_id, current_user)
|
||||
return [WorkflowTemplateRead.model_validate(t) for t in templates]
|
||||
) -> None:
|
||||
role_value = current_user.role.value if hasattr(current_user.role, "value") else current_user.role
|
||||
if role_value != UserRole.ADMIN.value:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="仅管理员可删除版本")
|
||||
await document_service.delete_version(db, version_id, current_user)
|
||||
|
||||
|
||||
@router.post(
|
||||
|
||||
@@ -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_current_user, get_db_session, require_study_member, require_study_roles, require_study_not_locked
|
||||
from app.core.deps import get_cra_site_scope, get_current_user, get_db_session, require_study_member, require_study_roles, require_study_not_locked
|
||||
from app.crud import audit as audit_crud
|
||||
from app.crud import drug_shipment as shipment_crud
|
||||
from app.crud import site as site_crud
|
||||
@@ -42,6 +42,9 @@ async def create_shipment(
|
||||
current_user=Depends(get_current_user),
|
||||
) -> DrugShipmentRead:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
if cra_scope and shipment_in.center_id not in cra_scope[0]:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
|
||||
site = await _ensure_center_active(db, study_id, shipment_in.center_id)
|
||||
shipment_in = shipment_in.model_copy(update={"site_name": site.name})
|
||||
shipment = await shipment_crud.create_shipment(db, study_id, shipment_in, created_by=current_user.id)
|
||||
@@ -73,12 +76,18 @@ async def list_shipments(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> list[DrugShipmentRead]:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
center_ids = cra_scope[0] if cra_scope else None
|
||||
if center_id and center_ids is not None and center_id not in center_ids:
|
||||
return []
|
||||
items = await shipment_crud.list_shipments(
|
||||
db,
|
||||
study_id,
|
||||
center_id=center_id,
|
||||
center_ids=center_ids,
|
||||
site_name=site_name,
|
||||
tracking_no=tracking_no,
|
||||
direction=direction,
|
||||
@@ -98,11 +107,15 @@ async def get_shipment(
|
||||
study_id: uuid.UUID,
|
||||
shipment_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> DrugShipmentRead:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
shipment = await shipment_crud.get_shipment(db, shipment_id)
|
||||
if not shipment or shipment.study_id != study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="运输记录不存在")
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
if cra_scope and shipment.center_id not in cra_scope[0]:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
|
||||
return DrugShipmentRead.model_validate(shipment)
|
||||
|
||||
|
||||
@@ -122,6 +135,9 @@ async def update_shipment(
|
||||
shipment = await shipment_crud.get_shipment(db, shipment_id)
|
||||
if not shipment or shipment.study_id != study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="运输记录不存在")
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
if cra_scope and shipment.center_id not in cra_scope[0]:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
|
||||
if shipment_in.center_id:
|
||||
site = await _ensure_center_active(db, study_id, shipment_in.center_id)
|
||||
shipment_in = shipment_in.model_copy(update={"site_name": site.name})
|
||||
@@ -156,6 +172,9 @@ async def delete_shipment(
|
||||
shipment = await shipment_crud.get_shipment(db, shipment_id)
|
||||
if not shipment or shipment.study_id != study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="运输记录不存在")
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
if cra_scope and shipment.center_id not in cra_scope[0]:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
|
||||
await _ensure_center_active(db, study_id, shipment.center_id)
|
||||
await shipment_crud.delete_shipment(db, shipment)
|
||||
await audit_crud.log_action(
|
||||
|
||||
@@ -6,7 +6,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.core.deps import get_current_user, get_db_session, require_study_not_locked
|
||||
from app.core.deps import get_cra_site_scope, get_current_user, get_db_session, require_study_not_locked
|
||||
from app.crud import audit as audit_crud
|
||||
from app.crud import contract_fee as contract_fee_crud
|
||||
from app.crud import contract_fee_payment as payment_crud
|
||||
@@ -84,7 +84,17 @@ async def list_contract_fees(
|
||||
current_user=Depends(get_current_user),
|
||||
) -> FeeApiResponse[list[ContractFeeListItem]]:
|
||||
await _ensure_project_access(db, project_id, current_user, write=False)
|
||||
rows = await contract_fee_crud.list_contract_fees(db, project_id, center_id=center_id, q=q)
|
||||
cra_scope = await get_cra_site_scope(db, project_id, current_user)
|
||||
center_ids = cra_scope[0] if cra_scope else None
|
||||
if center_id and center_ids is not None and center_id not in center_ids:
|
||||
return FeeApiResponse(data=[], meta={"total": 0})
|
||||
rows = await contract_fee_crud.list_contract_fees(
|
||||
db,
|
||||
project_id,
|
||||
center_id=center_id,
|
||||
center_ids=center_ids,
|
||||
q=q,
|
||||
)
|
||||
items: list[ContractFeeListItem] = []
|
||||
for contract, center_name, paid_total, verified_total, last_paid_date, last_verified_date in rows:
|
||||
paid_total_decimal = _to_decimal(paid_total)
|
||||
@@ -130,6 +140,9 @@ async def get_contract_fee(
|
||||
if not contract:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="合同费用不存在")
|
||||
await _ensure_project_access(db, contract.project_id, current_user, write=False)
|
||||
cra_scope = await get_cra_site_scope(db, contract.project_id, current_user)
|
||||
if cra_scope and contract.center_id not in cra_scope[0]:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
|
||||
|
||||
payments = await payment_crud.list_payments(db, contract.id)
|
||||
attachment_types = ["contract_fee_contract", "contract_fee_voucher", "contract_fee_invoice"]
|
||||
|
||||
@@ -4,7 +4,7 @@ from datetime import date
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_current_user, get_db_session, require_study_not_locked
|
||||
from app.core.deps import get_cra_site_scope, get_current_user, get_db_session, require_study_not_locked
|
||||
from app.crud import audit as audit_crud
|
||||
from app.crud import member as member_crud
|
||||
from app.crud import special_expense as special_crud
|
||||
@@ -76,10 +76,15 @@ async def list_special_expenses(
|
||||
) -> FeeApiResponse[list[SpecialExpenseListItem]]:
|
||||
await _ensure_project_access(db, project_id, current_user, write=False)
|
||||
_validate_category(category)
|
||||
cra_scope = await get_cra_site_scope(db, project_id, current_user)
|
||||
center_ids = cra_scope[0] if cra_scope else None
|
||||
if center_id and center_ids is not None and center_id not in center_ids:
|
||||
return FeeApiResponse(data=[], meta={"total": 0})
|
||||
rows = await special_crud.list_special_expenses(
|
||||
db,
|
||||
project_id,
|
||||
center_id=center_id,
|
||||
center_ids=center_ids,
|
||||
category=category,
|
||||
date_from=date_from,
|
||||
date_to=date_to,
|
||||
@@ -123,6 +128,9 @@ async def get_special_expense(
|
||||
if not expense:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="特殊费用不存在")
|
||||
await _ensure_project_access(db, expense.project_id, current_user, write=False)
|
||||
cra_scope = await get_cra_site_scope(db, expense.project_id, current_user)
|
||||
if cra_scope and expense.center_id not in cra_scope[0]:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
|
||||
return FeeApiResponse(data=SpecialExpenseRead.model_validate(expense))
|
||||
|
||||
|
||||
|
||||
@@ -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_current_user, get_db_session, require_study_member, require_study_roles, require_study_not_locked
|
||||
from app.core.deps import get_cra_site_scope, get_current_user, get_db_session, require_study_member, require_study_roles, require_study_not_locked
|
||||
from app.crud import audit as audit_crud
|
||||
from app.crud import finance_contract as contract_crud
|
||||
from app.crud import site as site_crud
|
||||
@@ -41,6 +41,9 @@ async def create_contract(
|
||||
current_user=Depends(get_current_user),
|
||||
) -> FinanceContractRead:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
if cra_scope and contract_in.site_name not in cra_scope[1]:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
|
||||
await _ensure_site_name_active(db, study_id, contract_in.site_name)
|
||||
contract = await contract_crud.create_contract(db, study_id, contract_in, created_by=current_user.id)
|
||||
await audit_crud.log_action(
|
||||
@@ -68,9 +71,22 @@ async def list_contracts(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> list[FinanceContractRead]:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
items = await contract_crud.list_contracts(db, study_id, site_name=site_name, contract_no=contract_no, skip=skip, limit=limit)
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
site_names = cra_scope[1] if cra_scope else None
|
||||
if site_name and site_names is not None and site_name not in site_names:
|
||||
return []
|
||||
items = await contract_crud.list_contracts(
|
||||
db,
|
||||
study_id,
|
||||
site_name=site_name,
|
||||
site_names=site_names,
|
||||
contract_no=contract_no,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
)
|
||||
return [FinanceContractRead.model_validate(item) for item in items]
|
||||
|
||||
|
||||
@@ -83,11 +99,15 @@ async def get_contract(
|
||||
study_id: uuid.UUID,
|
||||
contract_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> FinanceContractRead:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
contract = await contract_crud.get_contract(db, contract_id)
|
||||
if not contract or contract.study_id != study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="合同不存在")
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
if cra_scope and contract.site_name not in cra_scope[1]:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
|
||||
return FinanceContractRead.model_validate(contract)
|
||||
|
||||
|
||||
@@ -107,6 +127,9 @@ async def update_contract(
|
||||
contract = await contract_crud.get_contract(db, contract_id)
|
||||
if not contract or contract.study_id != study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="合同不存在")
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
if cra_scope and contract.site_name not in cra_scope[1]:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
|
||||
await _ensure_site_name_active(db, study_id, contract.site_name)
|
||||
contract = await contract_crud.update_contract(db, contract, contract_in)
|
||||
await audit_crud.log_action(
|
||||
@@ -137,6 +160,9 @@ async def delete_contract(
|
||||
contract = await contract_crud.get_contract(db, contract_id)
|
||||
if not contract or contract.study_id != study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="合同不存在")
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
if cra_scope and contract.site_name not in cra_scope[1]:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
|
||||
await _ensure_site_name_active(db, study_id, contract.site_name)
|
||||
await contract_crud.delete_contract(db, contract)
|
||||
await audit_crud.log_action(
|
||||
|
||||
@@ -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_current_user, get_db_session, require_study_member, require_study_roles, require_study_not_locked
|
||||
from app.core.deps import get_cra_site_scope, get_current_user, get_db_session, require_study_member, require_study_roles, require_study_not_locked
|
||||
from app.crud import audit as audit_crud
|
||||
from app.crud import finance_special as special_crud
|
||||
from app.crud import site as site_crud
|
||||
@@ -41,6 +41,9 @@ async def create_special(
|
||||
current_user=Depends(get_current_user),
|
||||
) -> FinanceSpecialRead:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
if cra_scope and special_in.site_name not in cra_scope[1]:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
|
||||
await _ensure_site_name_active(db, study_id, special_in.site_name)
|
||||
item = await special_crud.create_special(db, study_id, special_in, created_by=current_user.id)
|
||||
await audit_crud.log_action(
|
||||
@@ -68,9 +71,22 @@ async def list_specials(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> list[FinanceSpecialRead]:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
items = await special_crud.list_specials(db, study_id, site_name=site_name, fee_type=fee_type, skip=skip, limit=limit)
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
site_names = cra_scope[1] if cra_scope else None
|
||||
if site_name and site_names is not None and site_name not in site_names:
|
||||
return []
|
||||
items = await special_crud.list_specials(
|
||||
db,
|
||||
study_id,
|
||||
site_name=site_name,
|
||||
site_names=site_names,
|
||||
fee_type=fee_type,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
)
|
||||
return [FinanceSpecialRead.model_validate(item) for item in items]
|
||||
|
||||
|
||||
@@ -83,11 +99,15 @@ async def get_special(
|
||||
study_id: uuid.UUID,
|
||||
special_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> FinanceSpecialRead:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
item = await special_crud.get_special(db, special_id)
|
||||
if not item or item.study_id != study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="特殊费用不存在")
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
if cra_scope and item.site_name not in cra_scope[1]:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
|
||||
return FinanceSpecialRead.model_validate(item)
|
||||
|
||||
|
||||
@@ -107,6 +127,9 @@ async def update_special(
|
||||
item = await special_crud.get_special(db, special_id)
|
||||
if not item or item.study_id != study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="特殊费用不存在")
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
if cra_scope and item.site_name not in cra_scope[1]:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
|
||||
await _ensure_site_name_active(db, study_id, item.site_name)
|
||||
item = await special_crud.update_special(db, item, special_in)
|
||||
await audit_crud.log_action(
|
||||
@@ -137,6 +160,9 @@ async def delete_special(
|
||||
item = await special_crud.get_special(db, special_id)
|
||||
if not item or item.study_id != study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="特殊费用不存在")
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
if cra_scope and item.site_name not in cra_scope[1]:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
|
||||
await _ensure_site_name_active(db, study_id, item.site_name)
|
||||
await special_crud.delete_special(db, item)
|
||||
await audit_crud.log_action(
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_db_session, get_current_user, require_study_member
|
||||
from app.schemas.notification import NotificationItem
|
||||
from app.services import document_service
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get(
|
||||
"/notifications",
|
||||
response_model=list[NotificationItem],
|
||||
dependencies=[Depends(require_study_member())],
|
||||
)
|
||||
async def list_notifications(
|
||||
study_id: uuid.UUID,
|
||||
skip: int = 0,
|
||||
limit: int = 20,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> list[NotificationItem]:
|
||||
return await document_service.list_distribution_notifications(
|
||||
db,
|
||||
study_id,
|
||||
current_user,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
)
|
||||
@@ -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, finance_specials, fees_contracts, fees_specials, fees_attachments, drug_shipments, startup, knowledge_notes, subject_histories, faq_categories, faqs, documents, overview
|
||||
from app.api.v1 import auth, users, admin_users, studies, sites, members, attachments, audit_logs, dashboard, subjects, visits, aes, finance_dashboard, finance_contracts, finance_specials, fees_contracts, fees_specials, fees_attachments, drug_shipments, startup, knowledge_notes, subject_histories, faq_categories, faqs, documents, overview, notifications
|
||||
|
||||
|
||||
api_router = APIRouter()
|
||||
@@ -9,6 +9,7 @@ api_router.include_router(admin_users.router, prefix="/admin", tags=["admin"])
|
||||
api_router.include_router(users.router, prefix="/users", tags=["users"])
|
||||
api_router.include_router(studies.router, prefix="/studies", tags=["studies"])
|
||||
api_router.include_router(overview.router, prefix="/studies/{study_id}", tags=["overview"])
|
||||
api_router.include_router(notifications.router, prefix="/studies/{study_id}", tags=["notifications"])
|
||||
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"])
|
||||
api_router.include_router(attachments.router, prefix="/studies/{study_id}/{entity_type}/{entity_id}/attachments", tags=["attachments"])
|
||||
|
||||
@@ -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_current_user, get_db_session, require_study_member, require_study_roles, require_study_not_locked
|
||||
from app.core.deps import get_cra_site_scope, get_current_user, get_db_session, require_study_member, require_study_roles, require_study_not_locked
|
||||
from app.crud import site as site_crud
|
||||
from app.crud import startup as startup_crud
|
||||
from app.crud import study as study_crud
|
||||
@@ -60,14 +60,18 @@ async def list_sites(
|
||||
limit: int = 100,
|
||||
include_inactive: bool = False,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> list[SiteRead]:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
site_ids = cra_scope[0] if cra_scope else None
|
||||
sites = await site_crud.list_by_study(
|
||||
db,
|
||||
study_id,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
include_inactive=include_inactive,
|
||||
site_ids=site_ids,
|
||||
)
|
||||
return list(sites)
|
||||
|
||||
|
||||
@@ -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_current_user, get_db_session, require_study_member, require_study_roles, require_study_not_locked
|
||||
from app.core.deps import get_cra_site_scope, get_current_user, get_db_session, require_study_member
|
||||
from app.crud import audit as audit_crud
|
||||
from app.crud import site as site_crud
|
||||
from app.crud import startup as startup_crud
|
||||
@@ -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_study_roles(["PM", "CRA"]))],
|
||||
dependencies=[Depends(require_study_member())],
|
||||
)
|
||||
async def create_feasibility(
|
||||
study_id: uuid.UUID,
|
||||
@@ -62,6 +62,9 @@ async def create_feasibility(
|
||||
current_user=Depends(get_current_user),
|
||||
) -> StartupFeasibilityRead:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
if cra_scope and record_in.site_id not in cra_scope[0]:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
|
||||
await _ensure_site_active(db, record_in.site_id)
|
||||
record = await startup_crud.create_feasibility(db, study_id, record_in, created_by=current_user.id)
|
||||
await audit_crud.log_action(
|
||||
@@ -87,9 +90,12 @@ async def list_feasibilities(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> list[StartupFeasibilityRead]:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
items = await startup_crud.list_feasibilities(db, study_id, skip=skip, limit=limit)
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
site_ids = cra_scope[0] if cra_scope else None
|
||||
items = await startup_crud.list_feasibilities(db, study_id, skip=skip, limit=limit, site_ids=site_ids)
|
||||
return [StartupFeasibilityRead.model_validate(item) for item in items]
|
||||
|
||||
|
||||
@@ -102,18 +108,22 @@ async def get_feasibility(
|
||||
study_id: uuid.UUID,
|
||||
record_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> StartupFeasibilityRead:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
record = await startup_crud.get_feasibility(db, record_id)
|
||||
if not record or record.study_id != study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="立项记录不存在")
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
if cra_scope and record.site_id not in cra_scope[0]:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
|
||||
return StartupFeasibilityRead.model_validate(record)
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/feasibility/{record_id}",
|
||||
response_model=StartupFeasibilityRead,
|
||||
dependencies=[Depends(require_study_roles(["PM", "CRA"]))],
|
||||
dependencies=[Depends(require_study_member())],
|
||||
)
|
||||
async def update_feasibility(
|
||||
study_id: uuid.UUID,
|
||||
@@ -126,6 +136,9 @@ async def update_feasibility(
|
||||
record = await startup_crud.get_feasibility(db, record_id)
|
||||
if not record or record.study_id != study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="立项记录不存在")
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
if cra_scope and record.site_id not in cra_scope[0]:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
|
||||
await _ensure_site_active(db, record.site_id)
|
||||
record = await startup_crud.update_feasibility(db, record, record_in)
|
||||
await audit_crud.log_action(
|
||||
@@ -144,7 +157,7 @@ async def update_feasibility(
|
||||
@router.delete(
|
||||
"/feasibility/{record_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(require_study_roles(["PM", "CRA"]))],
|
||||
dependencies=[Depends(require_study_member())],
|
||||
)
|
||||
async def delete_feasibility(
|
||||
study_id: uuid.UUID,
|
||||
@@ -156,6 +169,9 @@ async def delete_feasibility(
|
||||
record = await startup_crud.get_feasibility(db, record_id)
|
||||
if not record or record.study_id != study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="立项记录不存在")
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
if cra_scope and record.site_id not in cra_scope[0]:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
|
||||
await _ensure_site_active(db, record.site_id)
|
||||
await startup_crud.delete_feasibility(db, record)
|
||||
await audit_crud.log_action(
|
||||
@@ -174,7 +190,7 @@ async def delete_feasibility(
|
||||
"/ethics",
|
||||
response_model=StartupEthicsRead,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
dependencies=[Depends(require_study_roles(["PM", "CRA"]))],
|
||||
dependencies=[Depends(require_study_member())],
|
||||
)
|
||||
async def create_ethics(
|
||||
study_id: uuid.UUID,
|
||||
@@ -183,6 +199,9 @@ async def create_ethics(
|
||||
current_user=Depends(get_current_user),
|
||||
) -> StartupEthicsRead:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
if cra_scope and record_in.site_id not in cra_scope[0]:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
|
||||
await _ensure_site_active(db, record_in.site_id)
|
||||
record = await startup_crud.create_ethics(db, study_id, record_in, created_by=current_user.id)
|
||||
await audit_crud.log_action(
|
||||
@@ -208,9 +227,12 @@ async def list_ethics(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> list[StartupEthicsRead]:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
items = await startup_crud.list_ethics(db, study_id, skip=skip, limit=limit)
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
site_ids = cra_scope[0] if cra_scope else None
|
||||
items = await startup_crud.list_ethics(db, study_id, skip=skip, limit=limit, site_ids=site_ids)
|
||||
return [StartupEthicsRead.model_validate(item) for item in items]
|
||||
|
||||
|
||||
@@ -223,18 +245,22 @@ async def get_ethics(
|
||||
study_id: uuid.UUID,
|
||||
record_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> StartupEthicsRead:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
record = await startup_crud.get_ethics(db, record_id)
|
||||
if not record or record.study_id != study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="伦理记录不存在")
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
if cra_scope and record.site_id not in cra_scope[0]:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
|
||||
return StartupEthicsRead.model_validate(record)
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/ethics/{record_id}",
|
||||
response_model=StartupEthicsRead,
|
||||
dependencies=[Depends(require_study_roles(["PM", "CRA"]))],
|
||||
dependencies=[Depends(require_study_member())],
|
||||
)
|
||||
async def update_ethics(
|
||||
study_id: uuid.UUID,
|
||||
@@ -247,6 +273,9 @@ async def update_ethics(
|
||||
record = await startup_crud.get_ethics(db, record_id)
|
||||
if not record or record.study_id != study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="伦理记录不存在")
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
if cra_scope and record.site_id not in cra_scope[0]:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
|
||||
await _ensure_site_active(db, record.site_id)
|
||||
record = await startup_crud.update_ethics(db, record, record_in)
|
||||
await audit_crud.log_action(
|
||||
@@ -265,7 +294,7 @@ async def update_ethics(
|
||||
@router.delete(
|
||||
"/ethics/{record_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(require_study_roles(["PM", "CRA"]))],
|
||||
dependencies=[Depends(require_study_member())],
|
||||
)
|
||||
async def delete_ethics(
|
||||
study_id: uuid.UUID,
|
||||
@@ -277,6 +306,9 @@ async def delete_ethics(
|
||||
record = await startup_crud.get_ethics(db, record_id)
|
||||
if not record or record.study_id != study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="伦理记录不存在")
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
if cra_scope and record.site_id not in cra_scope[0]:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
|
||||
await _ensure_site_active(db, record.site_id)
|
||||
await startup_crud.delete_ethics(db, record)
|
||||
await audit_crud.log_action(
|
||||
@@ -295,7 +327,7 @@ async def delete_ethics(
|
||||
"/kickoff",
|
||||
response_model=KickoffMeetingRead,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
dependencies=[Depends(require_study_roles(["PM", "CRA"]))],
|
||||
dependencies=[Depends(require_study_member())],
|
||||
)
|
||||
async def create_kickoff(
|
||||
study_id: uuid.UUID,
|
||||
@@ -304,6 +336,9 @@ async def create_kickoff(
|
||||
current_user=Depends(get_current_user),
|
||||
) -> KickoffMeetingRead:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
if cra_scope and meeting_in.site_id not in cra_scope[0]:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
|
||||
await _ensure_site_active(db, meeting_in.site_id)
|
||||
meeting = await startup_crud.create_kickoff(db, study_id, meeting_in, created_by=current_user.id)
|
||||
await audit_crud.log_action(
|
||||
@@ -329,9 +364,12 @@ async def list_kickoffs(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> list[KickoffMeetingRead]:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
items = await startup_crud.list_kickoffs(db, study_id, skip=skip, limit=limit)
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
site_ids = cra_scope[0] if cra_scope else None
|
||||
items = await startup_crud.list_kickoffs(db, study_id, skip=skip, limit=limit, site_ids=site_ids)
|
||||
return [KickoffMeetingRead.model_validate(item) for item in items]
|
||||
|
||||
|
||||
@@ -344,18 +382,22 @@ async def get_kickoff(
|
||||
study_id: uuid.UUID,
|
||||
meeting_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> KickoffMeetingRead:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
meeting = await startup_crud.get_kickoff(db, meeting_id)
|
||||
if not meeting or meeting.study_id != study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="启动会记录不存在")
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
if cra_scope and meeting.site_id not in cra_scope[0]:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
|
||||
return KickoffMeetingRead.model_validate(meeting)
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/kickoff/{meeting_id}",
|
||||
response_model=KickoffMeetingRead,
|
||||
dependencies=[Depends(require_study_roles(["PM", "CRA"]))],
|
||||
dependencies=[Depends(require_study_member())],
|
||||
)
|
||||
async def update_kickoff(
|
||||
study_id: uuid.UUID,
|
||||
@@ -368,6 +410,9 @@ async def update_kickoff(
|
||||
meeting = await startup_crud.get_kickoff(db, meeting_id)
|
||||
if not meeting or meeting.study_id != study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="启动会记录不存在")
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
if cra_scope and meeting.site_id not in cra_scope[0]:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
|
||||
await _ensure_site_active(db, meeting.site_id)
|
||||
meeting = await startup_crud.update_kickoff(db, meeting, meeting_in)
|
||||
await audit_crud.log_action(
|
||||
@@ -387,7 +432,7 @@ async def update_kickoff(
|
||||
"/training-authorizations",
|
||||
response_model=TrainingAuthorizationRead,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
dependencies=[Depends(require_study_roles(["PM", "CRA"]))],
|
||||
dependencies=[Depends(require_study_member())],
|
||||
)
|
||||
async def create_training_authorization(
|
||||
study_id: uuid.UUID,
|
||||
@@ -396,6 +441,9 @@ async def create_training_authorization(
|
||||
current_user=Depends(get_current_user),
|
||||
) -> TrainingAuthorizationRead:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
if cra_scope and record_in.site_name not in cra_scope[1]:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
|
||||
await _ensure_site_name_active(db, study_id, record_in.site_name)
|
||||
record = await startup_crud.create_training_authorization(db, study_id, record_in, created_by=current_user.id)
|
||||
await audit_crud.log_action(
|
||||
@@ -421,9 +469,12 @@ async def list_training_authorizations(
|
||||
skip: int = 0,
|
||||
limit: int = 200,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> list[TrainingAuthorizationRead]:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
items = await startup_crud.list_training_authorizations(db, study_id, skip=skip, limit=limit)
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
site_names = cra_scope[1] if cra_scope else None
|
||||
items = await startup_crud.list_training_authorizations(db, study_id, skip=skip, limit=limit, site_names=site_names)
|
||||
return [TrainingAuthorizationRead.model_validate(item) for item in items]
|
||||
|
||||
|
||||
@@ -436,18 +487,22 @@ async def get_training_authorization(
|
||||
study_id: uuid.UUID,
|
||||
record_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> TrainingAuthorizationRead:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
record = await startup_crud.get_training_authorization(db, record_id)
|
||||
if not record or record.study_id != study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="培训授权人员不存在")
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
if cra_scope and record.site_name not in cra_scope[1]:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
|
||||
return TrainingAuthorizationRead.model_validate(record)
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/training-authorizations/{record_id}",
|
||||
response_model=TrainingAuthorizationRead,
|
||||
dependencies=[Depends(require_study_roles(["PM", "CRA"]))],
|
||||
dependencies=[Depends(require_study_member())],
|
||||
)
|
||||
async def update_training_authorization(
|
||||
study_id: uuid.UUID,
|
||||
@@ -460,6 +515,9 @@ async def update_training_authorization(
|
||||
record = await startup_crud.get_training_authorization(db, record_id)
|
||||
if not record or record.study_id != study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="培训授权人员不存在")
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
if cra_scope and record.site_name not in cra_scope[1]:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
|
||||
await _ensure_site_name_active(db, study_id, record.site_name)
|
||||
record = await startup_crud.update_training_authorization(db, record, record_in)
|
||||
await audit_crud.log_action(
|
||||
@@ -478,7 +536,7 @@ async def update_training_authorization(
|
||||
@router.delete(
|
||||
"/training-authorizations/{record_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(require_study_roles(["PM", "CRA"]))],
|
||||
dependencies=[Depends(require_study_member())],
|
||||
)
|
||||
async def delete_training_authorization(
|
||||
study_id: uuid.UUID,
|
||||
@@ -490,6 +548,9 @@ async def delete_training_authorization(
|
||||
record = await startup_crud.get_training_authorization(db, record_id)
|
||||
if not record or record.study_id != study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="培训授权人员不存在")
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
if cra_scope and record.site_name not in cra_scope[1]:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
|
||||
await _ensure_site_name_active(db, study_id, record.site_name)
|
||||
await startup_crud.delete_training_authorization(db, record)
|
||||
await audit_crud.log_action(
|
||||
|
||||
@@ -84,6 +84,11 @@ async def update_study(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="项目已锁定,无法编辑。请先解锁项目。"
|
||||
)
|
||||
|
||||
if study_in.code and study_in.code != study.code:
|
||||
existing = await study_crud.get_by_code(db, study_in.code)
|
||||
if existing and existing.id != study.id:
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="项目编号已存在")
|
||||
|
||||
updated = await study_crud.update(db, study, study_in)
|
||||
return updated
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_current_user, get_db_session, require_study_member, require_study_roles, require_study_not_locked
|
||||
from app.core.deps import get_cra_site_scope, get_current_user, get_db_session, require_study_member, require_study_roles, require_study_not_locked
|
||||
from app.crud import audit as audit_crud
|
||||
from app.crud import site as site_crud
|
||||
from app.crud import subject as subject_crud
|
||||
from app.crud import study as study_crud
|
||||
from app.models.ae import AdverseEvent
|
||||
from app.schemas.subject import SubjectCreate, SubjectRead, SubjectUpdate
|
||||
|
||||
router = APIRouter()
|
||||
@@ -41,6 +43,9 @@ async def create_subject(
|
||||
current_user=Depends(get_current_user),
|
||||
) -> SubjectRead:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
if cra_scope and subject_in.site_id not in cra_scope[0]:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
|
||||
try:
|
||||
subject = await subject_crud.create_subject(db, study_id, subject_in)
|
||||
except ValueError as exc:
|
||||
@@ -67,10 +72,31 @@ async def list_subjects(
|
||||
study_id: uuid.UUID,
|
||||
site_id: uuid.UUID | None = None,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> list[SubjectRead]:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
subjects = await subject_crud.list_subjects(db, study_id, site_id=site_id)
|
||||
return list(subjects)
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
site_ids = cra_scope[0] if cra_scope else None
|
||||
if site_id and site_ids is not None and site_id not in site_ids:
|
||||
return []
|
||||
subjects = await subject_crud.list_subjects(db, study_id, site_id=site_id, site_ids=site_ids)
|
||||
subject_list = list(subjects)
|
||||
if not subject_list:
|
||||
return []
|
||||
subject_ids = [subject.id for subject in subject_list]
|
||||
ae_rows = await db.execute(
|
||||
select(AdverseEvent.subject_id).where(
|
||||
AdverseEvent.study_id == study_id,
|
||||
AdverseEvent.subject_id.in_(subject_ids),
|
||||
)
|
||||
)
|
||||
ae_subject_ids = {row[0] for row in ae_rows.all() if row[0]}
|
||||
result: list[SubjectRead] = []
|
||||
for subject in subject_list:
|
||||
data = SubjectRead.model_validate(subject)
|
||||
data.has_ae = subject.id in ae_subject_ids
|
||||
result.append(data)
|
||||
return result
|
||||
|
||||
|
||||
@router.get(
|
||||
@@ -82,13 +108,25 @@ async def get_subject(
|
||||
study_id: uuid.UUID,
|
||||
subject_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> SubjectRead:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
subject = await subject_crud.get_subject(db, subject_id)
|
||||
if not subject or subject.study_id != study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="参与者不存在")
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
if cra_scope and subject.site_id not in cra_scope[0]:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
|
||||
await _ensure_subject_active(db, subject)
|
||||
return subject
|
||||
data = SubjectRead.model_validate(subject)
|
||||
ae_row = await db.execute(
|
||||
select(AdverseEvent.id).where(
|
||||
AdverseEvent.study_id == study_id,
|
||||
AdverseEvent.subject_id == subject.id,
|
||||
).limit(1)
|
||||
)
|
||||
data.has_ae = ae_row.scalar_one_or_none() is not None
|
||||
return data
|
||||
|
||||
|
||||
@router.patch(
|
||||
@@ -107,6 +145,9 @@ async def update_subject(
|
||||
subject = await subject_crud.get_subject(db, subject_id)
|
||||
if not subject or subject.study_id != study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="参与者不存在")
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
if cra_scope and subject.site_id not in cra_scope[0]:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
|
||||
await _ensure_subject_active(db, subject)
|
||||
old_status = subject.status
|
||||
updated = await subject_crud.update_subject(db, subject, subject_in)
|
||||
@@ -144,6 +185,9 @@ async def delete_subject(
|
||||
subject = await subject_crud.get_subject(db, subject_id)
|
||||
if not subject or subject.study_id != study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="参与者不存在")
|
||||
cra_scope = await get_cra_site_scope(db, study_id, current_user)
|
||||
if cra_scope and subject.site_id not in cra_scope[0]:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足")
|
||||
await subject_crud.delete_subject(db, subject)
|
||||
await audit_crud.log_action(
|
||||
db,
|
||||
|
||||
@@ -40,6 +40,7 @@ async def list_visits(
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> list[VisitRead]:
|
||||
await _ensure_subject(db, study_id, subject_id)
|
||||
await visit_crud.mark_overdue_as_lost(db, subject_id)
|
||||
visits = await visit_crud.list_visits(db, subject_id)
|
||||
return list(visits)
|
||||
|
||||
@@ -61,15 +62,18 @@ async def create_visit(
|
||||
await _ensure_subject_active(db, subject)
|
||||
if visit_in.subject_id != subject_id:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="参与者不匹配")
|
||||
next_visit_code = await visit_crud.get_next_visit_code(db, subject_id)
|
||||
if next_visit_code == "V1" and not visit_in.planned_date:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="首次新增访视必须填写计划访视日期")
|
||||
visit = await visit_crud.create_visit(
|
||||
db,
|
||||
study_id=study_id,
|
||||
visit_in=visit_in,
|
||||
subject=subject,
|
||||
visit_code=visit_in.visit_code,
|
||||
visit_code=next_visit_code,
|
||||
planned_date=visit_in.planned_date,
|
||||
)
|
||||
if visit_in.visit_code == "V1" and visit_in.planned_date:
|
||||
if next_visit_code == "V1" and visit_in.planned_date:
|
||||
study = await study_crud.get(db, study_id)
|
||||
if study:
|
||||
await visit_crud.create_followup_visits(
|
||||
|
||||
Reference in New Issue
Block a user