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(web/personal_center): 个人中心展示 #1245

Merged
merged 30 commits into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
314b7f8
feat(web/person_center): 个人中心展示
neronkl Sep 18, 2023
e2357ef
refactor: code refactor
neronkl Sep 18, 2023
0df2f03
refactor: code refactor
neronkl Sep 18, 2023
24ff4f7
refactor: code refactor
neronkl Sep 19, 2023
7505a5b
fix: 修复自然人关系查询异常
neronkl Sep 19, 2023
2fb5d78
fix: 边界限制
neronkl Sep 20, 2023
c995aa1
feature: 支持租户手机号/邮箱编辑
neronkl Sep 20, 2023
cfac771
refactor: cr调整
neronkl Sep 21, 2023
01f2e35
fix: 修复通过继承手机号,仍需对入参进行校验
neronkl Sep 21, 2023
a890922
refactor: cr调整
neronkl Sep 21, 2023
f87ed30
test: add testing case
neronkl Sep 22, 2023
418831b
refactor: cr调整
neronkl Sep 25, 2023
d13b2f3
refactor: cr调整
neronkl Sep 26, 2023
d7c49cf
fix: code conflict work out
neronkl Sep 26, 2023
539a4f2
refactor: cr调整
neronkl Sep 26, 2023
aa82490
fix: 取值错误
neronkl Oct 8, 2023
8d81dd7
refactor: 单测cr调整
neronkl Oct 9, 2023
62b129c
fix: 解决code冲突
neronkl Oct 9, 2023
10acc80
refactor: cr调整
neronkl Oct 9, 2023
ddba1c5
refactor: cr调整
neronkl Oct 9, 2023
d1d7846
refactor: cr调整
neronkl Oct 9, 2023
0948f51
refactor: cr调整
neronkl Oct 9, 2023
d3a2634
rollback conftest.py
narasux Oct 10, 2023
3a025e9
init default local data source in create_tenant
narasux Oct 10, 2023
08f22a0
init DataSourceUser & TenantUser when create_user.py
narasux Oct 10, 2023
a9557aa
Update tenant.py
narasux Oct 10, 2023
afe622d
Update auth.py
narasux Oct 10, 2023
2f8c1b4
refactor: cr调整
neronkl Oct 10, 2023
6c0c69f
refactor: cr调整
neronkl Oct 10, 2023
d2549d5
refactor: cr调整
neronkl Oct 10, 2023
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
3 changes: 3 additions & 0 deletions src/bk-user/bkuser/apis/web/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ class CurrentUserTenantMixin:
request: Request

def get_current_tenant_id(self) -> str:
"""
获取当前登录用户所属租户的ID
"""
tenant_id = self.request.user.get_property("tenant_id")
if not tenant_id:
raise error_codes.GET_CURRENT_TENANT_FAILED
Expand Down
5 changes: 4 additions & 1 deletion src/bk-user/bkuser/apis/web/organization/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,12 @@ class TenantUserInfoOutputSLZ(serializers.Serializer):
phone_country_code = serializers.CharField(
help_text="手机号国际区号", required=False, default=settings.DEFAULT_PHONE_COUNTRY_CODE
)
account_expired_at = serializers.DateTimeField(help_text="账号过期时间")
account_expired_at = serializers.SerializerMethodField(help_text="账号过期时间")
departments = serializers.SerializerMethodField(help_text="用户所属部门")

def get_account_expired_at(self, instance: TenantUser) -> str:
return instance.account_expired_at_display


class TenantUserListOutputSLZ(TenantUserInfoOutputSLZ):
@swagger_serializer_method(serializer_or_field=TenantUserDepartmentOutputSLZ(many=True))
Expand Down
10 changes: 10 additions & 0 deletions src/bk-user/bkuser/apis/web/person_center/__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.
"""
135 changes: 135 additions & 0 deletions src/bk-user/bkuser/apis/web/person_center/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# -*- 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 django.utils.translation import gettext_lazy as _
from drf_yasg.utils import swagger_serializer_method
from rest_framework import serializers

from bkuser.apis.web.organization.serializers import TenantUserDepartmentOutputSLZ, TenantUserLeaderOutputSLZ
from bkuser.apps.tenant.models import TenantUser
from bkuser.biz.tenant import TenantUserHandler
from bkuser.common.error_codes import error_codes
from bkuser.common.validators import validate_phone_with_country_code


class TenantBaseInfoOutputSLZ(serializers.Serializer):
id = serializers.CharField(help_text="租户ID")
name = serializers.CharField(help_text="租户名称")


class TenantUserBaseInfoOutputSLZ(serializers.Serializer):
id = serializers.CharField(help_text="租户用户ID")
username = serializers.CharField(help_text="用户名")
full_name = serializers.CharField(help_text="全名")
nannan00 marked this conversation as resolved.
Show resolved Hide resolved
tenant = TenantBaseInfoOutputSLZ(help_text="租户")


class NaturalUserWithTenantUserListOutputSLZ(serializers.Serializer):
id = serializers.CharField(help_text="自然人ID")
full_name = serializers.CharField(help_text="自然人全名")
tenant_users = serializers.ListField(help_text="自然人关联的租户账号列表", child=TenantUserBaseInfoOutputSLZ())


class TenantUserRetrieveOutputSLZ(serializers.Serializer):
id = serializers.CharField(help_text="租户用户ID")
username = serializers.CharField(help_text="用户名", required=False)
full_name = serializers.CharField(help_text="用户全名", required=False)
logo = serializers.CharField(help_text="头像", required=False)

# 邮箱信息
is_inherited_email = serializers.BooleanField(help_text="是否继承数据源邮箱")
email = serializers.EmailField(help_text="用户邮箱", required=False)
custom_email = serializers.EmailField(help_text="自定义用户邮箱")

# 手机号信息
is_inherited_phone = serializers.BooleanField(help_text="是否继承数据源手机号")
phone = serializers.CharField(help_text="用户手机号", required=False)
phone_country_code = serializers.CharField(
help_text="手机号国际区号", required=False, default=settings.DEFAULT_PHONE_COUNTRY_CODE
)
custom_phone = serializers.CharField(help_text="自定义用户手机号")
custom_phone_country_code = serializers.CharField(help_text="自定义用户手机国际区号")

account_expired_at = serializers.SerializerMethodField(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]:
tenant_user_departments = TenantUserHandler.get_tenant_user_departments_map_by_id([instance.id])
departments = tenant_user_departments.get(instance.id) or []
return [{"id": i.id, "name": i.name} for i in departments]

@swagger_serializer_method(serializer_or_field=TenantUserLeaderOutputSLZ(many=True))
def get_leaders(self, instance: TenantUser) -> List[Dict]:
leaders = TenantUserHandler.get_tenant_user_leaders_map_by_id([instance.id]).get(instance.id) or []
return [
{
"id": i.id,
"username": i.username,
"full_name": i.full_name,
}
for i in leaders
]

def get_account_expired_at(self, instance: TenantUser) -> str:
return instance.account_expired_at_display

def to_representation(self, instance: TenantUser) -> Dict:
data = super().to_representation(instance)
user = instance.data_source_user
if user is not None:
data.update(
{
"full_name": user.full_name,
"username": user.username,
"email": user.email,
"phone": user.phone,
"phone_country_code": user.phone_country_code,
"logo": user.logo or settings.DEFAULT_DATA_SOURCE_USER_LOGO,
}
)
return data


class TenantUserPhoneUpdateInputSLZ(serializers.Serializer):
is_inherited_phone = serializers.BooleanField(required=True, help_text="是否继承数据源手机号")
custom_phone = serializers.CharField(required=False, allow_blank=True, help_text="自定义用户手机号")
custom_phone_country_code = serializers.CharField(
required=False, help_text="自定义用户手机国际区号", default=settings.DEFAULT_PHONE_COUNTRY_CODE
)
nannan00 marked this conversation as resolved.
Show resolved Hide resolved

def validate(self, attrs):
# custom_phone_country_code 具有默认值
# 不通过继承,则需校验手机号,custom_phone 必须存在
if not attrs["is_inherited_phone"]:
if not attrs.get("custom_phone"):
raise error_codes.VALIDATION_ERROR.f(_("自定义手机号码为必填项"))

validate_phone_with_country_code(
phone=attrs["custom_phone"], country_code=attrs["custom_phone_country_code"]
)
nannan00 marked this conversation as resolved.
Show resolved Hide resolved

return attrs


class TenantUserEmailUpdateInputSLZ(serializers.Serializer):
is_inherited_email = serializers.BooleanField(required=True, help_text="是否继承数据源邮箱")
custom_email = serializers.EmailField(required=False, help_text="自定义用户邮箱", allow_blank=True)

def validate(self, attrs):
# 不通过继承,custom_email 必须存在
if not attrs["is_inherited_email"] and not attrs.get("custom_email"):
raise error_codes.VALIDATION_ERROR.f(_("自定义邮箱为必填项"))

return attrs
34 changes: 34 additions & 0 deletions src/bk-user/bkuser/apis/web/person_center/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# -*- 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(
"current-natural-user/",
views.NaturalUserTenantUserListApi.as_view(),
name="person_center.current_natural_user",
),
# 租户用户详情
path("tenant-users/<str:id>/", views.TenantUserRetrieveApi.as_view(), name="person_center.tenant_users.retrieve"),
path(
"tenant-users/<str:id>/phone/",
views.TenantUserPhoneUpdateApi.as_view(),
name="person_center.tenant_users.phone.patch",
nannan00 marked this conversation as resolved.
Show resolved Hide resolved
),
path(
"tenant-users/<str:id>/email/",
views.TenantUserEmailUpdateApi.as_view(),
name="person_center.tenant_users.email.patch",
),
]
151 changes: 151 additions & 0 deletions src/bk-user/bkuser/apis/web/person_center/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# -*- 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 drf_yasg.utils import swagger_auto_schema
from rest_framework import generics, status
from rest_framework.response import Response

from bkuser.apis.web.person_center.serializers import (
NaturalUserWithTenantUserListOutputSLZ,
TenantUserEmailUpdateInputSLZ,
TenantUserPhoneUpdateInputSLZ,
TenantUserRetrieveOutputSLZ,
)
from bkuser.apps.tenant.models import TenantUser
from bkuser.biz.natural_user import NaturalUserWithTenantUsers, NatureUserHandler, TenantBaseInfo, TenantUserBaseInfo
from bkuser.biz.tenant import TenantUserEmailInfo, TenantUserHandler, TenantUserPhoneInfo
from bkuser.common.error_codes import error_codes
from bkuser.common.views import ExcludePutAPIViewMixin


class NaturalUserTenantUserListApi(generics.ListAPIView):
pagination_class = None

@swagger_auto_schema(
tags=["person_center"],
operation_description="个人中心-关联账户列表",
responses={status.HTTP_200_OK: NaturalUserWithTenantUserListOutputSLZ()},
)
def get(self, request, *args, **kwargs):
current_tenant_user_id = request.user.username

# 获取当前登录的租户用户的自然人:两种情况绑定、未绑定,在函数中做处理
nature_user = NatureUserHandler.get_nature_user_by_tenant_user_id(current_tenant_user_id)

# 将当前登录置顶
# 通过比对租户用户id, 当等于当前登录用户的租户id,将其排序到查询集的顶部, 否则排序到查询集的底部
tenant_users = TenantUser.objects.select_related("data_source_user").filter(
data_source_user_id__in=nature_user.data_source_user_ids
)

# FIXME:这里后续数据量可能会很大,需要更优的处理方式 / 或者接口增加一个返回字段is_login 做当前登录用户的标识
sorted_tenant_users = sorted(tenant_users, key=lambda t: t.id != current_tenant_user_id)
nannan00 marked this conversation as resolved.
Show resolved Hide resolved

# 响应数据组装
nature_user_with_tenant_users = NaturalUserWithTenantUsers(
id=nature_user.id,
full_name=nature_user.full_name,
tenant_users=[
TenantUserBaseInfo(
id=user.id,
username=user.data_source_user.username,
full_name=user.data_source_user.full_name,
tenant=TenantBaseInfo(id=user.tenant_id, name=user.tenant.name),
)
for user in sorted_tenant_users
],
)
nannan00 marked this conversation as resolved.
Show resolved Hide resolved

return Response(NaturalUserWithTenantUserListOutputSLZ(nature_user_with_tenant_users).data)


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

@swagger_auto_schema(
tags=["person_center"],
operation_description="个人中心-关联账户详情",
responses={status.HTTP_200_OK: TenantUserRetrieveOutputSLZ()},
)
def get(self, request, *args, **kwargs):
instance: TenantUser = self.get_object()

# 获取当前登录的租户用户的自然人
nature_user = NatureUserHandler.get_nature_user_by_tenant_user_id(request.user.username)

# 边界限制
# 该租户用户的数据源用户,不属于当前自然人
if instance.data_source_user_id not in nature_user.data_source_user_ids:
raise error_codes.NO_PERMISSION

return Response(TenantUserRetrieveOutputSLZ(instance).data)


class TenantUserPhoneUpdateApi(ExcludePutAPIViewMixin, generics.UpdateAPIView):
queryset = TenantUser.objects.all()
lookup_url_kwarg = "id"

@swagger_auto_schema(
tags=["person_center"],
operation_description="租户用户更新手机号",
request_body=TenantUserPhoneUpdateInputSLZ,
responses={status.HTTP_200_OK: ""},
)
def patch(self, request, *args, **kwargs):
instance: TenantUser = self.get_object()

# 获取当前登录的租户用户的自然人
nature_user = NatureUserHandler.get_nature_user_by_tenant_user_id(request.user.username)

# 边界限制
# 该租户用户的数据源用户,不属于当前自然人
if instance.data_source_user_id not in nature_user.data_source_user_ids:
raise error_codes.NO_PERMISSION

input_slz = TenantUserPhoneUpdateInputSLZ(data=request.data)
input_slz.is_valid(raise_exception=True)
nannan00 marked this conversation as resolved.
Show resolved Hide resolved

phone_info = TenantUserPhoneInfo(**input_slz.validated_data)
nannan00 marked this conversation as resolved.
Show resolved Hide resolved
TenantUserHandler.update_tenant_user_phone(instance, phone_info)

return Response()


class TenantUserEmailUpdateApi(ExcludePutAPIViewMixin, generics.UpdateAPIView):
queryset = TenantUser.objects.all()
lookup_url_kwarg = "id"

@swagger_auto_schema(
tags=["person_center"],
operation_description="租户用户更新手机号",
request_body=TenantUserEmailUpdateInputSLZ,
responses={status.HTTP_200_OK: ""},
)
def patch(self, request, *args, **kwargs):
instance: TenantUser = self.get_object()

# 获取当前登录的租户用户的自然人
nature_user = NatureUserHandler.get_nature_user_by_tenant_user_id(request.user.username)

# 边界限制
# 该租户用户的数据源用户,不属于当前自然人下的
if instance.data_source_user_id not in nature_user.data_source_user_ids:
raise error_codes.NO_PERMISSION

input_slz = TenantUserEmailUpdateInputSLZ(data=request.data)
input_slz.is_valid(raise_exception=True)

email_info = TenantUserEmailInfo(**input_slz.validated_data)
TenantUserHandler.update_tenant_user_email(instance, email_info)

return Response()
2 changes: 2 additions & 0 deletions src/bk-user/bkuser/apis/web/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,7 @@
path("tenant-organization/", include("bkuser.apis.web.organization.urls")),
path("data-sources/", include("bkuser.apis.web.data_source.urls")),
path("data-sources/", include("bkuser.apis.web.data_source_organization.urls")),
# 个人中心
path("personal-center/", include("bkuser.apis.web.person_center.urls")),
path("tenant-setting/", include("bkuser.apis.web.tenant_setting.urls")),
]
6 changes: 6 additions & 0 deletions src/bk-user/bkuser/apps/tenant/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"""
from django.conf import settings
from django.db import models
from django.utils import timezone

from bkuser.apps.data_source.models import DataSource, DataSourceDepartment, DataSourceUser
from bkuser.apps.tenant.constants import TenantFeatureFlag, UserFieldDataType
Expand Down Expand Up @@ -78,6 +79,11 @@ class Meta:
("data_source_user", "tenant"),
]

@property
def account_expired_at_display(self) -> str:
local_time = timezone.localtime(self.account_expired_at)
return local_time.strftime("%Y-%m-%d %H:%M:%S")


class TenantDepartment(TimestampedModel):
"""
Expand Down
Loading
Loading