Repository: TencentBlueKing/bk-user

Issue:
feat: add UserBuiltinField & TenantUserCustomField model



Discussion:

Resolving patch:
diff --git a/src/bk-user/bkuser/apps/tenant/constants.py b/src/bk-user/bkuser/apps/tenant/constants.py
--- a/src/bk-user/bkuser/apps/tenant/constants.py
+++ b/src/bk-user/bkuser/apps/tenant/constants.py
@@ -9,7 +9,7 @@
 specific language governing permissions and limitations under the License.
 """
 import pytz
-from blue_krill.data_types.enum import FeatureFlag, FeatureFlagField
+from blue_krill.data_types.enum import EnumField, FeatureFlag, FeatureFlagField, StructuredEnum
 from django.utils.translation import gettext_lazy as _
 
 TIME_ZONE_CHOICES = [(i, i) for i in list(pytz.common_timezones)]
@@ -19,3 +19,12 @@ class TenantFeatureFlag(FeatureFlag):  # type: ignore
     """租户特性标志"""
 
     USER_NUMBER_VISIBLE = FeatureFlagField(label=_("人员数量是否可见"), default=True)
+
+
+class UserFieldDataType(str, StructuredEnum):
+    """租户用户自定义字段数据类型"""
+
+    STRING = EnumField("string", label=_("字符串"))
+    NUMBER = EnumField("number", label=_("数字"))
+    ENUM = EnumField("enum", label=_("枚举"))
+    MULTI_ENUM = EnumField("multi_enum", label=_("多选枚举"))
diff --git a/src/bk-user/bkuser/apps/tenant/migrations/0002_tenantusercustomfield_userbuiltinfield.py b/src/bk-user/bkuser/apps/tenant/migrations/0002_tenantusercustomfield_userbuiltinfield.py
new file mode 100644
--- /dev/null
+++ b/src/bk-user/bkuser/apps/tenant/migrations/0002_tenantusercustomfield_userbuiltinfield.py
@@ -0,0 +1,61 @@
+# -*- 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.
+"""
+# Generated by Django 3.2.20 on 2023-09-12 11:19
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('tenant', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='UserBuiltinField',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('created_at', models.DateTimeField(auto_now_add=True)),
+                ('updated_at', models.DateTimeField(auto_now=True)),
+                ('name', models.CharField(max_length=128, unique=True, verbose_name='字段名称')),
+                ('display_name', models.CharField(max_length=128, verbose_name='展示用名称')),
+                ('data_type', models.CharField(choices=[('string', '字符串'), ('number', '数字'), ('datetime', '日期时间'), ('enum', '枚举'), ('multi_enum', '多选枚举')], max_length=32, verbose_name='数据类型')),
+                ('required', models.BooleanField(verbose_name='是否必填')),
+                ('unique', models.BooleanField(verbose_name='是否唯一')),
+                ('default', models.JSONField(default='', verbose_name='默认值')),
+                ('options', models.JSONField(default={}, verbose_name='配置项')),
+            ],
+            options={
+                'abstract': False,
+            },
+        ),
+        migrations.CreateModel(
+            name='TenantUserCustomField',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('created_at', models.DateTimeField(auto_now_add=True)),
+                ('updated_at', models.DateTimeField(auto_now=True)),
+                ('name', models.CharField(max_length=128, verbose_name='字段名称')),
+                ('display_name', models.CharField(max_length=128, verbose_name='展示用名称')),
+                ('data_type', models.CharField(choices=[('string', '字符串'), ('number', '数字'), ('datetime', '日期时间'), ('enum', '枚举'), ('multi_enum', '多选枚举')], max_length=32, verbose_name='数据类型')),
+                ('required', models.BooleanField(verbose_name='是否必填')),
+                ('order', models.IntegerField(default=0, verbose_name='展示顺序')),
+                ('default', models.JSONField(default='', verbose_name='默认值')),
+                ('options', models.JSONField(default={}, verbose_name='配置项')),
+                ('tenant', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.CASCADE, to='tenant.tenant')),
+            ],
+            options={
+                'unique_together': {('tenant', 'name'), ('tenant', 'display_name')},
+            },
+        ),
+    ]
diff --git a/src/bk-user/bkuser/apps/tenant/models.py b/src/bk-user/bkuser/apps/tenant/models.py
--- a/src/bk-user/bkuser/apps/tenant/models.py
+++ b/src/bk-user/bkuser/apps/tenant/models.py
@@ -12,7 +12,7 @@
 from django.db import models
 
 from bkuser.apps.data_source.models import DataSource, DataSourceDepartment, DataSourceUser
-from bkuser.apps.tenant.constants import TenantFeatureFlag
+from bkuser.apps.tenant.constants import TenantFeatureFlag, UserFieldDataType
 from bkuser.common.constants import PERMANENT_TIME, BkLanguageEnum
 from bkuser.common.models import TimestampedModel
 
@@ -108,26 +108,35 @@ class Meta:
         ]
 
 
-# TODO: 是否直接定义 TenantCommonConfig 表，DynamicFieldInfo是一个JSON字段
-# class DynamicFieldInfo(TimestampedModel):
-#     """动态的用户字段元信息"""
-#
-#     tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, db_index=True)
-#
-#     id = models.CharField("字段唯一标识", max_length=64)
-#     name = models.CharField("字段名称", max_length=64)
-#     # TODO: 需要枚举支持的数据类型
-#     data_type = models.CharField("数据类型", max_length=32)
-#     require = models.BooleanField("是否必填", default=False)
-#     unique = models.BooleanField("是否唯一", default=False)
-#     editable = models.BooleanField("是否可b", default=False)
-#     # TODO：不同类型，可能有额外配置，比如枚举有key和value选项，是否配置为json_schema格式，便于校验呢？？？
-#
-#     class Meta:
-#         unique_together = [
-#             ("tenant", "id"),
-#             ("tenant", "name"),
-#         ]
+class UserBuiltinField(TimestampedModel):
+    """用户内置字段"""
+
+    name = models.CharField("字段名称", unique=True, max_length=128)
+    display_name = models.CharField("展示用名称", max_length=128)
+    data_type = models.CharField("数据类型", choices=UserFieldDataType.get_choices(), max_length=32)
+    required = models.BooleanField("是否必填")
+    unique = models.BooleanField("是否唯一")
+    default = models.JSONField("默认值", default="")
+    options = models.JSONField("配置项", default={})
+
+
+class TenantUserCustomField(TimestampedModel):
+    """租户用户自定义字段"""
+
+    tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, db_constraint=False)
+    name = models.CharField("字段名称", max_length=128)
+    display_name = models.CharField("展示用名称", max_length=128)
+    data_type = models.CharField("数据类型", choices=UserFieldDataType.get_choices(), max_length=32)
+    required = models.BooleanField("是否必填")
+    order = models.IntegerField("展示顺序", default=0)
+    default = models.JSONField("默认值", default="")
+    options = models.JSONField("配置项", default={})
+
+    class Meta:
+        unique_together = [
+            ("tenant", "name"),
+            ("tenant", "display_name"),
+        ]
 
 
 # # TODO: 是否直接定义 TenantCommonConfig 表，AccountValidityPeriod是一个JSON字段？


Test patch:
diff --git a/src/bk-user/tests/apis/web/data_source/test_data_source.py b/src/bk-user/tests/apis/web/data_source/test_data_source.py
--- a/src/bk-user/tests/apis/web/data_source/test_data_source.py
+++ b/src/bk-user/tests/apis/web/data_source/test_data_source.py
@@ -76,12 +76,14 @@ def local_ds_plugin_config() -> Dict[str, Any]:
                         "scene": NotificationScene.USER_INITIALIZE,
                         "sender": "蓝鲸智云",
                         "content": "您的账户已经成功创建，请尽快修改密码",
+                        "content_html": "<p>您的账户已经成功创建，请尽快修改密码</p>",
                     },
                     {
                         "method": NotificationMethod.SMS,
                         "scene": NotificationScene.RESET_PASSWORD,
                         "sender": "蓝鲸智云",
                         "content": "点击以下链接以重置代码",
+                        "content_html": "<p>点击以下链接以重置代码</p>",
                     },
                 ],
             },
@@ -112,12 +114,14 @@ def local_ds_plugin_config() -> Dict[str, Any]:
                         "scene": NotificationScene.PASSWORD_EXPIRING,
                         "sender": "蓝鲸智云",
                         "content": "您的密码即将到期！",
+                        "content_html": "<p>您的密码即将到期！</p>",
                     },
                     {
                         "method": NotificationMethod.SMS,
                         "scene": NotificationScene.PASSWORD_EXPIRED,
                         "sender": "蓝鲸智云",
                         "content": "您的密码已到期！",
+                        "content_html": "<p>您的密码已到期！</p>",
                     },
                 ],
             },