Skip to content

Commit

Permalink
feat(organization): 查询租户部门下租户用户列表/租户用户详情
Browse files Browse the repository at this point in the history
查询租户部门下租户用户列表/租户用户详情

feat #1131 #1146
  • Loading branch information
neronkl committed Aug 22, 2023
1 parent 42e7fba commit 93eaa4e
Show file tree
Hide file tree
Showing 7 changed files with 475 additions and 3 deletions.
10 changes: 10 additions & 0 deletions src/bk-user/bkuser/apis/web/organization/__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.
"""
118 changes: 118 additions & 0 deletions src/bk-user/bkuser/apis/web/organization/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# -*- 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 Dict, List

from django.conf import settings
from drf_yasg.utils import swagger_serializer_method
from rest_framework import serializers

from bkuser.apps.tenant.models import TenantUser
from bkuser.biz.tenant import TenantUserHandler


class TenantUserDepartmentOutputSLZ(serializers.Serializer):
id = serializers.IntegerField(help_text="租户用户ID")
name = serializers.CharField(help_text="租户部门名称")


class TenantUserLeaderOutputSLZ(serializers.Serializer):
id = serializers.IntegerField(help_text="租户用户ID")
username = serializers.CharField(help_text="上级用户名")
full_name = serializers.BooleanField(help_text="上级名称")


class TenantDepartmentUserSearchInputSLZ(serializers.Serializer):
page = serializers.IntegerField(required=False, default=1)
page_size = serializers.IntegerField(required=False, default=10)
recursive = serializers.BooleanField(help_text="仅仅展示该部门下人员", default=True)
keyword = serializers.CharField(help_text="搜索关键字", required=False)


class TenantDepartmentUserOutputSLZ(serializers.Serializer):
id = serializers.CharField(help_text="租户用户ID")
username = serializers.CharField(help_text="租户用户名", required=False)
full_name = serializers.CharField(help_text="用户姓名", required=False)
email = serializers.EmailField(help_text="用户邮箱", required=False)
phone = serializers.CharField(help_text="用户手机号", required=False)
phone_country_code = serializers.CharField(
help_text="手机号国际区号", required=False, default=settings.DEFAULT_PHONE_COUNTRY_CODE
)
account_expired_at = serializers.DateTimeField(help_text="账号过期时间")
departments = serializers.SerializerMethodField(help_text="用户所属部门")
leaders = serializers.SerializerMethodField(help_text="用户上级")

@swagger_serializer_method(serializer_or_field=TenantUserDepartmentOutputSLZ(many=True))
def get_departments(self, instance: TenantUser) -> List[Dict]:
departments = self.context["tenant_user_departments"].get(instance.id, [])
if not departments:
return []
return [i.model_dump(include={"id", "name"}) for i in departments]

@swagger_serializer_method(serializer_or_field=TenantUserLeaderOutputSLZ(many=True))
def get_leaders(self, instance: TenantUser) -> List[Dict]:
leader_infos = self.context["tenant_user_leaders"].get(instance.id)
if not leader_infos:
return []
return [
{
"id": i.id,
**i.data_source_user.model_dump(include={"username", "full_name"}),
}
for i in leader_infos
]

def to_representation(self, instance: TenantUser) -> Dict:
data = super().to_representation(instance)

user = instance.data_source_user
if user:
data["full_name"] = user.full_name
data["username"] = user.username
data["email"] = user.email
data["phone"] = user.phone
data["phone_country_code"] = user.phone_country_code
data["logo"] = user.logo or settings.DEFAULT_DATA_SOURCE_USER_LOGO
return data


class TenantUserRetrieveOutputSLZ(TenantDepartmentUserOutputSLZ):
@swagger_serializer_method(serializer_or_field=TenantUserDepartmentOutputSLZ(many=True))
def get_departments(self, instance: TenantUser) -> List[Dict]:
tenant_user_departments = TenantUserHandler.get_tenant_user_departments_by_id([instance.id])
if not tenant_user_departments:
return []
departments = tenant_user_departments[0].departments
return [i.model_dump(include={"id", "name"}) for i in departments]

@swagger_serializer_method(serializer_or_field=TenantUserLeaderOutputSLZ(many=True))
def get_leaders(self, instance: TenantUser) -> List[Dict]:
tenant_user_leaders = TenantUserHandler.get_tenant_user_leaders_by_id([instance.id])
if not tenant_user_leaders:
return []
leader_infos = tenant_user_leaders[0].leaders
return [
{
"id": i.id,
**i.data_source_user.model_dump(include={"username", "full_name"}),
}
for i in leader_infos
]

def to_representation(self, instance: TenantUser) -> Dict:
data = super().to_representation(instance)
user = instance.data_source_user
data["full_name"] = user.full_name
data["username"] = user.username
data["email"] = user.email
data["phone"] = user.phone
data["phone_country_code"] = user.phone_country_code
data["logo"] = user.logo or settings.DEFAULT_DATA_SOURCE_USER_LOGO
return data
19 changes: 19 additions & 0 deletions src/bk-user/bkuser/apis/web/organization/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# -*- 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("departments/<int:id>/users/", views.TenantDepartmentUserListApi.as_view(), name="departments.users.list"),
path("users/<str:id>/", views.TenantUsersRetrieveApi.as_view(), name="department.users.retrieve"),
]
101 changes: 101 additions & 0 deletions src/bk-user/bkuser/apis/web/organization/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# -*- 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.db.models import Q
from drf_yasg.utils import swagger_auto_schema
from rest_framework import generics, status
from rest_framework.response import Response

from bkuser.apis.web.organization.serializers import (
TenantDepartmentUserOutputSLZ,
TenantDepartmentUserSearchInputSLZ,
TenantUserRetrieveOutputSLZ,
)
from bkuser.apps.tenant.models import TenantDepartment, TenantUser
from bkuser.biz.tenant import TenantUserHandler
from bkuser.common.pagination import CustomPageNumberPagination

logger = logging.getLogger(__name__)


class TenantDepartmentUserListApi(generics.ListAPIView):
queryset = TenantUser.objects.all()
lookup_url_kwarg = "id"
pagination_class = CustomPageNumberPagination
serializer_class = TenantDepartmentUserOutputSLZ

def get_serializer_context(self):
tenant_department = TenantDepartment.objects.get(id=self.kwargs[self.lookup_url_kwarg])
tenant_user_ids = TenantUser.objects.filter(tenant_id=tenant_department.tenant_id).values_list("id", flat=True)

# 租户用户基础信息
tenant_users = TenantUserHandler.list_tenant_user_by_id(tenant_user_ids)
tenant_users_info_map = {i.id: i for i in tenant_users}

# 租户用户上级信息
tenant_user_leaders = TenantUserHandler.get_tenant_user_leaders_by_id(tenant_user_ids)
tenant_user_leader_map = {i.id: i.leaders for i in tenant_user_leaders}

# 租户用户所属租户组织
tenant_user_departments = TenantUserHandler.get_tenant_user_departments_by_id(tenant_user_ids)
tenant_user_department_map = {i.id: i.departments for i in tenant_user_departments}

return {
"tenant_users_info": tenant_users_info_map,
"tenant_user_leaders": tenant_user_leader_map,
"tenant_user_departments": tenant_user_department_map,
}

@swagger_auto_schema(
operation_description="租户部门下用户详情列表",
responses={status.HTTP_200_OK: TenantDepartmentUserOutputSLZ(many=True)},
)
def get(self, request, *args, **kwargs):
slz = TenantDepartmentUserSearchInputSLZ(data=self.request.query_params)
slz.is_valid(raise_exception=True)
data = slz.validated_data
recursive = data.get("recursive")
keyword = data.get("keyword")
# 过滤该租户部门下的用户
tenant_user_ids = TenantUserHandler.get_tenant_users_by_tenant_department(
tenant_department_id=self.kwargs["id"], recursive=recursive
)

# build response
queryset = self.filter_queryset(self.get_queryset())
if keyword:
queryset = queryset.prefetch_related("data_source_user").filter(
Q(data_source_user__username__icontains=keyword)
| Q(data_source_user__email__icontains=keyword)
| Q(data_source_user__phone__icontains=keyword),
id__in=tenant_user_ids,
)
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)

serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)


class TenantUsersRetrieveApi(generics.RetrieveAPIView):
queryset = TenantUser.objects.all()
lookup_url_kwarg = "id"
serializer_class = TenantUserRetrieveOutputSLZ

@swagger_auto_schema(
operation_description="租户部门下单个用户详情",
responses={status.HTTP_200_OK: TenantUserRetrieveOutputSLZ()},
)
def get(self, request, *args, **kwargs):
return self.retrieve(request, *args, **kwargs)
1 change: 1 addition & 0 deletions src/bk-user/bkuser/apis/web/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@
path("basic/", include("bkuser.apis.web.basic.urls")),
# 租户
path("tenants/", include("bkuser.apis.web.tenant.urls")),
path("tenant-organization/", include("bkuser.apis.web.organization.urls")),
]
59 changes: 58 additions & 1 deletion src/bk-user/bkuser/biz/data_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,19 @@

from pydantic import BaseModel

from bkuser.apps.data_source.models import DataSource
from bkuser.apps.data_source.models import (
DataSource,
DataSourceDepartment,
DataSourceDepartmentRelation,
DataSourceDepartmentUserRelation,
DataSourceUser,
)


class DataSourceDepartmentInfoWithChildren(BaseModel):
id: int
name: str
children: List[int]


class DataSourceSimpleInfo(BaseModel):
Expand All @@ -38,3 +50,48 @@ def get_data_source_map_by_owner(
data[i.owner_tenant_id].append(DataSourceSimpleInfo(id=i.id, name=i.name))

return data

@staticmethod
def get_data_source_ids_by_tenant(tenant_id: str) -> List[int]:
# 当前属于租户的数据源
data_source_ids = DataSource.objects.filter(owner_tenant_id=tenant_id).values_list("id", flat=True)
# TODO 协同数据源获取
return list(data_source_ids)


class DataSourceDepartmentHandler:
@staticmethod
def get_department_info_by_id(department_ids: List[int]) -> Dict[int, DataSourceDepartmentInfoWithChildren]:
"""
获取部门基础信息
"""
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] = DataSourceDepartmentInfoWithChildren(
id=item.id,
name=item.name,
children=list(children.values_list("department_id", flat=True)),
)
return departments_map

@staticmethod
def get_users_by_department_id(department_id: int, recursive: bool = True) -> List[str]:
# 是否返回子部门用户
if recursive:
user_ids = DataSourceDepartmentUserRelation.objects.filter(department_id=department_id).values_list(
"user_id"
)
else:
department = DataSourceDepartmentRelation.objects.get(department_id=department_id)
recursive_department_ids = department.get_descendants(include_self=True).values_list(
"department_id", flat=True
)
user_ids = DataSourceDepartmentUserRelation.objects.filter(
department_id__in=recursive_department_ids
).values_list("user_id")

# 过滤数据源用户
data_source_users = DataSourceUser.objects.filter(id__in=user_ids)
return list(data_source_users.values_list("id", flat=True))
Loading

0 comments on commit 93eaa4e

Please sign in to comment.