迁移合同费用通用附件
1、删除旧费用附件 API、CRUD、模型和专用前端面板,统一改用通用附件能力。 2、调整合同费用接口为 study_id 语义,并补充合同、凭证、发票等附件类型的权限映射。 3、将合同费用新建和编辑改为抽屉流程,详情页与列表页复用同一编辑组件。 4、补充数据库迁移、附件列表、合同费用抽屉和权限场景测试,覆盖旧权限移除与新流程。
This commit is contained in:
@@ -15,6 +15,7 @@ from app.crud import audit as audit_crud
|
||||
from app.crud import contract_fee as contract_fee_crud
|
||||
from app.crud import drug_shipment as drug_shipment_crud
|
||||
from app.crud import faq_reply as faq_reply_crud
|
||||
from app.crud import material_equipment as material_equipment_crud
|
||||
from app.crud import precaution as precaution_crud
|
||||
from app.crud import study as study_crud
|
||||
from app.crud import startup as startup_crud
|
||||
@@ -33,6 +34,49 @@ FEE_ATTACHMENT_ENTITY_TYPES = {
|
||||
"contract_fee_voucher",
|
||||
"contract_fee_invoice",
|
||||
}
|
||||
FEE_ATTACHMENT_PERMISSION_BY_ACTION = {
|
||||
"create": "fees_contracts:create",
|
||||
"read": "fees_contracts:read",
|
||||
"delete": "fees_contracts_attachments:delete",
|
||||
}
|
||||
STARTUP_INITIATION_ATTACHMENT_PERMISSION_BY_ACTION = {
|
||||
"create": "startup_initiation:create",
|
||||
"read": "startup_initiation:read",
|
||||
"delete": "startup_initiation_attachments:delete",
|
||||
}
|
||||
STARTUP_ETHICS_ATTACHMENT_PERMISSION_BY_ACTION = {
|
||||
"create": "startup_ethics:create",
|
||||
"read": "startup_ethics:read",
|
||||
"delete": "startup_ethics_attachments:delete",
|
||||
}
|
||||
STARTUP_AUTH_ATTACHMENT_PERMISSION_BY_ACTION = {
|
||||
"create": "startup_auth:create",
|
||||
"read": "startup_auth:read",
|
||||
"delete": "startup_auth_attachments:delete",
|
||||
}
|
||||
DRUG_SHIPMENT_ATTACHMENT_PERMISSION_BY_ACTION = {
|
||||
"create": "drug_shipments:create",
|
||||
"read": "drug_shipments:read",
|
||||
"delete": "drug_shipments_attachments:delete",
|
||||
}
|
||||
MATERIAL_EQUIPMENT_ATTACHMENT_ENTITY_TYPES = {
|
||||
"material_equipment",
|
||||
}
|
||||
MATERIAL_EQUIPMENT_ATTACHMENT_PERMISSION_BY_ACTION = {
|
||||
"create": "material_equipments:update",
|
||||
"read": "material_equipments:read",
|
||||
"delete": "material_equipments_attachments:delete",
|
||||
}
|
||||
PRECAUTION_ATTACHMENT_PERMISSION_BY_ACTION = {
|
||||
"create": "precautions:create",
|
||||
"read": "precautions:read",
|
||||
"delete": "precautions_attachments:delete",
|
||||
}
|
||||
FAQ_REPLY_ATTACHMENT_PERMISSION_BY_ACTION = {
|
||||
"create": "faq_reply:create",
|
||||
"read": "faq:read",
|
||||
"delete": "faq_attachments:delete",
|
||||
}
|
||||
STARTUP_AUTH_ATTACHMENT_ENTITY_TYPES = {
|
||||
"startup_kickoff",
|
||||
"startup_kickoff_minutes",
|
||||
@@ -41,23 +85,6 @@ STARTUP_AUTH_ATTACHMENT_ENTITY_TYPES = {
|
||||
"training_authorization",
|
||||
}
|
||||
|
||||
ATTACHMENT_PERMISSION_MODULES = {
|
||||
"contract_fee_contract": "fees_contracts_attachments",
|
||||
"contract_fee_voucher": "fees_contracts_attachments",
|
||||
"contract_fee_invoice": "fees_contracts_attachments",
|
||||
"startup_feasibility": "startup_initiation_attachments",
|
||||
"startup_ethics": "startup_ethics_attachments",
|
||||
"startup_kickoff": "startup_auth_attachments",
|
||||
"startup_kickoff_minutes": "startup_auth_attachments",
|
||||
"startup_kickoff_signin": "startup_auth_attachments",
|
||||
"startup_kickoff_ppt": "startup_auth_attachments",
|
||||
"training_authorization": "startup_auth_attachments",
|
||||
"drug_shipment": "drug_shipments_attachments",
|
||||
"precaution": "precautions_attachments",
|
||||
"faq_replies": "faq_attachments",
|
||||
}
|
||||
|
||||
|
||||
def _content_disposition(filename: str, disposition: str = "inline") -> str:
|
||||
fallback = "".join((ch if ord(ch) < 128 else "_") for ch in filename) or "download"
|
||||
quoted = quote(filename, safe="")
|
||||
@@ -84,25 +111,25 @@ async def _resolve_attachment_parent_permission(
|
||||
entity_id: uuid.UUID,
|
||||
action: str,
|
||||
) -> str | None:
|
||||
parent_action = "read" if action == "read" else "update"
|
||||
operation = _attachment_operation(action)
|
||||
|
||||
if entity_type in FEE_ATTACHMENT_ENTITY_TYPES:
|
||||
contract = await contract_fee_crud.get_contract_fee(db, entity_id)
|
||||
if not contract or contract.project_id != study_id:
|
||||
if not contract or contract.study_id != study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="合同费用不存在")
|
||||
return "fees_contracts:read" if action == "read" else "fees_contracts:update"
|
||||
return FEE_ATTACHMENT_PERMISSION_BY_ACTION[operation]
|
||||
|
||||
if entity_type == "startup_feasibility":
|
||||
record = await startup_crud.get_feasibility(db, entity_id)
|
||||
if not record or record.study_id != study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="立项记录不存在")
|
||||
return f"startup_initiation:{parent_action}"
|
||||
return STARTUP_INITIATION_ATTACHMENT_PERMISSION_BY_ACTION[operation]
|
||||
|
||||
if entity_type == "startup_ethics":
|
||||
record = await startup_crud.get_ethics(db, entity_id)
|
||||
if not record or record.study_id != study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="伦理记录不存在")
|
||||
return f"startup_ethics:{parent_action}"
|
||||
return STARTUP_ETHICS_ATTACHMENT_PERMISSION_BY_ACTION[operation]
|
||||
|
||||
if entity_type in STARTUP_AUTH_ATTACHMENT_ENTITY_TYPES:
|
||||
if entity_type == "training_authorization":
|
||||
@@ -113,29 +140,31 @@ async def _resolve_attachment_parent_permission(
|
||||
missing_detail = "启动会不存在"
|
||||
if not record or record.study_id != study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=missing_detail)
|
||||
return "startup_auth:read" if action == "read" else "startup_auth:update"
|
||||
return STARTUP_AUTH_ATTACHMENT_PERMISSION_BY_ACTION[operation]
|
||||
|
||||
if entity_type == "drug_shipment":
|
||||
shipment = await drug_shipment_crud.get_shipment(db, entity_id)
|
||||
if not shipment or shipment.study_id != study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="药物发货不存在")
|
||||
return "drug_shipments:read" if action == "read" else "drug_shipments:update"
|
||||
return DRUG_SHIPMENT_ATTACHMENT_PERMISSION_BY_ACTION[operation]
|
||||
|
||||
if entity_type in MATERIAL_EQUIPMENT_ATTACHMENT_ENTITY_TYPES:
|
||||
equipment = await material_equipment_crud.get_equipment(db, entity_id)
|
||||
if not equipment or equipment.study_id != study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="设备记录不存在")
|
||||
return MATERIAL_EQUIPMENT_ATTACHMENT_PERMISSION_BY_ACTION[operation]
|
||||
|
||||
if entity_type == "precaution":
|
||||
precaution = await precaution_crud.get_precaution(db, entity_id)
|
||||
if not precaution or precaution.study_id != study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="注意事项不存在")
|
||||
return "precautions:read" if action == "read" else "precautions:update"
|
||||
return PRECAUTION_ATTACHMENT_PERMISSION_BY_ACTION[operation]
|
||||
|
||||
if entity_type == "faq_replies":
|
||||
reply = await faq_reply_crud.get_reply(db, entity_id)
|
||||
if not reply or reply.study_id != study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ回复不存在")
|
||||
if action == "read":
|
||||
return "faq:read"
|
||||
if action == "delete":
|
||||
return "faq_reply:delete"
|
||||
return "faq_reply:create"
|
||||
return FAQ_REPLY_ATTACHMENT_PERMISSION_BY_ACTION[operation]
|
||||
|
||||
return None
|
||||
|
||||
@@ -158,23 +187,8 @@ async def _ensure_attachment_permission(
|
||||
if not membership or not membership.is_active:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="不是项目成员")
|
||||
|
||||
operation = _attachment_operation(action)
|
||||
module_prefix = ATTACHMENT_PERMISSION_MODULES.get(entity_type)
|
||||
if not module_prefix:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="附件类型未配置权限")
|
||||
|
||||
attachment_allowed = await role_has_api_permission(
|
||||
db,
|
||||
study_id,
|
||||
membership.role_in_study,
|
||||
f"{module_prefix}:{operation}",
|
||||
check_prerequisites=False,
|
||||
)
|
||||
if not attachment_allowed:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="接口权限不足")
|
||||
|
||||
if not parent_permission:
|
||||
return
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="附件类型未配置权限")
|
||||
|
||||
allowed = await role_has_api_permission(
|
||||
db,
|
||||
|
||||
Reference in New Issue
Block a user