feat: support contract template replace and delete
This commit is contained in:
@@ -9,6 +9,7 @@ from fastapi_modules.fastapi_leaudit.domian.Dto.contractTemplateDto import (
|
|||||||
ContractTemplateCreateDTO,
|
ContractTemplateCreateDTO,
|
||||||
ContractTemplateListQueryDTO,
|
ContractTemplateListQueryDTO,
|
||||||
ContractTemplateSearchQueryDTO,
|
ContractTemplateSearchQueryDTO,
|
||||||
|
ContractTemplateUpdateDTO,
|
||||||
)
|
)
|
||||||
from fastapi_modules.fastapi_leaudit.services.contractTemplateService import IContractTemplateService
|
from fastapi_modules.fastapi_leaudit.services.contractTemplateService import IContractTemplateService
|
||||||
from fastapi_modules.fastapi_leaudit.services.impl.contractTemplateServiceImpl import ContractTemplateServiceImpl
|
from fastapi_modules.fastapi_leaudit.services.impl.contractTemplateServiceImpl import ContractTemplateServiceImpl
|
||||||
@@ -82,7 +83,7 @@ class ContractTemplateController(BaseController):
|
|||||||
payload: dict = Depends(verify_access_token),
|
payload: dict = Depends(verify_access_token),
|
||||||
):
|
):
|
||||||
if not await self._check_permission(int(payload["user_id"]), ["contract_template:create:write"]):
|
if not await self._check_permission(int(payload["user_id"]), ["contract_template:create:write"]):
|
||||||
return JSONResponse(status_code=403, content={"code": 403, "msg": "当前仅允许租户管理员上传合同模板", "data": None})
|
return JSONResponse(status_code=403, content={"code": 403, "msg": "当前没有上传合同模板权限", "data": None})
|
||||||
body = ContractTemplateCreateDTO(
|
body = ContractTemplateCreateDTO(
|
||||||
title=title,
|
title=title,
|
||||||
template_code=template_code,
|
template_code=template_code,
|
||||||
@@ -129,13 +130,41 @@ class ContractTemplateController(BaseController):
|
|||||||
TemplateId: int,
|
TemplateId: int,
|
||||||
payload: dict = Depends(verify_access_token),
|
payload: dict = Depends(verify_access_token),
|
||||||
):
|
):
|
||||||
if not await self._check_permission(int(payload["user_id"]), ["contract_template:detail:read", "contract_template:list:read"]):
|
if not await self._check_permission(int(payload["user_id"]), ["contract_template:detail:read"]):
|
||||||
return JSONResponse(status_code=403, content={"code": 403, "msg": "当前用户没有查看合同模板详情权限", "data": None})
|
return JSONResponse(status_code=403, content={"code": 403, "msg": "当前用户没有查看合同模板详情权限", "data": None})
|
||||||
data = await self.ContractTemplateService.GetTemplateDetail(TemplateId, int(payload["user_id"]))
|
data = await self.ContractTemplateService.GetTemplateDetail(TemplateId, int(payload["user_id"]))
|
||||||
if not data:
|
if not data:
|
||||||
return JSONResponse(status_code=404, content={"code": 404, "msg": "合同模板不存在", "data": None})
|
return JSONResponse(status_code=404, content={"code": 404, "msg": "合同模板不存在", "data": None})
|
||||||
return JSONResponse(status_code=200, content={"code": 200, "message": "ok", "data": data.model_dump()})
|
return JSONResponse(status_code=200, content={"code": 200, "message": "ok", "data": data.model_dump()})
|
||||||
|
|
||||||
|
@self.router.put("/{TemplateId}")
|
||||||
|
async def UpdateContractTemplate(
|
||||||
|
TemplateId: int,
|
||||||
|
title: str = Form(...),
|
||||||
|
template_code: str = Form(...),
|
||||||
|
category_id: int = Form(...),
|
||||||
|
region: str | None = Form(default=None),
|
||||||
|
tenant_code: str | None = Form(default=None),
|
||||||
|
description: str | None = Form(default=None),
|
||||||
|
is_featured: bool = Form(default=False),
|
||||||
|
file: UploadFile = File(...),
|
||||||
|
pdf_file: UploadFile | None = File(default=None),
|
||||||
|
payload: dict = Depends(verify_access_token),
|
||||||
|
):
|
||||||
|
if not await self._check_permission(int(payload["user_id"]), ["contract_template:update:write"]):
|
||||||
|
return JSONResponse(status_code=403, content={"code": 403, "msg": "当前没有更新合同模板权限", "data": None})
|
||||||
|
body = ContractTemplateUpdateDTO(
|
||||||
|
title=title,
|
||||||
|
template_code=template_code,
|
||||||
|
category_id=category_id,
|
||||||
|
region=region,
|
||||||
|
tenant_code=tenant_code,
|
||||||
|
description=description,
|
||||||
|
is_featured=is_featured,
|
||||||
|
)
|
||||||
|
data = await self.ContractTemplateService.UpdateTemplate(TemplateId, body, file, pdf_file, int(payload["user_id"]))
|
||||||
|
return JSONResponse(status_code=200, content={"code": 200, "message": "ok", "data": data.model_dump()})
|
||||||
|
|
||||||
@self.router.delete("/{TemplateId}")
|
@self.router.delete("/{TemplateId}")
|
||||||
async def DeleteContractTemplate(
|
async def DeleteContractTemplate(
|
||||||
TemplateId: int,
|
TemplateId: int,
|
||||||
|
|||||||
@@ -41,3 +41,7 @@ class ContractTemplateCreateDTO(BaseModel):
|
|||||||
tenant_code: str | None = Field(None, description="所属租户编码")
|
tenant_code: str | None = Field(None, description="所属租户编码")
|
||||||
description: str | None = Field(None, description="模板简介")
|
description: str | None = Field(None, description="模板简介")
|
||||||
is_featured: bool = Field(False, description="是否推荐")
|
is_featured: bool = Field(False, description="是否推荐")
|
||||||
|
|
||||||
|
|
||||||
|
class ContractTemplateUpdateDTO(ContractTemplateCreateDTO):
|
||||||
|
"""合同模板替换上传参数。"""
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from fastapi_modules.fastapi_leaudit.domian.Dto.contractTemplateDto import (
|
|||||||
ContractTemplateCreateDTO,
|
ContractTemplateCreateDTO,
|
||||||
ContractTemplateListQueryDTO,
|
ContractTemplateListQueryDTO,
|
||||||
ContractTemplateSearchQueryDTO,
|
ContractTemplateSearchQueryDTO,
|
||||||
|
ContractTemplateUpdateDTO,
|
||||||
)
|
)
|
||||||
from fastapi_modules.fastapi_leaudit.domian.vo.contractTemplateVo import (
|
from fastapi_modules.fastapi_leaudit.domian.vo.contractTemplateVo import (
|
||||||
ContractTemplateCategoryVO,
|
ContractTemplateCategoryVO,
|
||||||
@@ -44,6 +45,17 @@ class IContractTemplateService(ABC):
|
|||||||
) -> ContractTemplateCreateVO:
|
) -> ContractTemplateCreateVO:
|
||||||
...
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def UpdateTemplate(
|
||||||
|
self,
|
||||||
|
TemplateId: int,
|
||||||
|
Body: ContractTemplateUpdateDTO,
|
||||||
|
File: UploadFile,
|
||||||
|
PdfFile: UploadFile | None,
|
||||||
|
CurrentUserId: int,
|
||||||
|
) -> ContractTemplateCreateVO:
|
||||||
|
...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def DeleteTemplate(self, TemplateId: int, CurrentUserId: int) -> None:
|
async def DeleteTemplate(self, TemplateId: int, CurrentUserId: int) -> None:
|
||||||
...
|
...
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from fastapi_modules.fastapi_leaudit.domian.Dto.contractTemplateDto import (
|
|||||||
ContractTemplateCreateDTO,
|
ContractTemplateCreateDTO,
|
||||||
ContractTemplateListQueryDTO,
|
ContractTemplateListQueryDTO,
|
||||||
ContractTemplateSearchQueryDTO,
|
ContractTemplateSearchQueryDTO,
|
||||||
|
ContractTemplateUpdateDTO,
|
||||||
)
|
)
|
||||||
from fastapi_modules.fastapi_leaudit.domian.vo.contractTemplateVo import (
|
from fastapi_modules.fastapi_leaudit.domian.vo.contractTemplateVo import (
|
||||||
ContractTemplateCategoryVO,
|
ContractTemplateCategoryVO,
|
||||||
@@ -225,6 +226,7 @@ class ContractTemplateServiceImpl(IContractTemplateService):
|
|||||||
LIMIT 1
|
LIMIT 1
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
(sql,) = self._bind_expanding(sql, params)
|
||||||
row = (await session.execute(sql, params)).mappings().first()
|
row = (await session.execute(sql, params)).mappings().first()
|
||||||
|
|
||||||
if not row:
|
if not row:
|
||||||
@@ -423,26 +425,215 @@ class ContractTemplateServiceImpl(IContractTemplateService):
|
|||||||
raise LeauditException(StatusCodeEnum.HTTP_500_INTERNAL_SERVER_ERROR, "合同模板创建成功但详情读取失败")
|
raise LeauditException(StatusCodeEnum.HTTP_500_INTERNAL_SERVER_ERROR, "合同模板创建成功但详情读取失败")
|
||||||
return ContractTemplateCreateVO(**detail.model_dump())
|
return ContractTemplateCreateVO(**detail.model_dump())
|
||||||
|
|
||||||
|
async def UpdateTemplate(
|
||||||
|
self,
|
||||||
|
TemplateId: int,
|
||||||
|
Body: ContractTemplateUpdateDTO,
|
||||||
|
File: UploadFile,
|
||||||
|
PdfFile: UploadFile | None,
|
||||||
|
CurrentUserId: int,
|
||||||
|
) -> ContractTemplateCreateVO:
|
||||||
|
if File is None or not File.filename:
|
||||||
|
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "替换模板主文件不能为空")
|
||||||
|
|
||||||
|
fileContent = await File.read()
|
||||||
|
if not fileContent:
|
||||||
|
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "替换模板主文件内容不能为空")
|
||||||
|
|
||||||
|
normalizedCode = (Body.template_code or "").strip()
|
||||||
|
normalizedTitle = (Body.title or "").strip()
|
||||||
|
if not normalizedCode:
|
||||||
|
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "模板编码不能为空")
|
||||||
|
if not normalizedTitle:
|
||||||
|
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "模板标题不能为空")
|
||||||
|
|
||||||
|
fileExt = Path(File.filename).suffix.lstrip(".").lower()
|
||||||
|
if fileExt not in {"doc", "docx", "pdf"}:
|
||||||
|
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "当前仅支持上传 DOC、DOCX、PDF 模板")
|
||||||
|
mimeType = File.content_type or mimetypes.guess_type(File.filename)[0] or "application/octet-stream"
|
||||||
|
|
||||||
|
pdfContent: bytes | None = None
|
||||||
|
pdfMimeType: str | None = None
|
||||||
|
pdfFileName: str | None = None
|
||||||
|
if PdfFile and PdfFile.filename:
|
||||||
|
pdfContent = await PdfFile.read()
|
||||||
|
if not pdfContent:
|
||||||
|
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "预览 PDF 文件内容不能为空")
|
||||||
|
pdfExt = Path(PdfFile.filename).suffix.lstrip(".").lower()
|
||||||
|
if pdfExt != "pdf":
|
||||||
|
raise LeauditException(StatusCodeEnum.HTTP_400_BAD_REQUEST, "预览文件仅支持 PDF")
|
||||||
|
pdfMimeType = PdfFile.content_type or "application/pdf"
|
||||||
|
pdfFileName = PdfFile.filename
|
||||||
|
|
||||||
|
async with GetAsyncSession() as session:
|
||||||
|
await self._ensureContractTemplateSchema(session)
|
||||||
|
currentUser = await self._getCurrentUserContext(CurrentUserId, session)
|
||||||
|
resolvedTenantCode, resolvedTenantName, resolvedRegion = self._resolve_upload_scope(currentUser, Body.region, Body.tenant_code)
|
||||||
|
|
||||||
|
params: dict[str, Any] = {"template_id": TemplateId}
|
||||||
|
scope_filters = self._build_template_scope_filters(currentUser, params, requestedRegion=None, writable=True)
|
||||||
|
existing_sql = text(
|
||||||
|
f"""
|
||||||
|
SELECT id
|
||||||
|
FROM contract_templates t
|
||||||
|
WHERE t.id = :template_id
|
||||||
|
AND t.deleted_at IS NULL
|
||||||
|
AND {' AND '.join(scope_filters)}
|
||||||
|
LIMIT 1
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
(existing_sql,) = self._bind_expanding(existing_sql, params)
|
||||||
|
existingRow = (await session.execute(existing_sql, params)).mappings().first()
|
||||||
|
if not existingRow:
|
||||||
|
raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "合同模板不存在或无权更新")
|
||||||
|
|
||||||
|
categoryRow = (
|
||||||
|
await session.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
SELECT id, name
|
||||||
|
FROM contract_categories
|
||||||
|
WHERE id = :category_id
|
||||||
|
AND deleted_at IS NULL
|
||||||
|
LIMIT 1
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{"category_id": Body.category_id},
|
||||||
|
)
|
||||||
|
).mappings().first()
|
||||||
|
if not categoryRow:
|
||||||
|
raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "合同模板分类不存在")
|
||||||
|
|
||||||
|
duplicateRow = (
|
||||||
|
await session.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
SELECT id
|
||||||
|
FROM contract_templates
|
||||||
|
WHERE (
|
||||||
|
tenant_code = :tenant_code
|
||||||
|
OR (
|
||||||
|
(tenant_code IS NULL OR BTRIM(tenant_code) = '')
|
||||||
|
AND region = :region
|
||||||
|
)
|
||||||
|
)
|
||||||
|
AND template_code = :template_code
|
||||||
|
AND id <> :template_id
|
||||||
|
AND deleted_at IS NULL
|
||||||
|
LIMIT 1
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{
|
||||||
|
"tenant_code": resolvedTenantCode,
|
||||||
|
"region": resolvedRegion,
|
||||||
|
"template_code": normalizedCode,
|
||||||
|
"template_id": TemplateId,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
).mappings().first()
|
||||||
|
if duplicateRow:
|
||||||
|
raise LeauditException(StatusCodeEnum.HTTP_409_CONFLICT, f"当前租户已存在模板编码 {normalizedCode}")
|
||||||
|
|
||||||
|
categoryName = str(categoryRow["name"] or "未分类")
|
||||||
|
objectKey = OssPathUtils.BuildContractTemplateKey(
|
||||||
|
Region=resolvedRegion,
|
||||||
|
CategoryName=categoryName,
|
||||||
|
TemplateCode=normalizedCode,
|
||||||
|
FileRole="source",
|
||||||
|
FileName=File.filename,
|
||||||
|
)
|
||||||
|
filePath = await self.OssService.UploadBytes(
|
||||||
|
ObjectKey=objectKey,
|
||||||
|
Content=fileContent,
|
||||||
|
ContentType=mimeType,
|
||||||
|
)
|
||||||
|
|
||||||
|
pdfPath: str | None = None
|
||||||
|
if pdfContent is not None and pdfFileName:
|
||||||
|
pdfObjectKey = OssPathUtils.BuildContractTemplateKey(
|
||||||
|
Region=resolvedRegion,
|
||||||
|
CategoryName=categoryName,
|
||||||
|
TemplateCode=normalizedCode,
|
||||||
|
FileRole="preview",
|
||||||
|
FileName=pdfFileName,
|
||||||
|
)
|
||||||
|
pdfPath = await self.OssService.UploadBytes(
|
||||||
|
ObjectKey=pdfObjectKey,
|
||||||
|
Content=pdfContent,
|
||||||
|
ContentType=pdfMimeType or "application/pdf",
|
||||||
|
)
|
||||||
|
|
||||||
|
await session.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
UPDATE contract_templates
|
||||||
|
SET template_code = :template_code,
|
||||||
|
title = :title,
|
||||||
|
category_id = :category_id,
|
||||||
|
tenant_code = :tenant_code,
|
||||||
|
tenant_name = :tenant_name,
|
||||||
|
region = :region,
|
||||||
|
description = :description,
|
||||||
|
file_path = :file_path,
|
||||||
|
pdf_file_path = :pdf_file_path,
|
||||||
|
file_format = :file_format,
|
||||||
|
original_file_name = :original_file_name,
|
||||||
|
mime_type = :mime_type,
|
||||||
|
file_size = :file_size,
|
||||||
|
pdf_file_size = :pdf_file_size,
|
||||||
|
is_featured = :is_featured,
|
||||||
|
updated_by = :updated_by,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = :template_id
|
||||||
|
AND deleted_at IS NULL
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{
|
||||||
|
"template_id": TemplateId,
|
||||||
|
"template_code": normalizedCode,
|
||||||
|
"title": normalizedTitle,
|
||||||
|
"category_id": Body.category_id,
|
||||||
|
"tenant_code": resolvedTenantCode,
|
||||||
|
"tenant_name": resolvedTenantName,
|
||||||
|
"region": resolvedRegion,
|
||||||
|
"description": (Body.description or "").strip() or None,
|
||||||
|
"file_path": filePath,
|
||||||
|
"pdf_file_path": pdfPath,
|
||||||
|
"file_format": fileExt,
|
||||||
|
"original_file_name": File.filename,
|
||||||
|
"mime_type": mimeType,
|
||||||
|
"file_size": len(fileContent),
|
||||||
|
"pdf_file_size": len(pdfContent) if pdfContent is not None else None,
|
||||||
|
"is_featured": Body.is_featured,
|
||||||
|
"updated_by": CurrentUserId,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
detail = await self.GetTemplateDetail(TemplateId, CurrentUserId)
|
||||||
|
if not detail:
|
||||||
|
raise LeauditException(StatusCodeEnum.HTTP_500_INTERNAL_SERVER_ERROR, "合同模板更新成功但详情读取失败")
|
||||||
|
return ContractTemplateCreateVO(**detail.model_dump())
|
||||||
|
|
||||||
async def DeleteTemplate(self, TemplateId: int, CurrentUserId: int) -> None:
|
async def DeleteTemplate(self, TemplateId: int, CurrentUserId: int) -> None:
|
||||||
async with GetAsyncSession() as session:
|
async with GetAsyncSession() as session:
|
||||||
await self._ensureContractTemplateSchema(session)
|
await self._ensureContractTemplateSchema(session)
|
||||||
currentUser = await self._getCurrentUserContext(CurrentUserId, session)
|
currentUser = await self._getCurrentUserContext(CurrentUserId, session)
|
||||||
params: dict[str, Any] = {"template_id": TemplateId}
|
params: dict[str, Any] = {"template_id": TemplateId}
|
||||||
scope_filters = self._build_template_scope_filters(currentUser, params, requestedRegion=None, writable=True)
|
scope_filters = self._build_template_scope_filters(currentUser, params, requestedRegion=None, writable=True)
|
||||||
|
sql = text(
|
||||||
|
f"""
|
||||||
|
SELECT id
|
||||||
|
FROM contract_templates t
|
||||||
|
WHERE t.id = :template_id
|
||||||
|
AND t.deleted_at IS NULL
|
||||||
|
AND {' AND '.join(scope_filters)}
|
||||||
|
LIMIT 1
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
(sql,) = self._bind_expanding(sql, params)
|
||||||
row = (
|
row = (
|
||||||
await session.execute(
|
await session.execute(sql, params)
|
||||||
text(
|
|
||||||
f"""
|
|
||||||
SELECT id
|
|
||||||
FROM contract_templates t
|
|
||||||
WHERE t.id = :template_id
|
|
||||||
AND t.deleted_at IS NULL
|
|
||||||
AND {' AND '.join(scope_filters)}
|
|
||||||
LIMIT 1
|
|
||||||
"""
|
|
||||||
),
|
|
||||||
params,
|
|
||||||
)
|
|
||||||
).mappings().first()
|
).mappings().first()
|
||||||
if not row:
|
if not row:
|
||||||
raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "合同模板不存在或无权删除")
|
raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "合同模板不存在或无权删除")
|
||||||
@@ -859,8 +1050,8 @@ class ContractTemplateServiceImpl(IContractTemplateService):
|
|||||||
current_tenant_code = str(currentUser.get("tenant_code") or "").strip() or None
|
current_tenant_code = str(currentUser.get("tenant_code") or "").strip() or None
|
||||||
current_tenant_name = str(currentUser.get("tenant_name") or "").strip() or None
|
current_tenant_name = str(currentUser.get("tenant_name") or "").strip() or None
|
||||||
current_region = str(currentUser["tenant_scope_value"] or currentUser["area"] or "").strip()
|
current_region = str(currentUser["tenant_scope_value"] or currentUser["area"] or "").strip()
|
||||||
if not currentUser.get("is_area_admin"):
|
if not currentUser.get("can_manage"):
|
||||||
raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, "当前仅支持租户管理员上传合同模板")
|
raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, "当前仅允许有权限的管理员上传合同模板")
|
||||||
if not current_region:
|
if not current_region:
|
||||||
raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, "当前租户管理员账号未配置所属租户,无法上传合同模板")
|
raise LeauditException(StatusCodeEnum.HTTP_403_FORBIDDEN, "当前租户管理员账号未配置所属租户,无法上传合同模板")
|
||||||
if requested_tenant_code and not current_tenant_code:
|
if requested_tenant_code and not current_tenant_code:
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from fastapi_common.fastapi_common_web.exception.LeauditException import LeauditException
|
||||||
from fastapi_modules.fastapi_leaudit.services.impl.contractTemplateServiceImpl import ContractTemplateServiceImpl
|
from fastapi_modules.fastapi_leaudit.services.impl.contractTemplateServiceImpl import ContractTemplateServiceImpl
|
||||||
|
|
||||||
|
|
||||||
@@ -11,6 +14,9 @@ class _EmptyMappingResult:
|
|||||||
def all(self):
|
def all(self):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
def first(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class _FakeSession:
|
class _FakeSession:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -66,3 +72,95 @@ def test_contract_template_search_category_stats_binds_expanding_scope_params():
|
|||||||
assert fake_session.executed_sql._bindparams["visible_regions"].expanding is True
|
assert fake_session.executed_sql._bindparams["visible_regions"].expanding is True
|
||||||
assert fake_session.executed_params["visible_tenant_codes"] == ["PROVINCIAL", "PUBLIC", "MZ"]
|
assert fake_session.executed_params["visible_tenant_codes"] == ["PROVINCIAL", "PUBLIC", "MZ"]
|
||||||
assert fake_session.executed_params["visible_regions"] == ["省级", "公共", "梅州"]
|
assert fake_session.executed_params["visible_regions"] == ["省级", "公共", "梅州"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_contract_template_upload_scope_allows_global_manager_with_tenant():
|
||||||
|
service = ContractTemplateServiceImpl()
|
||||||
|
|
||||||
|
tenant_code, tenant_name, region = service._resolve_upload_scope(
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"area": "梅州",
|
||||||
|
"tenant_code": "MZ",
|
||||||
|
"tenant_name": "梅州",
|
||||||
|
"tenant_scope_value": "梅州",
|
||||||
|
"is_global": True,
|
||||||
|
"can_manage": True,
|
||||||
|
"is_area_admin": False,
|
||||||
|
},
|
||||||
|
"梅州",
|
||||||
|
"MZ",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert tenant_code == "MZ"
|
||||||
|
assert tenant_name == "梅州"
|
||||||
|
assert region == "梅州"
|
||||||
|
|
||||||
|
|
||||||
|
def test_contract_template_upload_scope_rejects_non_manager():
|
||||||
|
service = ContractTemplateServiceImpl()
|
||||||
|
|
||||||
|
with pytest.raises(LeauditException):
|
||||||
|
service._resolve_upload_scope(
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"area": "梅州",
|
||||||
|
"tenant_code": "MZ",
|
||||||
|
"tenant_name": "梅州",
|
||||||
|
"tenant_scope_value": "梅州",
|
||||||
|
"is_global": False,
|
||||||
|
"can_manage": False,
|
||||||
|
"is_area_admin": False,
|
||||||
|
},
|
||||||
|
"梅州",
|
||||||
|
"MZ",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_contract_template_detail_binds_expanding_scope_params():
|
||||||
|
service = ContractTemplateServiceImpl()
|
||||||
|
fake_session = _FakeSession()
|
||||||
|
|
||||||
|
async def noop_ensure_schema(session):
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def fake_user_context(current_user_id, session):
|
||||||
|
return {
|
||||||
|
"id": current_user_id,
|
||||||
|
"area": "梅州",
|
||||||
|
"tenant_code": "MZ",
|
||||||
|
"tenant_name": "梅州",
|
||||||
|
"tenant_scope_value": "梅州",
|
||||||
|
"is_global": False,
|
||||||
|
"can_manage": True,
|
||||||
|
"is_area_admin": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
service._ensureContractTemplateSchema = noop_ensure_schema
|
||||||
|
service._getCurrentUserContext = fake_user_context
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"fastapi_modules.fastapi_leaudit.services.impl.contractTemplateServiceImpl.GetAsyncSession",
|
||||||
|
return_value=_FakeSessionContext(fake_session),
|
||||||
|
):
|
||||||
|
result = asyncio.run(service.GetTemplateDetail(32, 5))
|
||||||
|
|
||||||
|
assert result is None
|
||||||
|
assert fake_session.executed_sql._bindparams["visible_tenant_codes"].expanding is True
|
||||||
|
assert fake_session.executed_sql._bindparams["visible_regions"].expanding is True
|
||||||
|
assert fake_session.executed_params["visible_tenant_codes"] == ["PROVINCIAL", "PUBLIC", "MZ"]
|
||||||
|
assert fake_session.executed_params["visible_regions"] == ["省级", "公共", "梅州"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_contract_template_detail_permission_does_not_fallback_to_list_permission():
|
||||||
|
from fastapi_modules.fastapi_leaudit.controllers.contractTemplateController import ContractTemplateController
|
||||||
|
|
||||||
|
controller = ContractTemplateController()
|
||||||
|
|
||||||
|
class FakePermissionService:
|
||||||
|
async def CheckPermission(self, user_id, permission_key):
|
||||||
|
return permission_key == "contract_template:list:read"
|
||||||
|
|
||||||
|
controller.PermissionService = FakePermissionService()
|
||||||
|
|
||||||
|
assert asyncio.run(controller._check_permission(5, ["contract_template:detail:read"])) is False
|
||||||
|
|||||||
Reference in New Issue
Block a user