diff --git a/backend/alembic/versions/20260514_01_add_permission_templates.py b/backend/alembic/versions/20260514_01_add_permission_templates.py index b2ad4c18..f64ce94a 100644 --- a/backend/alembic/versions/20260514_01_add_permission_templates.py +++ b/backend/alembic/versions/20260514_01_add_permission_templates.py @@ -126,14 +126,6 @@ def _insert_system_templates() -> None: "IMP", '{"IMP": {"subjects:create": false, "subjects:list": true, "subjects:read": true, "subjects:update": false, "subjects:delete": false, "visits:create": false, "visits:list": true, "visits:read": true, "visits:update": false, "visits:delete": false, "risk_issues:create": false, "risk_issues:list": true, "risk_issues:read": true, "risk_issues:update": false, "risk_issues:delete": false, "finance_contracts:create": false, "finance_contracts:list": true, "finance_contracts:read": true, "finance_contracts:update": false, "finance_contracts:delete": false, "fees_contracts:create": false, "fees_contracts:list": true, "fees_contracts:read": true, "fees_contracts:update": false, "fees_contracts:delete": false, "fees_payments:create": false, "fees_payments:update": false, "fees_payments:delete": false, "project_members:create": false, "project_members:list": false, "project_members:candidates": false, "project_members:update": false, "project_members:delete": false, "sites:create": false, "sites:list": true, "sites:read": true, "sites:update": false, "sites:delete": false, "ethics:create": false, "ethics:list": false, "ethics:read": false, "ethics:update": false, "ethics:delete": false, "feasibility:create": false, "feasibility:list": false, "feasibility:read": false, "feasibility:update": false, "feasibility:delete": false, "budget:create": false, "budget:list": true, "budget:read": true, "budget:update": false, "budget:delete": false, "timeline:create": false, "timeline:list": false, "timeline:read": false, "timeline:update": false, "timeline:delete": false, "permissions:read": false, "permissions:update": false, "overview:read": true, "monitoring_issues:create": false, "monitoring_issues:list": false, "monitoring_issues:read": false, "monitoring_issues:update": false, "monitoring_issues:delete": false, "monitoring_issues:close": false, "monitoring_issues:history": false, "monitoring_audit:create": false, "monitoring_audit:read": false, "monitoring_audit:update": false, "monitoring_audit:delete": false, "drug_shipments:create": true, "drug_shipments:list": true, "drug_shipments:read": true, "drug_shipments:update": true, "drug_shipments:delete": false, "materials:create": true, "materials:list": true, "materials:read": true, "materials:update": true, "materials:delete": false, "subject_pds:create": false, "subject_pds:list": false, "subject_pds:read": false, "subject_pds:update": false, "audit_logs:list": false, "audit_logs:read": false, "audit_logs:export": false, "knowledge_notes:create": false, "knowledge_notes:list": true, "knowledge_notes:read": true, "knowledge_notes:update": false, "knowledge_notes:delete": false, "subject_history:list": false, "subject_history:read": false, "subject_history:timeline": false, "subject_history:export": false, "subject_history:search": false, "milestones:list": true, "milestones:update": false, "attachments:create": false, "attachments:read": true, "attachments:update": false, "attachments:delete": false, "fees_attachments:create": false, "fees_attachments:read": false, "fees_attachments:delete": false, "faq:create": false, "faq:read": true, "faq:update": false, "faq:delete": false, "faq_category:create": false, "faq_category:read": true, "faq_category:update": false, "faq_category:delete": false, "faq_reply:create": false, "faq_reply:delete": false, "dashboard:read": true, "subject_histories:create": false, "subject_histories:list": true, "subject_histories:read": true, "subject_histories:update": false, "subject_histories:delete": false, "material_equipments:create": false, "material_equipments:list": true, "material_equipments:read": true, "material_equipments:update": false, "material_equipments:delete": false, "documents:create": false, "documents:read": true, "documents:update": false, "documents:delete": false}}', ), - ( - "0c907f29-dc88-4878-9c66-15cbc22f4e13", - "质量保证", - "质量检查和审计人员,以查询权限为主", - "QA", - "QA", - '{"QA": {"subjects:create": false, "subjects:list": true, "subjects:read": true, "subjects:update": false, "subjects:delete": false, "visits:create": false, "visits:list": true, "visits:read": true, "visits:update": false, "visits:delete": false, "risk_issues:create": false, "risk_issues:list": true, "risk_issues:read": true, "risk_issues:update": false, "risk_issues:delete": false, "finance_contracts:create": false, "finance_contracts:list": false, "finance_contracts:read": false, "finance_contracts:update": false, "finance_contracts:delete": false, "fees_contracts:create": false, "fees_contracts:list": false, "fees_contracts:read": false, "fees_contracts:update": false, "fees_contracts:delete": false, "fees_payments:create": false, "fees_payments:update": false, "fees_payments:delete": false, "project_members:create": false, "project_members:list": false, "project_members:candidates": false, "project_members:update": false, "project_members:delete": false, "sites:create": false, "sites:list": true, "sites:read": true, "sites:update": false, "sites:delete": false, "ethics:create": false, "ethics:list": false, "ethics:read": false, "ethics:update": false, "ethics:delete": false, "feasibility:create": false, "feasibility:list": false, "feasibility:read": false, "feasibility:update": false, "feasibility:delete": false, "budget:create": false, "budget:list": false, "budget:read": false, "budget:update": false, "budget:delete": false, "timeline:create": false, "timeline:list": false, "timeline:read": false, "timeline:update": false, "timeline:delete": false, "permissions:read": false, "permissions:update": false, "overview:read": true, "monitoring_issues:create": false, "monitoring_issues:list": false, "monitoring_issues:read": false, "monitoring_issues:update": false, "monitoring_issues:delete": false, "monitoring_issues:close": false, "monitoring_issues:history": false, "monitoring_audit:create": false, "monitoring_audit:read": true, "monitoring_audit:update": false, "monitoring_audit:delete": false, "drug_shipments:create": false, "drug_shipments:list": true, "drug_shipments:read": true, "drug_shipments:update": false, "drug_shipments:delete": false, "materials:create": false, "materials:list": true, "materials:read": true, "materials:update": false, "materials:delete": false, "subject_pds:create": false, "subject_pds:list": false, "subject_pds:read": false, "subject_pds:update": false, "audit_logs:list": true, "audit_logs:read": true, "audit_logs:export": true, "knowledge_notes:create": false, "knowledge_notes:list": true, "knowledge_notes:read": true, "knowledge_notes:update": false, "knowledge_notes:delete": false, "subject_history:list": false, "subject_history:read": false, "subject_history:timeline": false, "subject_history:export": false, "subject_history:search": false, "milestones:list": true, "milestones:update": false, "attachments:create": false, "attachments:read": true, "attachments:update": false, "attachments:delete": false, "fees_attachments:create": false, "fees_attachments:read": false, "fees_attachments:delete": false, "faq:create": false, "faq:read": true, "faq:update": false, "faq:delete": false, "faq_category:create": false, "faq_category:read": true, "faq_category:update": false, "faq_category:delete": false, "faq_reply:create": false, "faq_reply:delete": false, "dashboard:read": true, "subject_histories:create": false, "subject_histories:list": true, "subject_histories:read": true, "subject_histories:update": false, "subject_histories:delete": false, "material_equipments:create": false, "material_equipments:list": true, "material_equipments:read": true, "material_equipments:update": false, "material_equipments:delete": false, "documents:create": false, "documents:read": true, "documents:update": false, "documents:delete": false}}', - ), ] for tid, name, desc, category, recommended_roles, perms_json in templates: diff --git a/backend/alembic/versions/20260521_01_remove_qa_role.py b/backend/alembic/versions/20260521_01_remove_qa_role.py new file mode 100644 index 00000000..1407e4ff --- /dev/null +++ b/backend/alembic/versions/20260521_01_remove_qa_role.py @@ -0,0 +1,153 @@ +"""remove qa role + +Revision ID: 20260521_01 +Revises: 20260520_01 +Create Date: 2026-05-21 09:00:00.000000 + +""" + +from typing import Sequence, Union + +from alembic import op + + +revision: str = "20260521_01" +down_revision: Union[str, None] = "20260520_01" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +USER_ROLE_VALUES = ("ADMIN", "PM", "CRA", "PV", "IMP", "MEDICAL_REVIEW") + + +def upgrade() -> None: + op.execute("CREATE TEMP TABLE qa_user_ids AS SELECT id FROM users WHERE role = 'QA'") + op.execute("DELETE FROM permission_access_logs WHERE role = 'QA'") + op.execute("DELETE FROM api_endpoint_permissions WHERE role = 'QA'") + op.execute("DELETE FROM study_members WHERE role_in_study = 'QA'") + op.execute( + """ + UPDATE studies + SET active_roles = ( + SELECT COALESCE(json_agg(role), '[]'::json) + FROM json_array_elements_text(active_roles) AS role + WHERE role <> 'QA' + ) + WHERE active_roles::text LIKE '%QA%' + """ + ) + op.execute( + """ + DELETE FROM permission_template_versions + WHERE template_id IN ( + SELECT id FROM permission_templates + WHERE category = 'QA' OR recommended_roles = 'QA' OR permissions ? 'QA' + ) + """ + ) + op.execute( + """ + DELETE FROM permission_templates + WHERE category = 'QA' OR recommended_roles = 'QA' OR permissions ? 'QA' + """ + ) + op.execute( + """ + UPDATE permission_templates + SET created_by = NULL + WHERE created_by IN (SELECT id FROM qa_user_ids) + """ + ) + op.execute( + """ + DELETE FROM audit_logs + WHERE operator_role = 'QA' + OR operator_id IN (SELECT id FROM users WHERE role = 'QA') + """ + ) + op.execute( + """ + DO $$ + DECLARE + rel record; + BEGIN + FOR rel IN + SELECT + quote_ident(ns.nspname) AS schema_name, + quote_ident(cls.relname) AS table_name, + quote_ident(att.attname) AS column_name, + att.attnotnull AS not_null + FROM pg_constraint con + JOIN pg_class cls ON cls.oid = con.conrelid + JOIN pg_namespace ns ON ns.oid = cls.relnamespace + JOIN pg_attribute att ON att.attrelid = con.conrelid AND att.attnum = ANY(con.conkey) + WHERE con.contype = 'f' + AND con.confrelid = 'users'::regclass + AND array_length(con.conkey, 1) = 1 + LOOP + IF rel.table_name = 'users' AND rel.column_name = 'id' THEN + CONTINUE; + END IF; + + IF rel.not_null THEN + EXECUTE format( + 'DELETE FROM %s.%s WHERE %s IN (SELECT id FROM qa_user_ids)', + rel.schema_name, + rel.table_name, + rel.column_name + ); + ELSE + EXECUTE format( + 'UPDATE %s.%s SET %s = NULL WHERE %s IN (SELECT id FROM qa_user_ids)', + rel.schema_name, + rel.table_name, + rel.column_name, + rel.column_name + ); + END IF; + END LOOP; + END $$; + """ + ) + + # Delete QA users after dependent project-role and audit rows have been removed. + op.execute("DELETE FROM users WHERE id IN (SELECT id FROM qa_user_ids)") + op.execute("DROP TABLE qa_user_ids") + + allowed = ", ".join(f"'{role}'" for role in USER_ROLE_VALUES) + op.execute( + f""" + ALTER TABLE users + ALTER COLUMN role TYPE text + USING role::text + """ + ) + op.execute("DROP TYPE user_role") + op.execute(f"CREATE TYPE user_role AS ENUM ({allowed})") + op.execute( + """ + ALTER TABLE users + ALTER COLUMN role TYPE user_role + USING role::user_role + """ + ) + + +def downgrade() -> None: + allowed = ", ".join(f"'{role}'" for role in (*USER_ROLE_VALUES, "QA")) + op.execute( + """ + ALTER TABLE users + ALTER COLUMN role TYPE text + USING role::text + """ + ) + op.execute("DROP TYPE user_role") + op.execute(f"CREATE TYPE user_role AS ENUM ({allowed})") + op.execute( + """ + ALTER TABLE users + ALTER COLUMN role TYPE user_role + USING role::user_role + """ + ) diff --git a/backend/app/api/v1/members.py b/backend/app/api/v1/members.py index 0f344568..770f9e07 100644 --- a/backend/app/api/v1/members.py +++ b/backend/app/api/v1/members.py @@ -18,7 +18,6 @@ router = APIRouter() PROJECT_ROLE_RANK = { "ADMIN": 100, "PM": 80, - "QA": 60, "PV": 50, "MEDICAL_REVIEW": 50, "CRA": 40, diff --git a/backend/app/api/v1/study_active_roles.py b/backend/app/api/v1/study_active_roles.py index 895f3ca5..4d2560d3 100644 --- a/backend/app/api/v1/study_active_roles.py +++ b/backend/app/api/v1/study_active_roles.py @@ -25,6 +25,8 @@ def _normalize_active_roles(value: object) -> list[str]: continue if role == "ADMIN": raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="ADMIN 不能作为项目角色") + if role == "QA": + raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="QA 不能作为项目角色") if len(role) > 20: raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="项目角色长度不能超过20个字符") if role not in roles: diff --git a/backend/app/core/api_permissions.py b/backend/app/core/api_permissions.py index 6cad6a4e..4f03adb4 100644 --- a/backend/app/core/api_permissions.py +++ b/backend/app/core/api_permissions.py @@ -20,14 +20,14 @@ API_ENDPOINT_PERMISSIONS = { "module": "subjects", "action": "read", "description": "查询参与者列表", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"], + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], "prerequisite_permissions": [], }, "subjects:read": { "module": "subjects", "action": "read", "description": "查询参与者详情", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"], + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], "prerequisite_permissions": [], }, "subjects:update": { @@ -56,14 +56,14 @@ API_ENDPOINT_PERMISSIONS = { "module": "subjects", "action": "read", "description": "查询访视列表", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"], + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], "prerequisite_permissions": [], }, "visits:read": { "module": "subjects", "action": "read", "description": "查询访视详情", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"], + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], "prerequisite_permissions": [], }, "visits:update": { @@ -92,14 +92,14 @@ API_ENDPOINT_PERMISSIONS = { "module": "risk_issues", "action": "read", "description": "查询不良事件列表", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"], + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], "prerequisite_permissions": [], }, "risk_issues:read": { "module": "risk_issues", "action": "read", "description": "查询不良事件详情", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"], + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], "prerequisite_permissions": [], }, "risk_issues:update": { @@ -251,13 +251,13 @@ API_ENDPOINT_PERMISSIONS = { "module": "sites", "action": "read", "description": "查询中心列表", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"], + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], }, "sites:read": { "module": "sites", "action": "read", "description": "查询中心详情", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"], + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], }, "sites:update": { "module": "sites", @@ -413,7 +413,7 @@ API_ENDPOINT_PERMISSIONS = { "module": "project_overview", "action": "read", "description": "查询项目概览", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"], + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], }, # 监查问题管理 "monitoring_issues:create": { @@ -470,7 +470,7 @@ API_ENDPOINT_PERMISSIONS = { "module": "monitoring_audit", "action": "read", "description": "查询监查访视问题", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "QA"], + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW"], "prerequisite_permissions": [], }, "monitoring_audit:update": { @@ -499,14 +499,14 @@ API_ENDPOINT_PERMISSIONS = { "module": "materials", "action": "read", "description": "查询药物发货列表", - "default_roles": ["PM", "CRA", "IMP", "QA"], + "default_roles": ["PM", "CRA", "IMP"], "prerequisite_permissions": [], }, "drug_shipments:read": { "module": "materials", "action": "read", "description": "查询药物发货详情", - "default_roles": ["PM", "CRA", "IMP", "QA"], + "default_roles": ["PM", "CRA", "IMP"], "prerequisite_permissions": [], }, "drug_shipments:update": { @@ -534,13 +534,13 @@ API_ENDPOINT_PERMISSIONS = { "module": "materials", "action": "read", "description": "查询物资列表", - "default_roles": ["PM", "CRA", "IMP", "QA"], + "default_roles": ["PM", "CRA", "IMP"], }, "materials:read": { "module": "materials", "action": "read", "description": "查询物资详情", - "default_roles": ["PM", "CRA", "IMP", "QA"], + "default_roles": ["PM", "CRA", "IMP"], }, "materials:update": { "module": "materials", @@ -595,19 +595,19 @@ API_ENDPOINT_PERMISSIONS = { "module": "audit_export", "action": "read", "description": "查询审计日志列表", - "default_roles": ["PM", "QA"], + "default_roles": ["PM"], }, "audit_logs:read": { "module": "audit_export", "action": "read", "description": "查询审计日志详情", - "default_roles": ["PM", "QA"], + "default_roles": ["PM"], }, "audit_logs:export": { "module": "audit_export", "action": "read", "description": "导出审计日志", - "default_roles": ["PM", "QA"], + "default_roles": ["PM"], }, # 知识库笔记管理 "knowledge_notes:create": { @@ -620,13 +620,13 @@ API_ENDPOINT_PERMISSIONS = { "module": "shared_library", "action": "read", "description": "查询知识库笔记列表", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"], + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], }, "knowledge_notes:read": { "module": "shared_library", "action": "read", "description": "查询知识库笔记详情", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"], + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], }, "knowledge_notes:update": { "module": "shared_library", @@ -676,7 +676,7 @@ API_ENDPOINT_PERMISSIONS = { "module": "setup_config", "action": "read", "description": "查询立项配置", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"], + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], }, "setup_config:update": { "module": "setup_config", @@ -707,7 +707,7 @@ API_ENDPOINT_PERMISSIONS = { "module": "project_milestones", "action": "read", "description": "查询项目里程碑列表", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"], + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], }, "project_milestones:update": { "module": "project_milestones", @@ -751,7 +751,7 @@ API_ENDPOINT_PERMISSIONS = { "module": "attachments", "action": "read", "description": "查询附件", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"], + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], }, "attachments:update": { "module": "attachments", @@ -795,7 +795,7 @@ API_ENDPOINT_PERMISSIONS = { "module": "faq", "action": "read", "description": "查询FAQ", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"], + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], }, "faq:update": { "module": "faq", @@ -820,7 +820,7 @@ API_ENDPOINT_PERMISSIONS = { "module": "faq", "action": "read", "description": "查询FAQ分类", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"], + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], }, "faq_category:update": { "module": "faq", @@ -852,7 +852,7 @@ API_ENDPOINT_PERMISSIONS = { "module": "dashboard", "action": "read", "description": "查询仪表板", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"], + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], "prerequisite_permissions": ["sites:read", "subjects:read"], }, # 参与者历史管理 @@ -867,14 +867,14 @@ API_ENDPOINT_PERMISSIONS = { "module": "subject_histories", "action": "read", "description": "查询参与者历史列表", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"], + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], "prerequisite_permissions": [], }, "subject_histories:read": { "module": "subject_histories", "action": "read", "description": "查询参与者历史详情", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"], + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], "prerequisite_permissions": [], }, "subject_histories:update": { @@ -902,13 +902,13 @@ API_ENDPOINT_PERMISSIONS = { "module": "material_equipments", "action": "read", "description": "查询物资设备列表", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"], + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], }, "material_equipments:read": { "module": "material_equipments", "action": "read", "description": "查询物资设备详情", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"], + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], }, "material_equipments:update": { "module": "material_equipments", @@ -933,7 +933,7 @@ API_ENDPOINT_PERMISSIONS = { "module": "documents", "action": "read", "description": "查询文档", - "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"], + "default_roles": ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], }, "documents:update": { "module": "documents", @@ -1170,7 +1170,7 @@ OPERATION_TO_ENDPOINTS: dict[str, dict[str, list[str]]] = { } # 项目级权限角色列表 -PROJECT_PERMISSION_ROLES = ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"] +PROJECT_PERMISSION_ROLES = ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"] # 前置权限映射表(用于权限检查) # 定义每个操作需要的前置权限 diff --git a/backend/app/core/project_permissions.py b/backend/app/core/project_permissions.py index 78fcc595..1899a6f2 100644 --- a/backend/app/core/project_permissions.py +++ b/backend/app/core/project_permissions.py @@ -7,7 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.models.api_endpoint_permission import ApiEndpointPermission from app.models.study import Study -from app.core.api_permissions import API_ENDPOINT_PERMISSIONS, OPERATION_PREREQUISITES +from app.core.api_permissions import API_ENDPOINT_PERMISSIONS, OPERATION_PREREQUISITES, PROJECT_PERMISSION_ROLES from app.core.permission_cache import get_permission_cache @@ -114,7 +114,7 @@ async def get_api_endpoint_permissions( study_result = await db.execute(select(Study).where(Study.id == study_id)) study = study_result.scalar_one_or_none() active_roles = [role for role in (study.active_roles if study else []) if isinstance(role, str) and role.strip()] - roles = list(dict.fromkeys(["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA", *active_roles, *overrides.keys()])) + roles = list(dict.fromkeys([*PROJECT_PERMISSION_ROLES, *active_roles, *overrides.keys()])) matrix: dict[str, dict[str, dict[str, bool]]] = {} for role in roles: matrix[role] = {} diff --git a/backend/app/core/rbac.py b/backend/app/core/rbac.py index bfceaac4..28cd2edc 100644 --- a/backend/app/core/rbac.py +++ b/backend/app/core/rbac.py @@ -22,7 +22,6 @@ ROLE_ACTIONS: dict[str, set[str]] = { "ack", "delete_document", }, - "QA": {"view", "create_document", "create_version", "submit", "approve", "distribute", "ack"}, "PM": {"view", "create_document", "create_version", "submit", "approve", "distribute", "ack", "delete_document"}, "CRA": {"view", "create_version", "submit", "ack"}, "PV": {"view", "create_version", "submit", "ack"}, diff --git a/backend/app/models/user.py b/backend/app/models/user.py index a165ba0c..7ac6692e 100644 --- a/backend/app/models/user.py +++ b/backend/app/models/user.py @@ -19,7 +19,6 @@ class UserRole(str, enum.Enum): PV = "PV" MEDICAL_REVIEW = "MEDICAL_REVIEW" IMP = "IMP" - QA = "QA" class UserStatus(str, enum.Enum): diff --git a/backend/app/schemas/project_permission.py b/backend/app/schemas/project_permission.py index bb5640db..d819e6cc 100644 --- a/backend/app/schemas/project_permission.py +++ b/backend/app/schemas/project_permission.py @@ -5,7 +5,7 @@ from typing import Literal from pydantic import BaseModel, Field -ProjectPermissionRole = Literal["ADMIN", "PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"] +ProjectPermissionRole = Literal["ADMIN", "PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"] ProjectPermissionAction = Literal["read", "write"] diff --git a/backend/app/schemas/user.py b/backend/app/schemas/user.py index 5d39092e..7f02d939 100644 --- a/backend/app/schemas/user.py +++ b/backend/app/schemas/user.py @@ -5,7 +5,7 @@ from typing import Literal, Optional from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator -UserRole = Literal["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA", "ADMIN"] +UserRole = Literal["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "ADMIN"] UserStatus = Literal["PENDING", "ACTIVE", "REJECTED", "DISABLED"] PASSWORD_REGEX = re.compile(r"^(?=.*[A-Za-z])(?=.*\d).{8,}$") diff --git a/backend/app/services/permission_template_service.py b/backend/app/services/permission_template_service.py index bbed4e66..8ac02413 100644 --- a/backend/app/services/permission_template_service.py +++ b/backend/app/services/permission_template_service.py @@ -7,7 +7,7 @@ from sqlalchemy.orm import selectinload from app.models.permission_template import PermissionTemplate, PermissionTemplateVersion, TemplateType from app.models.api_endpoint_permission import ApiEndpointPermission -from app.core.api_permissions import API_ENDPOINT_PERMISSIONS +from app.core.api_permissions import API_ENDPOINT_PERMISSIONS, PROJECT_PERMISSION_ROLES from app.schemas.permission_template import PermissionTemplateCreate, PermissionTemplateUpdate @@ -21,6 +21,9 @@ class PermissionTemplateService: created_by: UUID, ) -> PermissionTemplate: """创建权限模板""" + if payload.template_type != TemplateType.CUSTOM: + raise ValueError("只能新增自定义角色") + # 验证权限配置 await PermissionTemplateService._validate_permissions(payload.permissions) @@ -78,6 +81,11 @@ class PermissionTemplateService: query = query.where(PermissionTemplate.template_type == template_type) if category: query = query.where(PermissionTemplate.category == category) + elif template_type == TemplateType.ROLE: + query = query.where( + (PermissionTemplate.is_system.is_(False)) + | (PermissionTemplate.category.in_(PROJECT_PERMISSION_ROLES)) + ) query = query.offset(skip).limit(limit) result = await db.execute(query) @@ -94,9 +102,11 @@ class PermissionTemplateService: if not template: raise ValueError(f"模板 {template_id} 不存在") - # 不允许修改系统预设模板 + # 系统预设模板只允许更新展示元数据,权限配置和分类等结构字段保持受保护。 if template.is_system: - raise ValueError("不允许修改系统预设模板") + protected_fields = {"permissions", "tags", "category", "recommended_roles"} + if protected_fields.intersection(payload.model_fields_set): + raise ValueError("不允许修改系统预设模板") # 验证权限配置 if payload.permissions: diff --git a/backend/tests/test_permission_templates.py b/backend/tests/test_permission_templates.py index 6cba6c21..6912dd72 100644 --- a/backend/tests/test_permission_templates.py +++ b/backend/tests/test_permission_templates.py @@ -67,6 +67,18 @@ async def test_create_template_creates_version_1(db_session, user_id): assert template.versions[0].change_log == "初始版本" +@pytest.mark.asyncio +async def test_create_template_rejects_non_custom_role_type(db_session, user_id): + payload = PermissionTemplateCreate( + name="预设角色不允许新增", + template_type=TemplateType.ROLE, + permissions=SAMPLE_PERMISSIONS, + ) + + with pytest.raises(ValueError, match="只能新增自定义角色"): + await PermissionTemplateService.create_template(db_session, payload, user_id) + + @pytest.mark.asyncio async def test_create_template_invalid_endpoint_key(db_session, user_id): payload = PermissionTemplateCreate( @@ -95,16 +107,19 @@ async def test_create_template_invalid_permission_value(db_session, user_id): @pytest.mark.asyncio async def test_get_template_exists(db_session, user_id): - payload = PermissionTemplateCreate( + template = PermissionTemplate( name="获取测试", template_type=TemplateType.ROLE, permissions=SAMPLE_PERMISSIONS, ) - created = await PermissionTemplateService.create_template(db_session, payload, user_id) - fetched = await PermissionTemplateService.get_template(db_session, created.id) + db_session.add(template) + await db_session.commit() + await db_session.refresh(template) + + fetched = await PermissionTemplateService.get_template(db_session, template.id) assert fetched is not None - assert fetched.id == created.id + assert fetched.id == template.id assert fetched.name == "获取测试" @@ -120,22 +135,52 @@ async def test_get_template_not_found(db_session): @pytest.mark.asyncio async def test_list_templates_filter_by_type(db_session, user_id): - await PermissionTemplateService.create_template( - db_session, - PermissionTemplateCreate(name="角色模板", template_type=TemplateType.ROLE, permissions=SAMPLE_PERMISSIONS), - user_id, + db_session.add( + PermissionTemplate( + name="角色模板", + template_type=TemplateType.ROLE, + is_system=True, + category="PM", + permissions=SAMPLE_PERMISSIONS, + ) ) await PermissionTemplateService.create_template( db_session, - PermissionTemplateCreate(name="场景模板", template_type=TemplateType.SCENARIO, permissions=SAMPLE_PERMISSIONS), + PermissionTemplateCreate(name="自定义角色", template_type=TemplateType.CUSTOM, permissions=SAMPLE_PERMISSIONS), user_id, ) + await db_session.commit() role_templates = await PermissionTemplateService.list_templates(db_session, template_type=TemplateType.ROLE) assert all(t.template_type == TemplateType.ROLE for t in role_templates) assert len(role_templates) >= 1 +@pytest.mark.asyncio +async def test_list_templates_excludes_qa_system_preset(db_session): + qa_template = PermissionTemplate( + name="质量保证", + template_type=TemplateType.ROLE, + is_system=True, + category="QA", + permissions={"QA": {"subjects:read": True}}, + ) + pm_template = PermissionTemplate( + name="项目经理", + template_type=TemplateType.ROLE, + is_system=True, + category="PM", + permissions={"PM": {"subjects:read": True}}, + ) + db_session.add_all([qa_template, pm_template]) + await db_session.commit() + + role_templates = await PermissionTemplateService.list_templates(db_session, template_type=TemplateType.ROLE) + + assert "PM" in {template.category for template in role_templates} + assert "QA" not in {template.category for template in role_templates if template.is_system} + + # --------------------------------------------------------------------------- # 更新模板 # --------------------------------------------------------------------------- @@ -175,7 +220,31 @@ async def test_update_template_permissions_creates_new_version(db_session, user_ @pytest.mark.asyncio -async def test_update_system_template_raises(db_session): +async def test_update_system_template_allows_name_and_description(db_session): + system_template = PermissionTemplate( + name="系统模板", + description="旧描述", + template_type=TemplateType.ROLE, + is_system=True, + permissions=SAMPLE_PERMISSIONS, + ) + db_session.add(system_template) + await db_session.commit() + await db_session.refresh(system_template) + + updated = await PermissionTemplateService.update_template( + db_session, + system_template.id, + PermissionTemplateUpdate(name="新名称", description="新描述"), + ) + + assert updated.name == "新名称" + assert updated.description == "新描述" + assert updated.permissions == SAMPLE_PERMISSIONS + + +@pytest.mark.asyncio +async def test_update_system_template_rejects_protected_fields(db_session): system_template = PermissionTemplate( name="系统模板", template_type=TemplateType.ROLE, @@ -188,7 +257,9 @@ async def test_update_system_template_raises(db_session): with pytest.raises(ValueError, match="不允许修改系统预设模板"): await PermissionTemplateService.update_template( - db_session, system_template.id, PermissionTemplateUpdate(name="改名") + db_session, + system_template.id, + PermissionTemplateUpdate(permissions={"PM": {"subjects:read": True}}), ) diff --git a/database/init.sql b/database/init.sql index 8dfd74c3..18fe3a06 100644 --- a/database/init.sql +++ b/database/init.sql @@ -2,14 +2,7 @@ DO $$ BEGIN - CREATE TYPE public.user_role AS ENUM ('ADMIN', 'PM', 'CRA', 'PV', 'IMP', 'QA'); -EXCEPTION - WHEN duplicate_object THEN null; -END $$; - -DO $$ -BEGIN - ALTER TYPE public.user_role ADD VALUE IF NOT EXISTS 'QA'; + CREATE TYPE public.user_role AS ENUM ('ADMIN', 'PM', 'CRA', 'PV', 'IMP', 'MEDICAL_REVIEW'); EXCEPTION WHEN duplicate_object THEN null; END $$; @@ -540,7 +533,6 @@ INSERT INTO public.users ( ('11111111-1111-1111-1111-111111111111', 'admin@example.com', '$2y$12$FMfyBt7YfZWeh/x8WMEIA.S6VUTIMryk2NxWHyOOuJvd6YrDwHAdu', 'System Admin', 'ADMIN', 'SYSTEM', 'ACTIVE', '2025-01-05 08:00:00+00', '2025-01-05 08:00:00+00'), ('22222222-2222-2222-2222-222222222222', 'pm@example.com', '$2y$12$P9UDKu48ru84uVrlquqhXufmw/CfMEzQosC0X8eT2EmY/PgM/03j2', '项目经理', 'PM', 'PMO', 'ACTIVE', '2025-01-05 08:10:00+00', '2025-01-05 08:10:00+00'), ('33333333-3333-3333-3333-333333333333', 'cra@example.com', '$2y$12$D/.ZeDrUByPZFPnAl9MnKuzKn2G4ctKQ9IAS/CqS2sfMdE/5NaKAK', 'CRA 用户', 'CRA', 'CRA', 'ACTIVE', '2025-01-05 08:20:00+00', '2025-01-05 08:20:00+00'), - ('44444444-4444-4444-4444-444444444444', 'qa@example.com', '$2y$12$P9UDKu48ru84uVrlquqhXufmw/CfMEzQosC0X8eT2EmY/PgM/03j2', 'QA 用户', 'QA', 'QA', 'ACTIVE', '2025-01-05 08:30:00+00', '2025-01-05 08:30:00+00'), ('55555555-5555-5555-5555-555555555555', 'pv@example.com', '$2y$12$P9UDKu48ru84uVrlquqhXufmw/CfMEzQosC0X8eT2EmY/PgM/03j2', 'PV 用户', 'PV', 'PV', 'ACTIVE', '2025-01-05 08:40:00+00', '2025-01-05 08:40:00+00'), ('66666666-6666-6666-6666-666666666666', 'imp@example.com', '$2y$12$P9UDKu48ru84uVrlquqhXufmw/CfMEzQosC0X8eT2EmY/PgM/03j2', '药品管理员', 'IMP', 'IMP', 'ACTIVE', '2025-01-05 08:50:00+00', '2025-01-05 08:50:00+00') ON CONFLICT (email) DO UPDATE SET @@ -592,7 +584,6 @@ INSERT INTO public.study_members ( ('dddddddd-dddd-dddd-dddd-dddddddddddd', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), (SELECT id FROM public.users WHERE email = 'admin@example.com'), 'ADMIN', true, '2025-01-06 10:00:00+00'), ('eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), (SELECT id FROM public.users WHERE email = 'pm@example.com'), 'PM', true, '2025-01-06 10:05:00+00'), ('ffffffff-ffff-ffff-ffff-ffffffffffff', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), (SELECT id FROM public.users WHERE email = 'cra@example.com'), 'CRA', true, '2025-01-06 10:10:00+00'), - ('11111111-2222-3333-4444-555555555555', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), (SELECT id FROM public.users WHERE email = 'qa@example.com'), 'QA', true, '2025-01-06 10:15:00+00'), ('22222222-3333-4444-5555-666666666666', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), (SELECT id FROM public.users WHERE email = 'pv@example.com'), 'PV', true, '2025-01-06 10:20:00+00'), ('33333333-4444-5555-6666-777777777777', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), (SELECT id FROM public.users WHERE email = 'imp@example.com'), 'IMP', true, '2025-01-06 10:25:00+00') ON CONFLICT (study_id, user_id) DO UPDATE SET diff --git a/database/seed_documents.sql b/database/seed_documents.sql index 04f9155b..47ebf45f 100644 --- a/database/seed_documents.sql +++ b/database/seed_documents.sql @@ -23,8 +23,8 @@ INSERT INTO public.workflow_templates ( ( '11111111-aaaa-bbbb-cccc-111111111212', (SELECT id FROM public.studies WHERE code = 'DOC-DEMO'), - '严格审批(CRA→PM→QA)', - '三节点审批,适用于关键文件', + '严格审批(CRA→PM)', + '双节点审批,适用于关键文件', true, (SELECT id FROM public.users WHERE email = 'pm@example.com'), now(), @@ -67,15 +67,6 @@ INSERT INTO public.workflow_nodes ( '项目经理复审', true, now() - ), - ( - '22222222-aaaa-bbbb-cccc-222222222314', - '11111111-aaaa-bbbb-cccc-111111111212', - 3, - 'QA', - 'QA 终审', - true, - now() ) ON CONFLICT (id) DO UPDATE SET template_id = EXCLUDED.template_id, diff --git a/docs/reports/docker-fix-report.md b/docs/reports/docker-fix-report.md index 8cb6fd4e..04da710d 100644 --- a/docs/reports/docker-fix-report.md +++ b/docs/reports/docker-fix-report.md @@ -99,7 +99,7 @@ ImportError: cannot import name 'PROJECT_PERMISSION_ROLES' from 'app.core.api_pe ```python # backend/app/core/api_permissions.py -PROJECT_PERMISSION_ROLES = ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"] +PROJECT_PERMISSION_ROLES = ["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"] ``` **文件**: `backend/app/core/api_permissions.py` diff --git a/frontend/src/components/PermissionAccessLogs.test.ts b/frontend/src/components/PermissionAccessLogs.test.ts index 3f5a08e8..f70228db 100644 --- a/frontend/src/components/PermissionAccessLogs.test.ts +++ b/frontend/src/components/PermissionAccessLogs.test.ts @@ -29,6 +29,13 @@ describe("PermissionAccessLogs", () => { expect(source).not.toContain("fetchIpLocations"); }); + it("does not expose QA in role filter presets", () => { + const source = readSource(); + + expect(source).not.toContain('label="QA" value="QA"'); + expect(source).not.toContain('QA: "QA"'); + }); + it("uses narrow metric cards without helper subtitles", () => { const source = readSource(); diff --git a/frontend/src/components/PermissionAccessLogs.vue b/frontend/src/components/PermissionAccessLogs.vue index 20a2beb8..c369ea1f 100644 --- a/frontend/src/components/PermissionAccessLogs.vue +++ b/frontend/src/components/PermissionAccessLogs.vue @@ -18,7 +18,6 @@ - @@ -230,7 +229,6 @@ const ROLE_LABELS: Record = { PV: "PV", MEDICAL_REVIEW: "医学审核", IMP: "药品管理", - QA: "QA", }; const SECURITY_AUTH_LABELS: Record = { diff --git a/frontend/src/components/PermissionTemplateSelector.test.ts b/frontend/src/components/PermissionTemplateSelector.test.ts index 3318617a..2e7c9cf1 100644 --- a/frontend/src/components/PermissionTemplateSelector.test.ts +++ b/frontend/src/components/PermissionTemplateSelector.test.ts @@ -27,4 +27,11 @@ describe("PermissionTemplateSelector", () => { expect(source).toContain("const perms = props.currentPermissions?.[role];"); expect(source).toContain("emit('edit-role', template.category!)"); }); + + it("does not define QA as a preset role label or icon", () => { + const source = readSource(); + + expect(source).not.toContain('QA: "QA"'); + expect(source).not.toContain('QA: "✅"'); + }); }); diff --git a/frontend/src/components/PermissionTemplateSelector.vue b/frontend/src/components/PermissionTemplateSelector.vue index d4b6f735..db2bf526 100644 --- a/frontend/src/components/PermissionTemplateSelector.vue +++ b/frontend/src/components/PermissionTemplateSelector.vue @@ -83,13 +83,13 @@ const templates = ref([]); const ROLE_LABELS: Record = { PM: "项目负责人", CRA: "CRA", PV: "PV", - MEDICAL_REVIEW: "医学审核", IMP: "药品管理员", QA: "QA", + MEDICAL_REVIEW: "医学审核", IMP: "药品管理员", }; const roleIcon = (category: string | null) => { const icons: Record = { PM: "👑", CRA: "📋", PV: "🔍", - MEDICAL_REVIEW: "🏥", IMP: "📦", QA: "✅", + MEDICAL_REVIEW: "🏥", IMP: "📦", }; return icons[category ?? ""] ?? "📄"; }; diff --git a/frontend/src/dictionaries/role.dict.ts b/frontend/src/dictionaries/role.dict.ts index 9c1552ca..8a53754b 100644 --- a/frontend/src/dictionaries/role.dict.ts +++ b/frontend/src/dictionaries/role.dict.ts @@ -9,7 +9,6 @@ const items = [ { value: "PV", label: TEXT.enums.userRole.PV, order: 4 }, { value: "MEDICAL_REVIEW", label: TEXT.enums.userRole.MEDICAL_REVIEW, order: 5 }, { value: "IMP", label: TEXT.enums.userRole.IMP, order: 6 }, - { value: "QA", label: TEXT.enums.userRole.QA, order: 7 }, ]; export const roleDict: Dict = createDict(items); diff --git a/frontend/src/locales/zh-CN.ts b/frontend/src/locales/zh-CN.ts index e6b5d372..9c52e3b3 100644 --- a/frontend/src/locales/zh-CN.ts +++ b/frontend/src/locales/zh-CN.ts @@ -963,7 +963,6 @@ export const TEXT = { PV: "PV", MEDICAL_REVIEW: "医学审核", IMP: "药品管理员", - QA: "QA", }, userStatus: { PENDING: "待审核", diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts index 14e64758..57622801 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -32,7 +32,7 @@ export interface LoginKeyResponse { expires_at: string; } -export type UserRole = "PM" | "CRA" | "PV" | "MEDICAL_REVIEW" | "IMP" | "QA" | "ADMIN"; +export type UserRole = "PM" | "CRA" | "PV" | "MEDICAL_REVIEW" | "IMP" | "ADMIN"; export type UserStatus = "PENDING" | "ACTIVE" | "REJECTED" | "DISABLED"; export interface UserInfo { diff --git a/frontend/src/views/admin/PermissionManagement.test.ts b/frontend/src/views/admin/PermissionManagement.test.ts index fdcc15fa..82a66d40 100644 --- a/frontend/src/views/admin/PermissionManagement.test.ts +++ b/frontend/src/views/admin/PermissionManagement.test.ts @@ -47,10 +47,30 @@ describe("permission management custom roles", () => { expect(source).toContain("const roleLabel = (role: string) => ROLE_LABELS[role] || role;"); expect(source).toContain("const templateRoleName = (row: PermissionTemplate) => row.category ? roleLabel(row.category) : row.name;"); - expect(source).toContain("name: templateRoleName(row), description: row.description ?? \"\","); expect(source).toContain("roleDisplayName(row)"); }); + it("locks role type for preset edits and creates custom roles only", () => { + const source = readSource(); + const dialogStart = source.indexOf(""); + const dialogEnd = source.indexOf("", dialogStart); + const dialogSource = source.slice(dialogStart, dialogEnd); + + expect(dialogSource).toContain(':disabled="Boolean(editingTemplateId)"'); + expect(source).toContain("const openCreateTemplate = () =>"); + expect(source).toContain("const openEditTemplate = (row: PermissionTemplate) =>"); + expect(source).toContain('template_type: "CUSTOM"'); + expect(dialogSource).toContain(''); + expect(dialogSource).toContain(''); + expect(dialogSource).not.toContain(''); + expect(dialogSource).not.toContain('label="分类"'); + expect(dialogSource).not.toContain('label="推荐角色"'); + expect(dialogSource).not.toContain('label="标签"'); + expect(dialogSource).not.toContain("如 PM / CRA / PV"); + expect(dialogSource).not.toContain("如 PM,CRA"); + expect(dialogSource).not.toContain("可选标签"); + }); + it("keeps member role selects bound to role keys used by the permission matrix", () => { const source = readSource(); @@ -61,6 +81,18 @@ describe("permission management custom roles", () => { expect(source).toContain("await addMember(selectedStudyId.value, addMemberForm);"); }); + it("does not offer QA as a preset project permission role", () => { + const source = readSource(); + + const allRolesStart = source.indexOf("const ALL_ROLES = ["); + const allRolesEnd = source.indexOf("];", allRolesStart); + const allRolesSource = source.slice(allRolesStart, allRolesEnd); + + expect(allRolesSource).not.toContain('key: "QA"'); + expect(source).not.toContain('QA: "QA"'); + expect(source).not.toContain("QA: 60"); + }); + it("submits only the selected role from role editor saves", () => { const source = readSource(); @@ -69,6 +101,24 @@ describe("permission management custom roles", () => { expect(source).not.toContain("const payload = flattenMatrix(apiMatrix.value, { includePm: isAdmin.value });"); }); + it("submits only editable metadata when saving an existing role template", () => { + const source = readSource(); + const updateBuilderStart = source.indexOf("const buildTemplateUpdatePayload = () =>"); + const createBuilderStart = source.indexOf("const buildTemplateCreatePayload = ()"); + const submitStart = source.indexOf("const submitTemplateForm = async () =>", createBuilderStart); + const updateBuilderSource = source.slice(updateBuilderStart, createBuilderStart); + const createBuilderSource = source.slice(createBuilderStart, submitStart); + + expect(updateBuilderSource).toContain("name: templateForm.value.name"); + expect(updateBuilderSource).toContain("description: templateForm.value.description || undefined"); + expect(updateBuilderSource).not.toContain("template_type"); + expect(updateBuilderSource).not.toContain("permissions"); + expect(updateBuilderSource).not.toContain("category"); + expect(createBuilderSource).toContain("permissions: {}"); + expect(source).toContain("const payload = buildTemplateUpdatePayload();"); + expect(source).toContain("const payload = buildTemplateCreatePayload();"); + }); + it("renders setup_config as a Chinese module name in the role editor", () => { const source = readSource(); diff --git a/frontend/src/views/admin/PermissionManagement.vue b/frontend/src/views/admin/PermissionManagement.vue index 8eacd2cb..23c5b0fc 100644 --- a/frontend/src/views/admin/PermissionManagement.vue +++ b/frontend/src/views/admin/PermissionManagement.vue @@ -385,21 +385,11 @@ - - - - + + + - - - - - - - - -