From 6faf444193d86ee894e7ceff34461a2a613079ae Mon Sep 17 00:00:00 2001 From: rolin999 Date: Fri, 10 Jan 2025 15:32:19 +0800 Subject: [PATCH] feat: added query user leader information by bk_username (#2031) --- .../bkuser/apis/open_v3/serializers/user.py | 8 +++ src/bk-user/bkuser/apis/open_v3/urls.py | 5 ++ .../bkuser/apis/open_v3/views/__init__.py | 15 +++++- src/bk-user/bkuser/apis/open_v3/views/user.py | 33 ++++++++++++ .../apidocs/en/list_user_leader.md | 53 +++++++++++++++++++ .../apidocs/zh/list_user_leader.md | 53 +++++++++++++++++++ src/bk-user/support-files/resources.yaml | 25 +++++++++ src/bk-user/tests/apis/open_v3/test_user.py | 46 +++++++++++++--- 8 files changed, 228 insertions(+), 10 deletions(-) create mode 100644 src/bk-user/support-files/apidocs/en/list_user_leader.md create mode 100644 src/bk-user/support-files/apidocs/zh/list_user_leader.md diff --git a/src/bk-user/bkuser/apis/open_v3/serializers/user.py b/src/bk-user/bkuser/apis/open_v3/serializers/user.py index a7d36545f..177be1742 100644 --- a/src/bk-user/bkuser/apis/open_v3/serializers/user.py +++ b/src/bk-user/bkuser/apis/open_v3/serializers/user.py @@ -62,3 +62,11 @@ class TenantUserDepartmentListOutputSLZ(serializers.Serializer): id = serializers.IntegerField(help_text="部门 ID") name = serializers.CharField(help_text="部门名称") ancestors = serializers.ListField(help_text="祖先部门列表", required=False, child=AncestorSLZ(), allow_empty=True) + + +class TenantUserLeaderListOutputSLZ(serializers.Serializer): + bk_username = serializers.CharField(help_text="蓝鲸用户唯一标识", source="id") + display_name = serializers.SerializerMethodField(help_text="用户展示名称") + + def get_display_name(self, obj: TenantUser) -> str: + return TenantUserHandler.generate_tenant_user_display_name(obj) diff --git a/src/bk-user/bkuser/apis/open_v3/urls.py b/src/bk-user/bkuser/apis/open_v3/urls.py index 05fc86580..c57a79910 100644 --- a/src/bk-user/bkuser/apis/open_v3/urls.py +++ b/src/bk-user/bkuser/apis/open_v3/urls.py @@ -40,6 +40,11 @@ views.TenantUserDepartmentListApi.as_view(), name="open_v3.tenant_user.department.list", ), + path( + "users//leaders/", + views.TenantUserLeaderListApi.as_view(), + name="open_v3.tenant_user.leaders.list", + ), ] ), ), diff --git a/src/bk-user/bkuser/apis/open_v3/views/__init__.py b/src/bk-user/bkuser/apis/open_v3/views/__init__.py index 05d381a18..09b78b64b 100644 --- a/src/bk-user/bkuser/apis/open_v3/views/__init__.py +++ b/src/bk-user/bkuser/apis/open_v3/views/__init__.py @@ -16,6 +16,17 @@ # to the current version of the project delivered to anyone in the future. from .tenant import TenantListApi -from .user import TenantUserDepartmentListApi, TenantUserDisplayNameListApi, TenantUserRetrieveApi +from .user import ( + TenantUserDepartmentListApi, + TenantUserDisplayNameListApi, + TenantUserLeaderListApi, + TenantUserRetrieveApi, +) -__all__ = ["TenantListApi", "TenantUserDisplayNameListApi", "TenantUserRetrieveApi", "TenantUserDepartmentListApi"] +__all__ = [ + "TenantListApi", + "TenantUserDisplayNameListApi", + "TenantUserRetrieveApi", + "TenantUserDepartmentListApi", + "TenantUserLeaderListApi", +] diff --git a/src/bk-user/bkuser/apis/open_v3/views/user.py b/src/bk-user/bkuser/apis/open_v3/views/user.py index 34b88f623..28dbe4b7b 100644 --- a/src/bk-user/bkuser/apis/open_v3/views/user.py +++ b/src/bk-user/bkuser/apis/open_v3/views/user.py @@ -17,6 +17,7 @@ import logging from typing import Dict, List +from django.db.models import QuerySet from drf_yasg.utils import swagger_auto_schema from rest_framework import generics, status from rest_framework.generics import get_object_or_404 @@ -28,12 +29,14 @@ TenantUserDepartmentListOutputSLZ, TenantUserDisplayNameListInputSLZ, TenantUserDisplayNameListOutputSLZ, + TenantUserLeaderListOutputSLZ, TenantUserRetrieveOutputSLZ, ) from bkuser.apps.data_source.models import ( DataSourceDepartment, DataSourceDepartmentRelation, DataSourceDepartmentUserRelation, + DataSourceUserLeaderRelation, ) from bkuser.apps.tenant.models import TenantDepartment, TenantUser @@ -187,3 +190,33 @@ def _get_dept_ancestors(dept_id: int) -> List[int]: return [] # 返回的祖先部门默认以降序排列,从根祖先部门 -> 父部门 return list(relation.get_ancestors().values_list("department_id", flat=True)) + + +class TenantUserLeaderListApi(OpenApiCommonMixin, generics.ListAPIView): + """ + 根据用户 bk_username 获取用户 Leader 列表信息 + """ + + pagination_class = None + + serializer_class = TenantUserLeaderListOutputSLZ + + def get_queryset(self) -> QuerySet[TenantUser]: + tenant_user = get_object_or_404(TenantUser.objects.all(), id=self.kwargs["id"]) + + leader_ids = list( + DataSourceUserLeaderRelation.objects.filter(user=tenant_user.data_source_user).values_list( + "leader_id", flat=True + ) + ) + + return TenantUser.objects.filter(data_source_user_id__in=leader_ids, tenant_id=tenant_user.tenant_id) + + @swagger_auto_schema( + tags=["open_v3.user"], + operation_id="list_user_leader", + operation_description="查询用户 Leader 列表", + responses={status.HTTP_200_OK: TenantUserLeaderListOutputSLZ(many=True)}, + ) + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) diff --git a/src/bk-user/support-files/apidocs/en/list_user_leader.md b/src/bk-user/support-files/apidocs/en/list_user_leader.md new file mode 100644 index 000000000..1c099c692 --- /dev/null +++ b/src/bk-user/support-files/apidocs/en/list_user_leader.md @@ -0,0 +1,53 @@ +### Description + +Query user's list of leaders + +### Parameters + +| Name | Type | Required | Description | +|----------------|---------|----------|------------------------------------------------| +| bk_username | string | Yes | Blueking user's unique identifier | + +### Request Example + +``` +// URL Path Parameter +/api/v3/open/tenant/users/mzmwjffhhbjg4fxz/leaders/ +``` + +### Response Example for Status Code 200 + +```json5 +{ + "data": [ + { + "bk_username": "q9k6bhqks0ckl5ew", + "display_name": "张三" + }, + { + "bk_username": "er0ugcammqwf1q5w", + "display_name": "李四" + } + ] +} +``` + +### Response Parameters Description + +| Name | Type | Description | +|--------------|--------|-----------------------------------| +| bk_username | string | Blueking user's unique identifier | +| display_name | string | User's display_name | + + +# Response Example for Non-200 Status Code + +```json5 +// status_code = 404 +{ + "error": { + "code": "NOT_FOUND", + "message": "Object not found" + } +} +``` diff --git a/src/bk-user/support-files/apidocs/zh/list_user_leader.md b/src/bk-user/support-files/apidocs/zh/list_user_leader.md new file mode 100644 index 000000000..713cb507b --- /dev/null +++ b/src/bk-user/support-files/apidocs/zh/list_user_leader.md @@ -0,0 +1,53 @@ +### 描述 + +查询用户 Leader 列表 + +### 输入参数 + +| 参数名称 | 参数类型 | 必选 | 描述 | +|----------------|---------|----|--------------------| +| bk_username | string | 是 | 蓝鲸用户唯一标识 | + +### 请求示例 + +``` +// URL Path 参数 +/api/v3/open/tenant/users/mzmwjffhhbjg4fxz/leaders/ +``` + +### 状态码 200 的响应示例 + +```json5 +{ + "data": [ + { + "bk_username": "q9k6bhqks0ckl5ew", + "display_name": "张三" + }, + { + "bk_username": "er0ugcammqwf1q5w", + "display_name": "李四" + } + ] +} +``` + +### 响应参数说明 + +| 参数名称 | 参数类型 | 描述 | +|--------------|--------|----------| +| bk_username | string | 蓝鲸用户唯一标识 | +| display_name | string | 用户展示名称 | + + +### 状态码非 200 的响应示例 + +```json5 +// status_code = 404 +{ + "error": { + "code": "NOT_FOUND", + "message": "对象未找到" + } +} +``` diff --git a/src/bk-user/support-files/resources.yaml b/src/bk-user/support-files/resources.yaml index f5fdbd526..2958c76ce 100644 --- a/src/bk-user/support-files/resources.yaml +++ b/src/bk-user/support-files/resources.yaml @@ -106,3 +106,28 @@ paths: appVerifiedRequired: true resourcePermissionRequired: true descriptionEn: query user's list of departments + + /api/v3/open/tenant/users/{bk_username}/leaders/: + get: + operationId: list_user_leader + description: 查询用户 Leader 列表 + tags: [] + responses: + default: + description: '' + x-bk-apigateway-resource: + isPublic: true + allowApplyPermission: false + matchSubpath: false + backend: + name: default + method: get + path: /api/v3/open/tenant/users/{bk_username}/leaders/ + matchSubpath: false + timeout: 0 + pluginConfigs: [] + authConfig: + userVerifiedRequired: false + appVerifiedRequired: true + resourcePermissionRequired: true + descriptionEn: Query user's list of leaders diff --git a/src/bk-user/tests/apis/open_v3/test_user.py b/src/bk-user/tests/apis/open_v3/test_user.py index 9a8a5c86f..7639c09d8 100644 --- a/src/bk-user/tests/apis/open_v3/test_user.py +++ b/src/bk-user/tests/apis/open_v3/test_user.py @@ -26,8 +26,8 @@ @pytest.mark.usefixtures("_init_tenant_users_depts") class TestTenantUserDisplayNameListApi: def test_standard(self, api_client): - zhangsan_id = TenantUser.objects.get(data_source_user__code="zhangsan").id - lisi_id = TenantUser.objects.get(data_source_user__code="lisi").id + zhangsan_id = TenantUser.objects.get(data_source_user__username="zhangsan").id + lisi_id = TenantUser.objects.get(data_source_user__username="lisi").id resp = api_client.get( reverse("open_v3.tenant_user.display_name.list"), data={"bk_usernames": ",".join([zhangsan_id, lisi_id])} ) @@ -38,7 +38,7 @@ def test_standard(self, api_client): assert {t["display_name"] for t in resp.data} == {"张三", "李四"} def test_with_invalid_bk_usernames(self, api_client): - zhangsan_id = TenantUser.objects.get(data_source_user__code="zhangsan").id + zhangsan_id = TenantUser.objects.get(data_source_user__username="zhangsan").id resp = api_client.get( reverse("open_v3.tenant_user.display_name.list"), data={"bk_usernames": ",".join([zhangsan_id, "invalid"])} ) @@ -67,7 +67,7 @@ def test_with_invalid_length(self, api_client): @pytest.mark.usefixtures("_init_tenant_users_depts") class TestTenantUserRetrieveApi: def test_standard(self, api_client, random_tenant): - zhangsan = TenantUser.objects.get(data_source_user__code="zhangsan") + zhangsan = TenantUser.objects.get(data_source_user__username="zhangsan") resp = api_client.get(reverse("open_v3.tenant_user.retrieve", kwargs={"id": zhangsan.id})) assert resp.status_code == status.HTTP_200_OK assert resp.data["bk_username"] == zhangsan.id @@ -85,7 +85,7 @@ def test_tenant_not_found(self, api_client): class TestTenantUserDepartmentListApi: def test_with_not_ancestors(self, api_client): # with_ancestors = False - zhangsan = TenantUser.objects.get(data_source_user__code="zhangsan") + zhangsan = TenantUser.objects.get(data_source_user__username="zhangsan") company = TenantDepartment.objects.get(data_source_department__name="公司") resp = api_client.get(reverse("open_v3.tenant_user.department.list", kwargs={"id": zhangsan.id})) assert resp.status_code == status.HTTP_200_OK @@ -94,7 +94,7 @@ def test_with_not_ancestors(self, api_client): assert "ancestors" not in resp.data[0] def test_with_no_ancestors(self, api_client): - zhangsan = TenantUser.objects.get(data_source_user__code="zhangsan") + zhangsan = TenantUser.objects.get(data_source_user__username="zhangsan") company = TenantDepartment.objects.get(data_source_department__name="公司") resp = api_client.get( reverse("open_v3.tenant_user.department.list", kwargs={"id": zhangsan.id}), data={"with_ancestors": True} @@ -105,7 +105,7 @@ def test_with_no_ancestors(self, api_client): assert resp.data[0]["ancestors"] == [] def test_with_ancestors(self, api_client): - lisi = TenantUser.objects.get(data_source_user__code="lisi") + lisi = TenantUser.objects.get(data_source_user__username="lisi") company = TenantDepartment.objects.get(data_source_department__name="公司") dept_a = TenantDepartment.objects.get(data_source_department__name="部门A") dept_aa = TenantDepartment.objects.get(data_source_department__name="中心AA") @@ -123,9 +123,39 @@ def test_with_invalid_user(self, api_client): assert resp.status_code == status.HTTP_404_NOT_FOUND def test_with_no_department(self, api_client): - freedom = TenantUser.objects.get(data_source_user__code="freedom") + freedom = TenantUser.objects.get(data_source_user__username="freedom") resp = api_client.get( reverse("open_v3.tenant_user.department.list", kwargs={"id": freedom.id}), data={"with_ancestors": True} ) assert resp.status_code == status.HTTP_200_OK assert len(resp.data) == 0 + + +@pytest.mark.usefixtures("_init_tenant_users_depts") +class TestTenantUserLeaderListApi: + def test_with_single_leader(self, api_client): + lisi = TenantUser.objects.get(data_source_user__username="lisi") + zhangsan = TenantUser.objects.get(data_source_user__username="zhangsan") + resp = api_client.get(reverse("open_v3.tenant_user.leaders.list", kwargs={"id": lisi.id})) + assert resp.status_code == status.HTTP_200_OK + assert resp.data[0]["bk_username"] == zhangsan.id + assert resp.data[0]["display_name"] == "张三" + + def test_with_multiple_leader(self, api_client): + lisi = TenantUser.objects.get(data_source_user__username="lisi") + wangwu = TenantUser.objects.get(data_source_user__username="wangwu") + maiba = TenantUser.objects.get(data_source_user__username="maiba") + resp = api_client.get(reverse("open_v3.tenant_user.leaders.list", kwargs={"id": maiba.id})) + assert resp.status_code == status.HTTP_200_OK + assert {t["bk_username"] for t in resp.data} == {wangwu.id, lisi.id} + assert {t["display_name"] for t in resp.data} == {"王五", "李四"} + + def test_with_no_leader(self, api_client): + zhangsan = TenantUser.objects.get(data_source_user__username="zhangsan") + resp = api_client.get(reverse("open_v3.tenant_user.leaders.list", kwargs={"id": zhangsan.id})) + assert resp.status_code == status.HTTP_200_OK + assert len(resp.data) == 0 + + def test_with_invalid_user(self, api_client): + resp = api_client.get(reverse("open_v3.tenant_user.leaders.list", kwargs={"id": "a1e5b2f6c3g7d4h8"})) + assert resp.status_code == status.HTTP_404_NOT_FOUND