Skip to content

Commit

Permalink
feat: add collaboration apis (#1690)
Browse files Browse the repository at this point in the history
  • Loading branch information
narasux authored May 8, 2024
1 parent 20c37c5 commit 8307152
Show file tree
Hide file tree
Showing 36 changed files with 1,216 additions and 126 deletions.
10 changes: 10 additions & 0 deletions src/bk-user/bkuser/apis/web/collaboration/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
"""
TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-用户管理(Bk-User) available.
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at http://opensource.org/licenses/MIT
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
specific language governing permissions and limitations under the License.
"""
222 changes: 222 additions & 0 deletions src/bk-user/bkuser/apis/web/collaboration/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
# -*- coding: utf-8 -*-
"""
TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-用户管理(Bk-User) available.
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at http://opensource.org/licenses/MIT
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
specific language governing permissions and limitations under the License.
"""
from typing import Any, Dict, List

import pydantic
from django.utils.translation import gettext_lazy as _
from drf_yasg.utils import swagger_serializer_method
from rest_framework import serializers
from rest_framework.exceptions import ValidationError

from bkuser.apps.sync.models import TenantSyncTask
from bkuser.apps.tenant.constants import CollaborationStrategyStatus, UserFieldDataType
from bkuser.apps.tenant.data_models import CollaborationStrategySourceConfig, CollaborationStrategyTargetConfig
from bkuser.apps.tenant.models import CollaborationStrategy, Tenant, TenantUserCustomField
from bkuser.utils.pydantic import stringify_pydantic_error

# ---------------------------------- 分享方 SLZ ----------------------------------


class CollaborationToStrategyListOutputSLZ(serializers.Serializer):
id = serializers.IntegerField(help_text="协同策略 ID")
name = serializers.CharField(help_text="协同策略名称")
target_tenant_id = serializers.CharField(help_text="目标租户 ID")
target_tenant_name = serializers.SerializerMethodField(help_text="目标租户名称")
creator = serializers.SerializerMethodField(help_text="创建人")
created_at = serializers.CharField(help_text="创建时间", source="created_at_display")
source_status = serializers.ChoiceField(
help_text="策略状态(分享方)",
choices=CollaborationStrategyStatus.get_choices(),
)
source_config = serializers.JSONField(help_text="策略配置(分享方)")

@swagger_serializer_method(serializer_or_field=serializers.CharField)
def get_target_tenant_name(self, obj: CollaborationStrategy) -> str:
return self.context["tenant_name_map"][obj.target_tenant_id]

@swagger_serializer_method(serializer_or_field=serializers.CharField)
def get_creator(self, obj: CollaborationStrategy) -> str:
return self.context["user_display_name_map"][obj.creator]


class CollaborationToStrategyCreateInputSLZ(serializers.Serializer):
name = serializers.CharField(help_text="协同策略名称")
target_tenant_id = serializers.CharField(help_text="目标租户 ID")
source_config = serializers.JSONField(help_text="策略配置")

def validate_name(self, name: str) -> str:
if CollaborationStrategy.objects.filter(name=name, source_tenant_id=self.context["tenant_id"]).exists():
raise ValidationError(_("同名协同策略已存在"))

return name

def validate_target_tenant_id(self, target_tenant_id: int) -> int:
if target_tenant_id == self.context["tenant_id"]:
raise ValidationError(_("目标租户不能是当前租户"))

if not Tenant.objects.filter(id=target_tenant_id).exists():
raise ValidationError(_("目标租户不存在"))

return target_tenant_id

def validate_source_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
try:
CollaborationStrategySourceConfig(**config)
except pydantic.ValidationError as e:
raise ValidationError(_("策略配置不合法:{}").format(stringify_pydantic_error(e)))

return config

def validate(self, attrs: Dict[str, Any]) -> Dict[str, Any]:
if CollaborationStrategy.objects.filter(
source_tenant=self.context["tenant_id"], target_tenant_id=attrs["target_tenant_id"]
).exists():
raise ValidationError(_("当前租户到租户 {} 的协同策略已存在").format(attrs["target_tenant_id"]))
return attrs


class CollaborationToStrategyCreateOutputSLZ(serializers.Serializer):
id = serializers.IntegerField(help_text="协同策略 ID")


class CollaborationToStrategyUpdateInputSLZ(serializers.Serializer):
name = serializers.CharField(help_text="协同策略名称")
source_config = serializers.JSONField(help_text="策略配置(分享方)")

def validate_name(self, name: str) -> str:
if (
CollaborationStrategy.objects.filter(name=name, source_tenant_id=self.context["tenant_id"])
.exclude(id=self.context["strategy_id"])
.exists()
):
raise ValidationError(_("同名协同策略已存在"))

return name

def validate_source_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
try:
CollaborationStrategySourceConfig(**config)
except pydantic.ValidationError as e:
raise ValidationError(_("策略配置不合法:{}").format(stringify_pydantic_error(e)))

return config


class CollaborationToStrategySourceStatusUpdateOutputSLZ(serializers.Serializer):
source_status = serializers.ChoiceField(help_text="策略状态", choices=CollaborationStrategyStatus.get_choices())


# ---------------------------------- 接受方 SLZ ----------------------------------


class CollaborationFromStrategyListOutputSLZ(serializers.Serializer):
id = serializers.IntegerField(help_text="协同策略 ID")
source_tenant_id = serializers.CharField(help_text="源租户 ID")
source_tenant_name = serializers.SerializerMethodField(help_text="源租户名称")
updated_at = serializers.CharField(help_text="最近更新时间", source="updated_at_display")
target_status = serializers.ChoiceField(
help_text="策略状态(接受方)",
choices=CollaborationStrategyStatus.get_choices(),
)
source_config = serializers.JSONField(help_text="策略配置(分享方)")
target_config = serializers.JSONField(help_text="策略配置(接受方)")

@swagger_serializer_method(serializer_or_field=serializers.CharField)
def get_source_tenant_name(self, obj: CollaborationStrategy) -> str:
return self.context["tenant_name_map"][obj.source_tenant_id]


class CollaborationSourceTenantCustomFieldListOutputSLZ(serializers.Serializer):
name = serializers.CharField(help_text="英文标识")
display_name = serializers.CharField(help_text="展示用名称")
data_type = serializers.ChoiceField(help_text="字段类型", choices=UserFieldDataType.get_choices())


def _validate_field_mapping_with_tenant_user_fields(
field_mapping: List[Dict[str, str]], source_tenant_id: str, target_tenant_id: str
):
"""校验协同策略中的字段映射,是否能匹配源 / 目标租户的自定义字段"""

source_tenant_custom_field_map = {
f.name: f for f in TenantUserCustomField.objects.filter(tenant_id=source_tenant_id)
}
target_tenant_custom_field_map = {
f.name: f for f in TenantUserCustomField.objects.filter(tenant_id=target_tenant_id)
}

# 源字段检查(是否合法)
source_fields = {m.get("source_field") for m in field_mapping}
if not_allowed_fields := source_fields - set(source_tenant_custom_field_map.keys()):
raise ValidationError(_("字段映射中的源字段 {} 不属于源租户用户自定义字段").format(not_allowed_fields))

# 目标字段检查(是否合法) 注:不需要检查必填字段是否已配置
target_fields = {m.get("target_field") for m in field_mapping}
if not_allowed_fields := target_fields - set(target_tenant_custom_field_map.keys()):
raise ValidationError(
_("字段映射中的目标字段 {} 不属于本租户用户自定义字段").format(not_allowed_fields),
)

for mp in field_mapping:
source_field = source_tenant_custom_field_map[mp["source_field"]]
target_field = target_tenant_custom_field_map[mp["target_field"]]
# 字段类型检查
if source_field.data_type != target_field.data_type:
raise ValidationError(
_("字段映射中的源字段 {} 和 目标字段 {} 的类型不一致").format(mp["source_field"], mp["target_field"]),
)

# 如果是枚举类型,还要校验枚举值是否一致(字面值可以不一致)
# TODO (su) 评估策略创建后,某一方修改枚举值的影响?
if source_field.data_type in [UserFieldDataType.ENUM, UserFieldDataType.MULTI_ENUM]: # noqa: SIM102
if {opt["id"] for opt in source_field.options} != {opt["id"] for opt in target_field.options}:
raise ValidationError(
_(
"字段映射中的源字段 {} 和 目标字段 {} 的枚举值不一致",
).format(mp["source_field"], mp["target_field"]),
)


class CollaborationFromStrategyUpdateInputSLZ(serializers.Serializer):
target_config = serializers.JSONField(help_text="策略配置(接受方)")

def validate_target_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
try:
CollaborationStrategyTargetConfig(**config)
except pydantic.ValidationError as e:
raise ValidationError(_("策略配置不合法:{}").format(stringify_pydantic_error(e)))

_validate_field_mapping_with_tenant_user_fields(
config["field_mapping"], self.context["source_tenant_id"], self.context["target_tenant_id"]
)
return config


class CollaborationFromStrategyConfirmInputSLZ(CollaborationFromStrategyUpdateInputSLZ):
...


class CollaborationFromStrategyTargetStatusUpdateOutputSLZ(serializers.Serializer):
target_status = serializers.ChoiceField(help_text="策略状态", choices=CollaborationStrategyStatus.get_choices())


class CollaborationSyncRecordListOutputSLZ(serializers.Serializer):
id = serializers.IntegerField(help_text="同步记录 ID")
source_tenant_id = serializers.CharField(help_text="源租户 ID", source="data_source.owner_tenant_id")
source_tenant_name = serializers.SerializerMethodField(help_text="源租户名称")
has_warning = serializers.BooleanField(help_text="是否有警告")
status = serializers.CharField(help_text="同步状态")
start_at = serializers.DateTimeField(help_text="创建时间")
duration = serializers.DurationField(help_text="持续时间")
summary = serializers.JSONField(help_text="任务执行概述")

@swagger_serializer_method(serializer_or_field=serializers.CharField)
def get_source_tenant_name(self, obj: TenantSyncTask) -> str:
return self.context["tenant_name_map"][obj.data_source.owner_tenant_id]
72 changes: 72 additions & 0 deletions src/bk-user/bkuser/apis/web/collaboration/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
"""
TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-用户管理(Bk-User) available.
Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at http://opensource.org/licenses/MIT
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
specific language governing permissions and limitations under the License.
"""
from django.urls import path

from . import views

urlpatterns = [
# ----------------------------- 分享方 -----------------------------
# 本租户分享给其他租户的协同策略 / 新建协同策略(分享)给其他租户
path(
"to-strategies/",
views.CollaborationToStrategyListCreateApi.as_view(),
name="collaboration.to-strategy.list_create",
),
# 编辑 / 删除协同策略(分享方)
path(
"to-strategies/<str:id>/",
views.CollaborationToStrategyUpdateDestroyApi.as_view(),
name="collaboration.to-strategy.update",
),
# 更新协同策略状态(分享方)
path(
"to-strategies/<str:id>/source-status/",
views.CollaborationToStrategySourceStatusUpdateApi.as_view(),
name="collaboration.to-strategy.source-status.update",
),
# ----------------------------- 接受方 -----------------------------
# 其他租户分享给本租户的协同策略
path(
"from-strategies/",
views.CollaborationFromStrategyListApi.as_view(),
name="collaboration.from-strategy.list",
),
# 更新协同策略(接受方)
path(
"from-strategies/<str:id>/",
views.CollaborationFromStrategyUpdateApi.as_view(),
name="collaboration.from-strategy.update",
),
# 获取源租户用户自定义字段(接受方)
path(
"from-strategies/<str:id>/source-tenant-custom-fields/",
views.CollaborationStrategySourceTenantCustomFieldListApi.as_view(),
name="collaboration.from-strategy.source-tenant-custom-fields",
),
# 确认协同策略(接受方)
path(
"from-strategies/<str:id>/operations/confirm/",
views.CollaborationFromStrategyConfirmApi.as_view(),
name="collaboration.from-strategy.confirm",
),
# 更新协同策略状态(接受方)
path(
"from-strategies/<str:id>/target-status/",
views.CollaborationFromStrategyTargetStatusUpdateApi.as_view(),
name="collaboration.from-strategy.target-status.update",
),
# 协同数据更新记录
path(
"sync-records/",
views.CollaborationSyncRecordListApi.as_view(),
name="collaboration.sync-record.list",
),
]
Loading

0 comments on commit 8307152

Please sign in to comment.