迁移合同费用通用附件
1、删除旧费用附件 API、CRUD、模型和专用前端面板,统一改用通用附件能力。 2、调整合同费用接口为 study_id 语义,并补充合同、凭证、发票等附件类型的权限映射。 3、将合同费用新建和编辑改为抽屉流程,详情页与列表页复用同一编辑组件。 4、补充数据库迁移、附件列表、合同费用抽屉和权限场景测试,覆盖旧权限移除与新流程。
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
"""drop legacy fee attachments
|
||||
|
||||
Revision ID: 20260528_01
|
||||
Revises: 20260527_09
|
||||
Create Date: 2026-05-28 17:20:00.000000
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
revision: str = "20260528_01"
|
||||
down_revision: Union[str, None] = "20260527_09"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute("DROP TABLE IF EXISTS fee_attachments")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
"""remove contract fee attachment create and read permissions
|
||||
|
||||
Revision ID: 20260529_02
|
||||
Revises: 20260529_01
|
||||
Create Date: 2026-05-29 15:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision: str = "20260529_02"
|
||||
down_revision: Union[str, None] = "20260529_01"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
REMOVED_KEYS = (
|
||||
"fees_contracts_attachments:create",
|
||||
"fees_contracts_attachments:read",
|
||||
)
|
||||
|
||||
RESTORE_FROM_PARENT = {
|
||||
"fees_contracts_attachments:create": "fees_contracts:create",
|
||||
"fees_contracts_attachments:read": "fees_contracts:read",
|
||||
}
|
||||
|
||||
|
||||
def _table_exists(inspector: sa.Inspector, table_name: str) -> bool:
|
||||
return table_name in inspector.get_table_names()
|
||||
|
||||
|
||||
def _remove_permission_rows() -> None:
|
||||
quoted_keys = ", ".join(f"'{key}'" for key in REMOVED_KEYS)
|
||||
op.execute(f"DELETE FROM api_endpoint_permissions WHERE endpoint_key IN ({quoted_keys})")
|
||||
|
||||
|
||||
def _remove_template_key(table_name: str, key: str, *, touch_updated_at: bool = False) -> None:
|
||||
updated_at_assignment = ", updated_at = NOW()" if touch_updated_at else ""
|
||||
op.execute(
|
||||
f"""
|
||||
UPDATE {table_name}
|
||||
SET permissions = (
|
||||
SELECT jsonb_object_agg(role_key, role_permissions - '{key}')
|
||||
FROM jsonb_each(permissions::jsonb) AS role_entries(role_key, role_permissions)
|
||||
){updated_at_assignment}
|
||||
WHERE EXISTS (
|
||||
SELECT 1
|
||||
FROM jsonb_each(permissions::jsonb) AS role_entries(role_key, role_permissions)
|
||||
WHERE role_permissions ? '{key}'
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def _restore_permission_rows(new_key: str, source_key: str) -> None:
|
||||
op.execute(
|
||||
f"""
|
||||
INSERT INTO api_endpoint_permissions (id, study_id, role, endpoint_key, allowed, updated_at)
|
||||
SELECT (md5(random()::text || clock_timestamp()::text))::uuid, source.study_id, source.role, '{new_key}', source.allowed, NOW()
|
||||
FROM api_endpoint_permissions AS source
|
||||
WHERE source.endpoint_key = '{source_key}'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM api_endpoint_permissions AS target
|
||||
WHERE target.study_id = source.study_id
|
||||
AND target.role = source.role
|
||||
AND target.endpoint_key = '{new_key}'
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def _restore_template_key(table_name: str, new_key: str, source_key: str, *, touch_updated_at: bool = False) -> None:
|
||||
updated_at_assignment = ", updated_at = NOW()" if touch_updated_at else ""
|
||||
op.execute(
|
||||
f"""
|
||||
UPDATE {table_name}
|
||||
SET permissions = (
|
||||
SELECT jsonb_object_agg(
|
||||
role_key,
|
||||
CASE
|
||||
WHEN role_permissions ? '{source_key}' AND NOT role_permissions ? '{new_key}'
|
||||
THEN role_permissions || jsonb_build_object('{new_key}', role_permissions -> '{source_key}')
|
||||
ELSE role_permissions
|
||||
END
|
||||
)
|
||||
FROM jsonb_each(permissions::jsonb) AS role_entries(role_key, role_permissions)
|
||||
){updated_at_assignment}
|
||||
WHERE EXISTS (
|
||||
SELECT 1
|
||||
FROM jsonb_each(permissions::jsonb) AS role_entries(role_key, role_permissions)
|
||||
WHERE role_permissions ? '{source_key}'
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
inspector = sa.inspect(bind)
|
||||
|
||||
if _table_exists(inspector, "api_endpoint_permissions"):
|
||||
_remove_permission_rows()
|
||||
|
||||
if _table_exists(inspector, "permission_templates"):
|
||||
for key in REMOVED_KEYS:
|
||||
_remove_template_key("permission_templates", key, touch_updated_at=True)
|
||||
|
||||
if _table_exists(inspector, "permission_template_versions"):
|
||||
for key in REMOVED_KEYS:
|
||||
_remove_template_key("permission_template_versions", key)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
inspector = sa.inspect(bind)
|
||||
|
||||
if _table_exists(inspector, "api_endpoint_permissions"):
|
||||
for new_key, source_key in RESTORE_FROM_PARENT.items():
|
||||
_restore_permission_rows(new_key, source_key)
|
||||
|
||||
if _table_exists(inspector, "permission_templates"):
|
||||
for new_key, source_key in RESTORE_FROM_PARENT.items():
|
||||
_restore_template_key("permission_templates", new_key, source_key, touch_updated_at=True)
|
||||
|
||||
if _table_exists(inspector, "permission_template_versions"):
|
||||
for new_key, source_key in RESTORE_FROM_PARENT.items():
|
||||
_restore_template_key("permission_template_versions", new_key, source_key)
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
"""remove business attachment create and read permissions
|
||||
|
||||
Revision ID: 20260529_03
|
||||
Revises: 20260529_02
|
||||
Create Date: 2026-05-29 15:12:00.000000
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision: str = "20260529_03"
|
||||
down_revision: Union[str, None] = "20260529_02"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
RESTORE_FROM_PARENT = {
|
||||
"startup_initiation_attachments:create": "startup_initiation:create",
|
||||
"startup_initiation_attachments:read": "startup_initiation:read",
|
||||
"startup_ethics_attachments:create": "startup_ethics:create",
|
||||
"startup_ethics_attachments:read": "startup_ethics:read",
|
||||
"startup_auth_attachments:create": "startup_auth:create",
|
||||
"startup_auth_attachments:read": "startup_auth:read",
|
||||
"drug_shipments_attachments:create": "drug_shipments:create",
|
||||
"drug_shipments_attachments:read": "drug_shipments:read",
|
||||
"precautions_attachments:create": "precautions:create",
|
||||
"precautions_attachments:read": "precautions:read",
|
||||
"faq_attachments:create": "faq_reply:create",
|
||||
"faq_attachments:read": "faq:read",
|
||||
}
|
||||
|
||||
REMOVED_KEYS = tuple(RESTORE_FROM_PARENT)
|
||||
|
||||
|
||||
def _table_exists(inspector: sa.Inspector, table_name: str) -> bool:
|
||||
return table_name in inspector.get_table_names()
|
||||
|
||||
|
||||
def _remove_permission_rows() -> None:
|
||||
quoted_keys = ", ".join(f"'{key}'" for key in REMOVED_KEYS)
|
||||
op.execute(f"DELETE FROM api_endpoint_permissions WHERE endpoint_key IN ({quoted_keys})")
|
||||
|
||||
|
||||
def _remove_template_key(table_name: str, key: str, *, touch_updated_at: bool = False) -> None:
|
||||
updated_at_assignment = ", updated_at = NOW()" if touch_updated_at else ""
|
||||
op.execute(
|
||||
f"""
|
||||
UPDATE {table_name}
|
||||
SET permissions = (
|
||||
SELECT jsonb_object_agg(role_key, role_permissions - '{key}')
|
||||
FROM jsonb_each(permissions::jsonb) AS role_entries(role_key, role_permissions)
|
||||
){updated_at_assignment}
|
||||
WHERE EXISTS (
|
||||
SELECT 1
|
||||
FROM jsonb_each(permissions::jsonb) AS role_entries(role_key, role_permissions)
|
||||
WHERE role_permissions ? '{key}'
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def _restore_permission_rows(new_key: str, source_key: str) -> None:
|
||||
op.execute(
|
||||
f"""
|
||||
INSERT INTO api_endpoint_permissions (id, study_id, role, endpoint_key, allowed, updated_at)
|
||||
SELECT (md5(random()::text || clock_timestamp()::text))::uuid, source.study_id, source.role, '{new_key}', source.allowed, NOW()
|
||||
FROM api_endpoint_permissions AS source
|
||||
WHERE source.endpoint_key = '{source_key}'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM api_endpoint_permissions AS target
|
||||
WHERE target.study_id = source.study_id
|
||||
AND target.role = source.role
|
||||
AND target.endpoint_key = '{new_key}'
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def _restore_template_key(table_name: str, new_key: str, source_key: str, *, touch_updated_at: bool = False) -> None:
|
||||
updated_at_assignment = ", updated_at = NOW()" if touch_updated_at else ""
|
||||
op.execute(
|
||||
f"""
|
||||
UPDATE {table_name}
|
||||
SET permissions = (
|
||||
SELECT jsonb_object_agg(
|
||||
role_key,
|
||||
CASE
|
||||
WHEN role_permissions ? '{source_key}' AND NOT role_permissions ? '{new_key}'
|
||||
THEN role_permissions || jsonb_build_object('{new_key}', role_permissions -> '{source_key}')
|
||||
ELSE role_permissions
|
||||
END
|
||||
)
|
||||
FROM jsonb_each(permissions::jsonb) AS role_entries(role_key, role_permissions)
|
||||
){updated_at_assignment}
|
||||
WHERE EXISTS (
|
||||
SELECT 1
|
||||
FROM jsonb_each(permissions::jsonb) AS role_entries(role_key, role_permissions)
|
||||
WHERE role_permissions ? '{source_key}'
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
inspector = sa.inspect(bind)
|
||||
|
||||
if _table_exists(inspector, "api_endpoint_permissions"):
|
||||
_remove_permission_rows()
|
||||
|
||||
if _table_exists(inspector, "permission_templates"):
|
||||
for key in REMOVED_KEYS:
|
||||
_remove_template_key("permission_templates", key, touch_updated_at=True)
|
||||
|
||||
if _table_exists(inspector, "permission_template_versions"):
|
||||
for key in REMOVED_KEYS:
|
||||
_remove_template_key("permission_template_versions", key)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
inspector = sa.inspect(bind)
|
||||
|
||||
if _table_exists(inspector, "api_endpoint_permissions"):
|
||||
for new_key, source_key in RESTORE_FROM_PARENT.items():
|
||||
_restore_permission_rows(new_key, source_key)
|
||||
|
||||
if _table_exists(inspector, "permission_templates"):
|
||||
for new_key, source_key in RESTORE_FROM_PARENT.items():
|
||||
_restore_template_key("permission_templates", new_key, source_key, touch_updated_at=True)
|
||||
|
||||
if _table_exists(inspector, "permission_template_versions"):
|
||||
for new_key, source_key in RESTORE_FROM_PARENT.items():
|
||||
_restore_template_key("permission_template_versions", new_key, source_key)
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
"""add material equipment attachment delete permission
|
||||
|
||||
Revision ID: 20260529_04
|
||||
Revises: 20260529_03
|
||||
Create Date: 2026-05-29 15:30:00.000000
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision: str = "20260529_04"
|
||||
down_revision: Union[str, None] = "20260529_03"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
SOURCE_KEY = "material_equipments:update"
|
||||
TARGET_KEY = "material_equipments_attachments:delete"
|
||||
|
||||
|
||||
def _table_exists(inspector: sa.Inspector, table_name: str) -> bool:
|
||||
return table_name in inspector.get_table_names()
|
||||
|
||||
|
||||
def _copy_permission_rows() -> None:
|
||||
op.execute(
|
||||
f"""
|
||||
INSERT INTO api_endpoint_permissions (id, study_id, role, endpoint_key, allowed, updated_at)
|
||||
SELECT (md5(random()::text || clock_timestamp()::text))::uuid, source.study_id, source.role, '{TARGET_KEY}', source.allowed, NOW()
|
||||
FROM api_endpoint_permissions AS source
|
||||
WHERE source.endpoint_key = '{SOURCE_KEY}'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM api_endpoint_permissions AS target
|
||||
WHERE target.study_id = source.study_id
|
||||
AND target.role = source.role
|
||||
AND target.endpoint_key = '{TARGET_KEY}'
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def _remove_permission_rows() -> None:
|
||||
op.execute(f"DELETE FROM api_endpoint_permissions WHERE endpoint_key = '{TARGET_KEY}'")
|
||||
|
||||
|
||||
def _copy_template_key(table_name: str, *, touch_updated_at: bool = False) -> None:
|
||||
updated_at_assignment = ", updated_at = NOW()" if touch_updated_at else ""
|
||||
op.execute(
|
||||
f"""
|
||||
UPDATE {table_name}
|
||||
SET permissions = (
|
||||
SELECT jsonb_object_agg(
|
||||
role_key,
|
||||
CASE
|
||||
WHEN role_permissions ? '{SOURCE_KEY}' AND NOT role_permissions ? '{TARGET_KEY}'
|
||||
THEN role_permissions || jsonb_build_object('{TARGET_KEY}', role_permissions -> '{SOURCE_KEY}')
|
||||
ELSE role_permissions
|
||||
END
|
||||
)
|
||||
FROM jsonb_each(permissions::jsonb) AS role_entries(role_key, role_permissions)
|
||||
){updated_at_assignment}
|
||||
WHERE EXISTS (
|
||||
SELECT 1
|
||||
FROM jsonb_each(permissions::jsonb) AS role_entries(role_key, role_permissions)
|
||||
WHERE role_permissions ? '{SOURCE_KEY}'
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def _remove_template_key(table_name: str, *, touch_updated_at: bool = False) -> None:
|
||||
updated_at_assignment = ", updated_at = NOW()" if touch_updated_at else ""
|
||||
op.execute(
|
||||
f"""
|
||||
UPDATE {table_name}
|
||||
SET permissions = (
|
||||
SELECT jsonb_object_agg(role_key, role_permissions - '{TARGET_KEY}')
|
||||
FROM jsonb_each(permissions::jsonb) AS role_entries(role_key, role_permissions)
|
||||
){updated_at_assignment}
|
||||
WHERE EXISTS (
|
||||
SELECT 1
|
||||
FROM jsonb_each(permissions::jsonb) AS role_entries(role_key, role_permissions)
|
||||
WHERE role_permissions ? '{TARGET_KEY}'
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
inspector = sa.inspect(bind)
|
||||
|
||||
if _table_exists(inspector, "api_endpoint_permissions"):
|
||||
_copy_permission_rows()
|
||||
|
||||
if _table_exists(inspector, "permission_templates"):
|
||||
_copy_template_key("permission_templates", touch_updated_at=True)
|
||||
|
||||
if _table_exists(inspector, "permission_template_versions"):
|
||||
_copy_template_key("permission_template_versions")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
inspector = sa.inspect(bind)
|
||||
|
||||
if _table_exists(inspector, "api_endpoint_permissions"):
|
||||
_remove_permission_rows()
|
||||
|
||||
if _table_exists(inspector, "permission_templates"):
|
||||
_remove_template_key("permission_templates", touch_updated_at=True)
|
||||
|
||||
if _table_exists(inspector, "permission_template_versions"):
|
||||
_remove_template_key("permission_template_versions")
|
||||
Reference in New Issue
Block a user