From 325cfce3587df7df9d6ff9e8c496a931dba9c007 Mon Sep 17 00:00:00 2001 From: nero <827392902@qq.com> Date: Fri, 18 Aug 2023 18:28:12 +0800 Subject: [PATCH] =?UTF-8?q?code=20review=20=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__init__.py | 0 .../apis/web/organization/serializers.py | 40 +++++++ .../urls.py | 4 +- .../views.py | 60 ++++++---- .../bkuser/apis/web/tenant/serializers.py | 2 +- .../web/tenant_organization/serializers.py | 106 ------------------ src/bk-user/bkuser/apis/web/urls.py | 2 +- src/bk-user/bkuser/biz/data_source.py | 40 ++----- src/bk-user/bkuser/biz/tenant.py | 47 ++++---- 9 files changed, 121 insertions(+), 180 deletions(-) rename src/bk-user/bkuser/apis/web/{tenant_organization => organization}/__init__.py (100%) create mode 100644 src/bk-user/bkuser/apis/web/organization/serializers.py rename src/bk-user/bkuser/apis/web/{tenant_organization => organization}/urls.py (85%) rename src/bk-user/bkuser/apis/web/{tenant_organization => organization}/views.py (57%) delete mode 100644 src/bk-user/bkuser/apis/web/tenant_organization/serializers.py diff --git a/src/bk-user/bkuser/apis/web/tenant_organization/__init__.py b/src/bk-user/bkuser/apis/web/organization/__init__.py similarity index 100% rename from src/bk-user/bkuser/apis/web/tenant_organization/__init__.py rename to src/bk-user/bkuser/apis/web/organization/__init__.py diff --git a/src/bk-user/bkuser/apis/web/organization/serializers.py b/src/bk-user/bkuser/apis/web/organization/serializers.py new file mode 100644 index 000000000..a818b75ad --- /dev/null +++ b/src/bk-user/bkuser/apis/web/organization/serializers.py @@ -0,0 +1,40 @@ +# -*- 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. +""" +import logging + +from django.conf import settings +from drf_yasg.utils import swagger_serializer_method +from rest_framework import serializers + +from bkuser.apps.tenant.models import Tenant + +logger = logging.getLogger(__name__) + + +class TenantDepartmentOutputSLZ(serializers.Serializer): + id = serializers.IntegerField(help_text="租户部门ID") + name = serializers.CharField(help_text="部门名称") + has_children = serializers.BooleanField(help_text="是否有子部门") + + +class TenantListOutputSLZ(serializers.Serializer): + id = serializers.CharField(help_text="租户组织节点ID") + name = serializers.CharField(help_text="租户组织节点名称名") + logo = serializers.SerializerMethodField(help_text="租户 Logo") + departments = serializers.SerializerMethodField(help_text="租户下每个数据源的根组织") + + def get_logo(self, instance: Tenant) -> str: + return instance.logo or settings.DEFAULT_TENANT_LOGO + + @swagger_serializer_method(serializer_or_field=TenantDepartmentOutputSLZ(many=True)) + def get_departments(self, instance: Tenant): + departments = self.context["tenant_root_department_map"].get(instance.id) + return [item.model_dump() for item in departments.values()] diff --git a/src/bk-user/bkuser/apis/web/tenant_organization/urls.py b/src/bk-user/bkuser/apis/web/organization/urls.py similarity index 85% rename from src/bk-user/bkuser/apis/web/tenant_organization/urls.py rename to src/bk-user/bkuser/apis/web/organization/urls.py index bcf8bc986..2cc95bc9d 100644 --- a/src/bk-user/bkuser/apis/web/tenant_organization/urls.py +++ b/src/bk-user/bkuser/apis/web/organization/urls.py @@ -14,6 +14,6 @@ urlpatterns = [ # 租户 - path("tenants/", views.TenantsListApi.as_view(), name="tenant_departments.retrieve_tenant"), - path("tenants//", views.TenantRetrieveUpdateApi.as_view(), name="tenant_departments.retrieve_tenant"), + path("tenants/", views.TenantListApi.as_view(), name="organization.tenant.list"), + path("tenants//", views.TenantRetrieveUpdateApi.as_view(), name="organization.tenant.retrieve_update"), ] diff --git a/src/bk-user/bkuser/apis/web/tenant_organization/views.py b/src/bk-user/bkuser/apis/web/organization/views.py similarity index 57% rename from src/bk-user/bkuser/apis/web/tenant_organization/views.py rename to src/bk-user/bkuser/apis/web/organization/views.py index e7630abc8..a770bf080 100644 --- a/src/bk-user/bkuser/apis/web/tenant_organization/views.py +++ b/src/bk-user/bkuser/apis/web/organization/views.py @@ -14,56 +14,71 @@ from rest_framework import generics, status from rest_framework.response import Response -from bkuser.apis.web.tenant_organization.serializers import ( - TenantsListOutputSLZ, - TenantsRetrieveOutputSLZ, - TenantUpdateInputSLZ, -) +from bkuser.apis.web.organization.serializers import TenantListOutputSLZ +from bkuser.apis.web.tenant.serializers import TenantRetrieveOutputSLZ, TenantUpdateInputSLZ +from bkuser.apps.data_source.models import DataSourceDepartmentRelation from bkuser.apps.tenant.models import Tenant +from bkuser.biz.data_source import DataSourceDepartmentHandler, DataSourceHandler from bkuser.biz.tenant import ( + TenantDepartmentHandler, TenantEditableBaseInfo, TenantFeatureFlag, TenantHandler, ) +from bkuser.common.error_codes import error_codes from bkuser.common.views import ExcludePatchAPIViewMixin logger = logging.getLogger(__name__) -class TenantsListApi(generics.ListAPIView): +class TenantListApi(generics.ListAPIView): pagination_class = None queryset = Tenant.objects.all() - serializer_class = TenantsListOutputSLZ + serializer_class = TenantListOutputSLZ def _get_tenant_id(self) -> str: - # TODO 根据登录用户获取租户 - return self.queryset.first().id + return self.request.user.get_property("tenant_id") def get_serializer_context(self): - return {"current_tenant_id": self._get_tenant_id()} - - def get_queryset(self): - tenant_id = self._get_tenant_id() - # TODO 协同数据源, 以租户的形式展示, 如何获取? - return Tenant.objects.filter(id__in=[tenant_id]) + current_tenant_id = self._get_tenant_id() + tenant_root_department_map = {} + # 获取旗下根部门的各个数据源 + # TODO 协同数据源,是以租户的形式展示,考虑怎么获取授权的数据源? + tenant_data_sources = DataSourceHandler.get_data_source_map_by_owner([current_tenant_id]) + for tenant_id, data_sources in tenant_data_sources.items(): + data_sources_ids = [i.id for i in data_sources] + # 通过获取数据源的根节点 + root_departments = ( + DataSourceDepartmentRelation.objects.root_nodes() + .filter(data_source_id__in=data_sources_ids) + .values_list("department_id", flat=True) + ) + # 获取部门基础信息 + departments_infos = DataSourceDepartmentHandler.list_departments_info(root_departments) + tenant_root_department_map[ + tenant_id + ] = TenantDepartmentHandler.convert_data_source_department_to_tenant_department( + tenant_id=current_tenant_id, data_source_departments=departments_infos + ) + return {"tenant_root_department_map": tenant_root_department_map} @swagger_auto_schema( operation_description="租户列表", - responses={status.HTTP_200_OK: TenantsListOutputSLZ(many=True)}, + responses={status.HTTP_200_OK: TenantListOutputSLZ(many=True)}, ) def get(self, request, *args, **kwargs): + # TODO 协同数据源, 以租户的形式展示, 如何获取? return self.list(request, *args, **kwargs) class TenantRetrieveUpdateApi(ExcludePatchAPIViewMixin, generics.RetrieveUpdateAPIView): queryset = Tenant.objects.all() pagination_class = None - serializer_class = TenantsRetrieveOutputSLZ + serializer_class = TenantRetrieveOutputSLZ lookup_url_kwarg = "id" def _get_tenant_id(self) -> str: - # TODO 根据当前租户获取租户ID - return self.queryset.first().id + return self.request.user.get_property("tenant_id") def get_serializer_context(self): # 根据当前登录的租户用户,获取租户ID @@ -75,7 +90,7 @@ def get_serializer_context(self): @swagger_auto_schema( operation_description="单个租户详情", - responses={status.HTTP_200_OK: TenantsRetrieveOutputSLZ()}, + responses={status.HTTP_200_OK: TenantRetrieveOutputSLZ()}, ) def get(self, request, *args, **kwargs): return self.retrieve(request, *args, **kwargs) @@ -93,12 +108,11 @@ def put(self, request, *args, **kwargs): instance = self.get_object() # NOTE 因协同数据源,而展示的租户,不返回管理员 if self._get_tenant_id() != instance.id: - return Response + raise error_codes.NO_PERMISSION should_updated_info = TenantEditableBaseInfo( - name=data["name"], logo=data.get("logo") or "", feature_flags=TenantFeatureFlag(**data["feature_flags"]) + name=data["name"], logo=data["logo"] or "", feature_flags=TenantFeatureFlag(**data["feature_flags"]) ) TenantHandler.update_with_managers(instance.id, should_updated_info, data["manager_ids"]) - return Response() diff --git a/src/bk-user/bkuser/apis/web/tenant/serializers.py b/src/bk-user/bkuser/apis/web/tenant/serializers.py index 3c5cfe19d..38f6024f5 100644 --- a/src/bk-user/bkuser/apis/web/tenant/serializers.py +++ b/src/bk-user/bkuser/apis/web/tenant/serializers.py @@ -100,7 +100,7 @@ def get_data_sources(self, obj: Tenant) -> List[Dict]: class TenantUpdateInputSLZ(serializers.Serializer): name = serializers.CharField(help_text="租户名称") - logo = serializers.CharField(help_text="租户 Logo", required=False) + logo = serializers.CharField(help_text="租户 Logo", required=False, default=settings.DEFAULT_TENANT_LOGO) manager_ids = serializers.ListField(child=serializers.CharField(), help_text="租户用户 ID 列表", allow_empty=False) feature_flags = TenantFeatureFlagSLZ(help_text="租户特性集") diff --git a/src/bk-user/bkuser/apis/web/tenant_organization/serializers.py b/src/bk-user/bkuser/apis/web/tenant_organization/serializers.py deleted file mode 100644 index a084ee59f..000000000 --- a/src/bk-user/bkuser/apis/web/tenant_organization/serializers.py +++ /dev/null @@ -1,106 +0,0 @@ -# -*- 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. -""" -import logging -from typing import List - -from django.conf import settings -from drf_yasg.utils import swagger_serializer_method -from mptt.templatetags.mptt_tags import cache_tree_children -from rest_framework import serializers - -from bkuser.apis.web.tenant.serializers import TenantFeatureFlagSLZ, TenantRetrieveManagerOutputSchema -from bkuser.apps.data_source.models import DataSourceDepartmentRelation -from bkuser.biz.data_source import DataSourceHandler -from bkuser.biz.tenant import TenantDepartmentHandler - -logger = logging.getLogger(__name__) - - -class TenantDepartmentOutputSchema(serializers.Serializer): - id = serializers.IntegerField(help_text="租户部门ID") - name = serializers.CharField(help_text="部门名称") - has_children = serializers.BooleanField(help_text="是否有子部门") - - -class TenantUpdateInputSLZ(serializers.Serializer): - name = serializers.CharField(help_text="租户名称") - logo = serializers.CharField(help_text="租户 Logo", required=False) - manager_ids = serializers.ListField(child=serializers.CharField(), help_text="租户用户 ID 列表", allow_empty=False) - feature_flags = TenantFeatureFlagSLZ(help_text="租户特性集") - - -class TenantsListOutputSLZ(serializers.Serializer): - id = serializers.CharField(help_text="租户组织节点ID") - name = serializers.CharField(help_text="租户组织节点名称名") - logo = serializers.SerializerMethodField(help_text="租户 Logo") - departments = serializers.SerializerMethodField(help_text="租户下每个数据源的根组织") - - def _list_departments_info(self, current_tenant_id, departments): - data = [] - # 此处已经过滤出和当前租户绑定组织 - tenant_department_map = TenantDepartmentHandler.get_tenant_departments_info_map( - current_tenant_id, [item.department.id for item in departments] - ) - for item in departments: - # NOTE:协同数据源,部分根部门并未授权到当前部门 - tenant_department = tenant_department_map.get(item.department.id, None) - if not tenant_department: - logger.info(f"DataSourceDepartment<{item.department.id}> does not bind Tenant<{current_tenant_id}>") - continue - data.append(tenant_department.model_dump()) - return data - - def _get_root_departments(self, instance): - # 协同数据源,是以租户的形式展示,转换为当前租户 - tenant_id = instance.id - current_tenant_id = self.context["current_tenant_id"] - # 获取旗下根部门的各个数据源 - # TODO 协同数据源,是以租户的形式展示,考虑怎么获取授权的数据源? - # 通过获取数据源的根节点 - data_sources = DataSourceHandler.get_data_source_map_by_owner([tenant_id])[tenant_id] - data_sources_ids = [i.id for i in data_sources] - # 获取租户下所有数据员的根组织 - data_source_root_departments = cache_tree_children( - DataSourceDepartmentRelation.objects.filter(level=0, data_source_id__in=data_sources_ids) - ) - return self._list_departments_info(current_tenant_id, data_source_root_departments) - - def get_logo(self, instance) -> str: - return instance.logo or settings.DEFAULT_TENANT_LOGO - - @swagger_serializer_method(serializer_or_field=TenantDepartmentOutputSchema(many=True)) - def get_departments(self, instance): - return self._get_root_departments(instance) - - -class TenantsRetrieveOutputSLZ(serializers.Serializer): - id = serializers.CharField(help_text="租户ID") - name = serializers.CharField(help_text="租户名") - logo = serializers.SerializerMethodField(help_text="租户 Logo") - feature_flags = TenantFeatureFlagSLZ(help_text="租户特性集") - managers = serializers.SerializerMethodField() - - @swagger_serializer_method(serializer_or_field=TenantRetrieveManagerOutputSchema(many=True)) - def get_managers(self, instance) -> List[dict]: - tenant_manager_map = self.context["tenant_manager_map"] - managers = tenant_manager_map.get(instance.id, []) - return [ - { - "id": i.id, - **i.data_source_user.model_dump( - include={"username", "full_name", "email", "phone", "phone_country_code"} - ), - } - for i in managers - ] - - def get_logo(self, instance) -> str: - return instance.logo or settings.DEFAULT_TENANT_LOGO diff --git a/src/bk-user/bkuser/apis/web/urls.py b/src/bk-user/bkuser/apis/web/urls.py index 103a4f5c5..62a4ae9cb 100644 --- a/src/bk-user/bkuser/apis/web/urls.py +++ b/src/bk-user/bkuser/apis/web/urls.py @@ -15,5 +15,5 @@ path("basic/", include("bkuser.apis.web.basic.urls")), # 租户 path("tenants/", include("bkuser.apis.web.tenant.urls")), - path("tenant-organization-tree/", include("bkuser.apis.web.tenant_organization.urls")), + path("tenant-organization-tree/", include("bkuser.apis.web.organization.urls")), ] diff --git a/src/bk-user/bkuser/biz/data_source.py b/src/bk-user/bkuser/biz/data_source.py index 7793b2d5a..568915df1 100644 --- a/src/bk-user/bkuser/biz/data_source.py +++ b/src/bk-user/bkuser/biz/data_source.py @@ -14,15 +14,12 @@ from pydantic import BaseModel from bkuser.apps.data_source.models import DataSource, DataSourceDepartment, DataSourceDepartmentRelation -from bkuser.common.error_codes import error_codes class DataSourceDepartmentInfo(BaseModel): id: int name: str - data_source_id: int - children: list - full_name: str + children: List[int] class DataSourceSimpleInfo(BaseModel): @@ -51,30 +48,17 @@ def get_data_source_map_by_owner( class DataSourceDepartmentHandler: @staticmethod - def retrieve_department(department_id: int) -> DataSourceDepartmentInfo: + def list_departments_info(department_ids: List[int]) -> Dict[int, DataSourceDepartmentInfo]: """ - 获取单个部门信息 + 获取部门基础信息 """ - try: - # 生成组织架构路径 - parent_ids = ( - DataSourceDepartmentRelation.objects.get(id=department_id) - .get_ancestors(include_self=True) - .values_list("id", flat=True) + departments = DataSourceDepartment.objects.filter(id__in=department_ids) + departments_map: Dict = {} + for item in departments: + children = DataSourceDepartmentRelation.objects.get(department=item).get_children() + departments_map[item.id] = DataSourceDepartmentInfo( + id=item.id, + name=item.name, + children=list(children.values_list("department_id", flat=True)), ) - parents = DataSourceDepartment.objects.filter(id__in=parent_ids).values_list("name", flat=True) - full_name = "/".join(list(parents)) - # 构建基础信息 - department = DataSourceDepartment.objects.get(id=department_id) - base_info = { - "id": department.id, - "name": department.name, - "full_name": full_name, - "data_source_id": department.data_source_id, - "children": DataSourceDepartmentRelation.objects.filter(parent_id=department.id).values( - "id", flat=True - ), - } - return DataSourceDepartmentInfo(**base_info) - except DataSourceDepartment.DoesNotExist: - raise error_codes.OBJECT_NOT_FOUND + return departments_map diff --git a/src/bk-user/bkuser/biz/tenant.py b/src/bk-user/bkuser/biz/tenant.py index e2eac919d..774c07fa2 100644 --- a/src/bk-user/bkuser/biz/tenant.py +++ b/src/bk-user/bkuser/biz/tenant.py @@ -14,8 +14,9 @@ from django.db import transaction from pydantic import BaseModel -from bkuser.apps.data_source.models import DataSource, DataSourceDepartmentRelation, DataSourcePlugin, DataSourceUser +from bkuser.apps.data_source.models import DataSource, DataSourcePlugin, DataSourceUser from bkuser.apps.tenant.models import Tenant, TenantDepartment, TenantManager, TenantUser +from bkuser.biz.data_source import DataSourceDepartmentInfo from bkuser.utils.uuid import generate_uuid @@ -68,7 +69,7 @@ class TenantManagerWithoutID(BaseModel): class TenantDepartmentBaseInfo(BaseModel): - tenant_department_id: int + id: int name: str has_children: bool @@ -196,8 +197,8 @@ def update_with_managers(tenant_id: str, tenant_info: TenantEditableBaseInfo, ma class TenantDepartmentHandler: @staticmethod - def get_tenant_departments_info_map( - tenant_id: str, data_source_departments: Optional[List[int]] = None + def convert_data_source_department_to_tenant_department( + tenant_id: str, data_source_departments: Dict[int, DataSourceDepartmentInfo] ) -> Dict[int, TenantDepartmentBaseInfo]: """ 获取租户的部门映射 @@ -205,20 +206,28 @@ def get_tenant_departments_info_map( # tenant_id 租户下部门关系映射 tenant_departments = TenantDepartment.objects.filter(tenant_id=tenant_id) if data_source_departments: - tenant_departments = tenant_departments.filter(data_source_department_id__in=data_source_departments) - # 构建映射关系数据 - departments_map: dict = {} - for item in tenant_departments: # NOTE: 协同数据源,可能存在未授权全部子部门 - children_ids = DataSourceDepartmentRelation.objects.filter( - parent_id=item.data_source_department.id - ).values_list("department_id", flat=True) - has_children = TenantDepartment.objects.filter(data_source_department_id__in=children_ids).exists() - departments_map[item.data_source_department.id] = TenantDepartmentBaseInfo( - **{ - "tenant_department_id": item.id, - "name": item.data_source_department.name, - "has_children": has_children, - } + # 提前拉取所有映射, 过滤绑定的租户部门 + department_ids = list(data_source_departments.keys()) + for department in data_source_departments.values(): + department_ids += department.children + tenant_departments = tenant_departments.filter(data_source_department_id__in=department_ids) + tenant_departments_ids = tenant_departments.values_list("data_source_department_id", flat=True) + # 构建映射关系数据 + tenant_departments_map: dict = {} + for department in tenant_departments: + data_source_department_id = department.data_source_department_id + # data_source_departments 没有有子部门信息 + if data_source_department_id not in data_source_departments: + continue + data_source_department_info = data_source_departments[data_source_department_id] + # 只要一个子部门被授权,都是存在子部门 + has_children = any( + True for child in data_source_department_info.children if child in tenant_departments_ids + ) + tenant_departments_map[data_source_department_id] = TenantDepartmentBaseInfo( + id=department.id, + name=data_source_department_info.name, + has_children=has_children, ) - return departments_map + return tenant_departments_map