Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added query the list of sub-departments based on department ID #2037

Merged
merged 17 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/bk-user/bkuser/apis/open_v3/serializers/department.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,17 @@ class TenantDepartmentRetrieveOutputSLZ(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 TenantDepartmentChildrenListInputSLZ(serializers.Serializer):
level = serializers.IntegerField(help_text="递归子部门的相对 Level 层级", required=False, default=1)

def validate_level(self, level: int) -> int:
if level < 1:
raise serializers.ValidationError("level 必须大于等于 1")
return level
rolin999 marked this conversation as resolved.
Show resolved Hide resolved


class TenantDepartmentChildrenListOutputSLZ(serializers.Serializer):
id = serializers.IntegerField(help_text="部门 ID")
name = serializers.CharField(help_text="部门名称")
rolin999 marked this conversation as resolved.
Show resolved Hide resolved
5 changes: 5 additions & 0 deletions src/bk-user/bkuser/apis/open_v3/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@
name="open_v3.tenant_department.retrieve",
),
path("users/", views.TenantUserListApi.as_view(), name="open_v3.tenant_user.list"),
path(
"departments/<int:id>/childrens/",
views.TenantDepartmentChildrenListApi.as_view(),
rolin999 marked this conversation as resolved.
Show resolved Hide resolved
name="open_v3.tenant_department.children.list",
),
]
),
),
Expand Down
3 changes: 2 additions & 1 deletion src/bk-user/bkuser/apis/open_v3/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
#
# We undertake not to change the open source license (MIT license) applicable
# to the current version of the project delivered to anyone in the future.
from .department import TenantDepartmentRetrieveApi
from .department import TenantDepartmentChildrenListApi, TenantDepartmentRetrieveApi
from .tenant import TenantListApi
from .user import (
TenantUserDepartmentListApi,
Expand All @@ -32,4 +32,5 @@
"TenantUserLeaderListApi",
"TenantUserListApi",
"TenantDepartmentRetrieveApi",
"TenantDepartmentChildrenListApi",
]
68 changes: 68 additions & 0 deletions src/bk-user/bkuser/apis/open_v3/views/department.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,22 @@
#
# We undertake not to change the open source license (MIT license) applicable
# to the current version of the project delivered to anyone in the future.
from typing import Any, Dict, List

from drf_yasg.utils import swagger_auto_schema
from mptt.querysets import TreeQuerySet
from rest_framework import generics, status
from rest_framework.generics import get_object_or_404
from rest_framework.response import Response

from bkuser.apis.open_v3.mixins import OpenApiCommonMixin
from bkuser.apis.open_v3.serializers.department import (
TenantDepartmentChildrenListInputSLZ,
TenantDepartmentChildrenListOutputSLZ,
TenantDepartmentRetrieveInputSLZ,
TenantDepartmentRetrieveOutputSLZ,
)
from bkuser.apps.data_source.models import DataSourceDepartment, DataSourceDepartmentRelation
from bkuser.apps.tenant.models import TenantDepartment
from bkuser.biz.organization import DataSourceDepartmentHandler

Expand Down Expand Up @@ -66,3 +72,65 @@ def get(self, request, *args, **kwargs):
info["ancestors"] = [{"id": d.id, "name": d.data_source_department.name} for d in tenant_depts]

return Response(TenantDepartmentRetrieveOutputSLZ(info).data)


class TenantDepartmentChildrenListApi(OpenApiCommonMixin, generics.ListAPIView):
"""
获取部门下的子部门列表信息(支持递归)
"""

serializer_class = TenantDepartmentChildrenListOutputSLZ
rolin999 marked this conversation as resolved.
Show resolved Hide resolved

@swagger_auto_schema(
tags=["open_v3.department"],
operation_id="list_department_children",
operation_description="查询部门下的子部门列表",
query_serializer=TenantDepartmentChildrenListInputSLZ(),
responses={status.HTTP_200_OK: TenantDepartmentChildrenListOutputSLZ(many=True)},
)
def get(self, request, *args, **kwargs):
slz = TenantDepartmentChildrenListInputSLZ(data=self.request.query_params)
slz.is_valid(raise_exception=True)
data = slz.validated_data

tenant_department = get_object_or_404(
TenantDepartment.objects.filter(tenant_id=self.tenant_id), id=kwargs["id"]
)

relation = DataSourceDepartmentRelation.objects.filter(
department_id=tenant_department.data_source_department_id
).first()
rolin999 marked this conversation as resolved.
Show resolved Hide resolved

# 计算绝对层级 Level
absolute_level = relation.level + data["level"]
# 按层级 Level 递归查询该部门的子部门
child_ids = relation.get_descendants().filter(level=absolute_level).values_list("department_id", flat=True)
rolin999 marked this conversation as resolved.
Show resolved Hide resolved

# 获取子部门列表信息
dept_infos = self.list_dept_infos(child_ids)
rolin999 marked this conversation as resolved.
Show resolved Hide resolved

return self.get_paginated_response(
TenantDepartmentChildrenListOutputSLZ(self.paginate_queryset(dept_infos), many=True).data
)

def list_dept_infos(self, child_ids: TreeQuerySet[int]) -> List[Dict[str, Any]]:
rolin999 marked this conversation as resolved.
Show resolved Hide resolved
"""
获取子部门列表信息
"""

# 预加载部门对应的租户部门
dept_id_map = dict(
TenantDepartment.objects.filter(
data_source_department_id__in=child_ids, tenant_id=self.tenant_id
).values_list("data_source_department_id", "id")
)

# 预加载部门对应的名称
dept_name_map = dict(DataSourceDepartment.objects.filter(id__in=child_ids).values_list("id", "name"))

# 组装数据
return [
{"id": dept_id_map[dept_id], "name": dept_name_map[dept_id]}
for dept_id in child_ids
if dept_id in dept_id_map
]
rolin999 marked this conversation as resolved.
Show resolved Hide resolved
72 changes: 72 additions & 0 deletions src/bk-user/support-files/apidocs/en/list_department_children.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
### Description

(Pagination) Query the sub-department list based on the department ID, support recursive query by level

### Parameters

| Name | Type | Required | Location | Description |
|---------------|------|----------|-------------|-------------------------------------------------------------------------------------------------------------|
| page | int | No | query param | Page number, default is 1 |
| page_size | int | No | query param | The number of pages per page, default is 10 |
| department_id | int | Yes | path | Unique identifier of the department |
| level | int | No | query param | The relative level of the recursive sub-department. The default is 1, which means the direct sub-department |

### Request Example

```
// URL Path & Query Parameters
/api/v3/open/tenant/departments/2/childrens/?level=2&page=1&page_size=5
```

### Response Example for Status Code 200

```json5
{
"data": {
"count": 2,
"results": [
{
"id": 6,
"name": "小组AAA",
},
{
"id": 7,
"name": "小组ABA",
}
],
}
}
```

### Response Parameters Description

| Name | Type | Description |
|------|--------|-------------------------------------|
| id | int | Unique identifier of the department |
| name | string | The name of the department |

For example, if the sub-departments of Department A are Center AA and Center AB, the sub-department of Center AA is
Group AAA, and the sub-department of Center AB is Group ABA, then the sub-department of Department A with a relative
level of level 2 is: Group AAA -> Group ABA

# Response Example for Non-200 Status Code

```json5
// status_code = 404
{
"error": {
"code": "NOT_FOUND",
"message": "Object not found"
}
}
```

```json5
// status_code = 400
{
"error": {
"code": "INVALID_ARGUMENT",
"message": "Parameter validation failed: level: level must be greater than or equal to 1"
}
}
```
71 changes: 71 additions & 0 deletions src/bk-user/support-files/apidocs/zh/list_department_children.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
### 描述

(分页)根据部门 ID 查询子部门列表,支持按照层级 Level 递归查询

### 输入参数

| 参数名称 | 参数类型 | 必选 | 参数位置 | 描述 |
|---------------|------|----|-------------|--------------------------------|
| page | int | 否 | query param | 页码,从 1 开始 |
| page_size | int | 否 | query param | 每页数量,默认为 10 |
| department_id | int | 是 | path | 部门唯一标识 |
| level | int | 否 | query_param | 递归子部门的相对 Level 层级,默认为 1,即直接子部门 |

### 请求示例

```
// URL Path & Query 参数
/api/v3/open/tenant/departments/2/childrens/?level=2&page=1&page_size=5
```

### 状态码 200 的响应示例

```json5
{
"data": {
"count": 2,
"results": [
{
"id": 6,
"name": "小组AAA",
},
{
"id": 7,
"name": "小组ABA",
}
],
}
}
```

### 响应参数说明

| 参数名称 | 参数类型 | 描述 |
|------|--------|--------|
| id | int | 部门唯一标识 |
| name | string | 部门名称 |

例如:部门A的子部门为中心AA、中心AB,中心AA的子部门为小组AAA,中心AB的子部门为小组ABA,则部门A的相对层级 level 为 2
的子部门为:小组AAA -> 小组ABA

### 状态码非 200 的响应示例

```json5
// status_code = 404
{
"error": {
"code": "NOT_FOUND",
"message": "对象未找到"
}
}
```

```json5
// status_code = 400
{
"error": {
"code": "INVALID_ARGUMENT",
"message": "参数校验不通过: level: level 必须大于等于 1"
}
}
```
25 changes: 25 additions & 0 deletions src/bk-user/support-files/resources.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,28 @@ paths:
appVerifiedRequired: true
resourcePermissionRequired: true
descriptionEn: (Pagination) Query user's list

/api/v3/open/tenant/departments/{department_id}/childrens/:
get:
operationId: list_department_children
description: 根据部门 ID 查询子部门列表信息
tags: []
responses:
default:
description: ''
x-bk-apigateway-resource:
isPublic: true
allowApplyPermission: false
matchSubpath: false
backend:
name: default
method: get
path: /api/v3/open/tenant/departments/{department_id}/childrens/
matchSubpath: false
timeout: 0
pluginConfigs: []
authConfig:
userVerifiedRequired: false
appVerifiedRequired: true
resourcePermissionRequired: true
descriptionEn: Query the list of sub-departments information based on department ID
68 changes: 68 additions & 0 deletions src/bk-user/tests/apis/open_v3/test_department.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,71 @@ def test_with_ancestors(self, api_client):
def test_with_not_found(self, api_client):
resp = api_client.get(reverse("open_v3.tenant_department.retrieve", kwargs={"id": 9999}))
assert resp.status_code == status.HTTP_404_NOT_FOUND


@pytest.mark.usefixtures("_init_tenant_users_depts")
class TestTenantDepartmentChildrenListApi:
def test_with_not_level(self, api_client):
company = TenantDepartment.objects.get(data_source_department__name="公司")
dept_a = TenantDepartment.objects.get(data_source_department__name="部门A")
dept_b = TenantDepartment.objects.get(data_source_department__name="部门B")
resp = api_client.get(reverse("open_v3.tenant_department.children.list", kwargs={"id": company.id}))

assert resp.status_code == status.HTTP_200_OK
assert resp.data["count"] == 2
assert [t["id"] for t in resp.data["results"]] == [dept_a.id, dept_b.id]
assert [t["name"] for t in resp.data["results"]] == ["部门A", "部门B"]

def test_with_level(self, api_client):
dept_a = TenantDepartment.objects.get(data_source_department__name="部门A")
group_aaa = TenantDepartment.objects.get(data_source_department__name="小组AAA")
group_aba = TenantDepartment.objects.get(data_source_department__name="小组ABA")
resp = api_client.get(
reverse("open_v3.tenant_department.children.list", kwargs={"id": dept_a.id}), data={"level": 2}
)

assert resp.status_code == status.HTTP_200_OK
assert resp.data["count"] == 2
assert [t["id"] for t in resp.data["results"]] == [group_aaa.id, group_aba.id]
assert [t["name"] for t in resp.data["results"]] == ["小组AAA", "小组ABA"]

def test_with_pagination(self, api_client):
company = TenantDepartment.objects.get(data_source_department__name="公司")
resp = api_client.get(
reverse("open_v3.tenant_department.children.list", kwargs={"id": company.id}),
data={"level": 2, "page": 1, "page_size": 2},
)

assert resp.status_code == status.HTTP_200_OK
assert resp.data["count"] == 3
assert len(resp.data["results"]) == 2

def test_with_not_children(self, api_client):
group_aaa = TenantDepartment.objects.get(data_source_department__name="小组AAA")
resp = api_client.get(reverse("open_v3.tenant_department.children.list", kwargs={"id": group_aaa.id}))

assert resp.status_code == status.HTTP_200_OK
assert resp.data["count"] == 0
assert len(resp.data["results"]) == 0

def test_with_not_level_children(self, api_client):
company = TenantDepartment.objects.get(data_source_department__name="公司")
resp = api_client.get(
reverse("open_v3.tenant_department.children.list", kwargs={"id": company.id}), data={"level": 4}
)

assert resp.status_code == status.HTTP_200_OK
assert resp.data["count"] == 0
assert len(resp.data["results"]) == 0

def test_with_invalid_level(self, api_client):
company = TenantDepartment.objects.get(data_source_department__name="公司")
resp = api_client.get(
reverse("open_v3.tenant_department.children.list", kwargs={"id": company.id}), data={"level": -1}
)

assert resp.status_code == status.HTTP_400_BAD_REQUEST

def test_with_department_not_found(self, api_client):
resp = api_client.get(reverse("open_v3.tenant_department.children.list", kwargs={"id": 9999}))
assert resp.status_code == status.HTTP_404_NOT_FOUND
Loading