Skip to content

Commit 68155ab

Browse files
committed
feat: implement Tool model and related API for Tool management
1 parent 342ef6d commit 68155ab

21 files changed

+298
-0
lines changed

apps/common/constants/permission_constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ class PermissionConstants(Enum):
109109
RoleConstants.USER])
110110
USER_EDIT = Permission(group=Group.USER, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN])
111111
USER_DELETE = Permission(group=Group.USER, operate=Operate.DELETE, role_list=[RoleConstants.ADMIN])
112+
TOOL_CREATE = Permission(group=Group.USER, operate=Operate.CREATE, role_list=[RoleConstants.ADMIN,
113+
RoleConstants.USER])
112114

113115
def get_workspace_application_permission(self):
114116
return lambda r, kwargs: Permission(group=self.value.group, operate=self.value.operate,

apps/maxkb/settings/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
'drf_spectacular',
4040
'drf_spectacular_sidecar',
4141
'users.apps.UsersConfig',
42+
'tools.apps.ToolConfig',
4243
'common',
4344
'system_manage'
4445
]

apps/maxkb/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
SpectacularRedocView.authentication_classes = [AnonymousAuthentication]
3131
urlpatterns = [
3232
path("api/", include("users.urls")),
33+
path("api/", include("tools.urls"))
3334
]
3435
urlpatterns += [
3536
path('schema/', SpectacularAPIView.as_view(), name='schema'), # schema的配置文件的路由,下面两个ui也是根据这个配置文件来生成的

apps/tools/__init__.py

Whitespace-only changes.

apps/tools/admin.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from django.contrib import admin
2+
3+
# Register your models here.

apps/tools/api/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# coding=utf-8

apps/tools/api/tool.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# coding=utf-8
2+
3+
from common.mixins.api_mixin import APIMixin
4+
from common.result import ResultSerializer
5+
from tools.serializers.tool import ToolModelSerializer, ToolCreateRequest
6+
7+
8+
class ToolCreateResponse(ResultSerializer):
9+
def get_data(self):
10+
return ToolModelSerializer()
11+
12+
13+
class ToolCreateAPI(APIMixin):
14+
@staticmethod
15+
def get_request():
16+
return ToolCreateRequest
17+
18+
@staticmethod
19+
def get_response():
20+
return ToolCreateResponse

apps/tools/apps.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from django.apps import AppConfig
2+
3+
4+
class ToolConfig(AppConfig):
5+
default_auto_field = 'django.db.models.BigAutoField'
6+
name = 'tools'

apps/tools/migrations/0001_initial.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Generated by Django 5.2 on 2025-04-16 09:49
2+
3+
import django.contrib.postgres.fields
4+
import django.db.models.deletion
5+
import uuid_utils.compat
6+
from django.db import migrations, models
7+
8+
9+
class Migration(migrations.Migration):
10+
11+
initial = True
12+
13+
dependencies = [
14+
('users', '0001_initial'),
15+
]
16+
17+
operations = [
18+
migrations.CreateModel(
19+
name='Tool',
20+
fields=[
21+
('id', models.UUIDField(default=uuid_utils.compat.uuid7, editable=False, primary_key=True, serialize=False, verbose_name='主键id')),
22+
('name', models.CharField(max_length=64, verbose_name='函数名称')),
23+
('desc', models.CharField(max_length=128, verbose_name='描述')),
24+
('code', models.CharField(max_length=102400, verbose_name='python代码')),
25+
('input_field_list', django.contrib.postgres.fields.ArrayField(base_field=models.JSONField(default=dict, verbose_name='输入字段'), default=list, size=None, verbose_name='输入字段列表')),
26+
('init_field_list', models.JSONField(default=list, verbose_name='启动字段列表')),
27+
('icon', models.CharField(default='/ui/favicon.ico', max_length=256, verbose_name='函数库icon')),
28+
('is_active', models.BooleanField(default=True)),
29+
('permission_type', models.CharField(choices=[('SHARED', '共享'), ('PRIVATE', '私有')], default='PRIVATE', max_length=20, verbose_name='权限类型')),
30+
('tool_type', models.CharField(choices=[('INTERNAL', '内置'), ('PUBLIC', '公开')], default='PUBLIC', max_length=20, verbose_name='函数类型')),
31+
('template_id', models.UUIDField(default=None, null=True, verbose_name='模版id')),
32+
('module_id', models.UUIDField(default='root', null=True, verbose_name='模块id')),
33+
('init_params', models.CharField(max_length=102400, null=True, verbose_name='初始化参数')),
34+
('create_time', models.DateTimeField(auto_now_add=True, null=True, verbose_name='创建时间')),
35+
('update_time', models.DateTimeField(auto_now=True, null=True, verbose_name='修改时间')),
36+
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.user', verbose_name='用户id')),
37+
],
38+
options={
39+
'db_table': 'tool',
40+
},
41+
),
42+
]

apps/tools/migrations/__init__.py

Whitespace-only changes.

apps/tools/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .tool import *

apps/tools/models/tool.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import uuid_utils.compat as uuid
2+
from django.contrib.postgres.fields import ArrayField
3+
from django.db import models
4+
5+
from users.models import User
6+
7+
8+
class PermissionType(models.TextChoices):
9+
SHARED = "SHARED", '共享'
10+
PRIVATE = "PRIVATE", "私有"
11+
12+
13+
class ToolType(models.TextChoices):
14+
INTERNAL = "INTERNAL", '内置'
15+
PUBLIC = "PUBLIC", "公开"
16+
17+
18+
class Tool(models.Model):
19+
id = models.UUIDField(primary_key=True, max_length=128, default=uuid.uuid7, editable=False, verbose_name="主键id")
20+
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="用户id")
21+
name = models.CharField(max_length=64, verbose_name="函数名称")
22+
desc = models.CharField(max_length=128, verbose_name="描述")
23+
code = models.CharField(max_length=102400, verbose_name="python代码")
24+
input_field_list = ArrayField(verbose_name="输入字段列表",
25+
base_field=models.JSONField(verbose_name="输入字段", default=dict)
26+
, default=list)
27+
init_field_list = models.JSONField(verbose_name="启动字段列表", default=list)
28+
icon = models.CharField(max_length=256, verbose_name="函数库icon", default="/ui/favicon.ico")
29+
is_active = models.BooleanField(default=True)
30+
permission_type = models.CharField(max_length=20, verbose_name='权限类型', choices=PermissionType.choices,
31+
default=PermissionType.PRIVATE)
32+
tool_type = models.CharField(max_length=20, verbose_name='函数类型', choices=ToolType.choices,
33+
default=ToolType.PUBLIC)
34+
template_id = models.UUIDField(max_length=128, verbose_name="模版id", null=True, default=None)
35+
module_id = models.UUIDField(max_length=128, verbose_name="模块id", null=True, default='root')
36+
init_params = models.CharField(max_length=102400, verbose_name="初始化参数", null=True)
37+
create_time = models.DateTimeField(verbose_name="创建时间", auto_now_add=True, null=True)
38+
update_time = models.DateTimeField(verbose_name="修改时间", auto_now=True, null=True)
39+
40+
class Meta:
41+
db_table = "tool"

apps/tools/serializers/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# coding=utf-8

apps/tools/serializers/tool.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# -*- coding: utf-8 -*-
2+
import re
3+
4+
import uuid_utils.compat as uuid
5+
from django.core import validators
6+
from django.utils.translation import gettext_lazy as _
7+
from rest_framework import serializers
8+
9+
from tools.models import Tool
10+
from util.field_message import ErrMessage
11+
12+
13+
class ToolModelSerializer(serializers.ModelSerializer):
14+
class Meta:
15+
model = Tool
16+
fields = ['id', 'name', 'icon', 'desc', 'code', 'input_field_list', 'init_field_list', 'init_params',
17+
'permission_type', 'is_active', 'user_id', 'template_id',
18+
'create_time', 'update_time']
19+
20+
21+
class ToolInputField(serializers.Serializer):
22+
name = serializers.CharField(required=True, error_messages=ErrMessage.char(_('variable name')))
23+
is_required = serializers.BooleanField(required=True, error_messages=ErrMessage.boolean(_('required')))
24+
type = serializers.CharField(required=True, error_messages=ErrMessage.char(_('type')), validators=[
25+
validators.RegexValidator(regex=re.compile("^string|int|dict|array|float$"),
26+
message=_('fields only support string|int|dict|array|float'), code=500)
27+
])
28+
source = serializers.CharField(required=True, error_messages=ErrMessage.char(_('source')), validators=[
29+
validators.RegexValidator(regex=re.compile("^custom|reference$"),
30+
message=_('The field only supports custom|reference'), code=500)
31+
])
32+
33+
34+
class ToolCreateRequest(serializers.Serializer):
35+
name = serializers.CharField(required=True, error_messages=ErrMessage.char(_('tool name')))
36+
37+
desc = serializers.CharField(required=False, allow_null=True, allow_blank=True,
38+
error_messages=ErrMessage.char(_('tool description')))
39+
40+
code = serializers.CharField(required=True, error_messages=ErrMessage.char(_('tool content')))
41+
42+
input_field_list = ToolInputField(required=True, many=True)
43+
44+
init_field_list = serializers.ListField(required=False, default=list)
45+
46+
is_active = serializers.BooleanField(required=False, error_messages=ErrMessage.char(_('Is active')))
47+
48+
49+
class ToolSerializer(serializers.Serializer):
50+
class Create(serializers.Serializer):
51+
user_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid(_('user id')))
52+
53+
def insert(self, instance, with_valid=True):
54+
if with_valid:
55+
self.is_valid(raise_exception=True)
56+
ToolCreateRequest(data=instance).is_valid(raise_exception=True)
57+
tool = Tool(id=uuid.uuid7(), name=instance.get('name'), desc=instance.get('desc'),
58+
code=instance.get('code'),
59+
user_id=self.data.get('user_id'),
60+
input_field_list=instance.get('input_field_list'),
61+
init_field_list=instance.get('init_field_list'),
62+
permission_type=instance.get('permission_type'),
63+
is_active=False)
64+
tool.save()
65+
return ToolModelSerializer(tool).data

apps/tools/tests.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from django.test import TestCase
2+
3+
# Create your tests here.

apps/tools/urls.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from django.urls import path
2+
3+
from . import views
4+
5+
app_name = "tool"
6+
urlpatterns = [
7+
path('workspace/<str:workspace_id>/tool/create', views.ToolCreateView.as_view()),
8+
]

apps/tools/views/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .tool import *

apps/tools/views/tool.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from django.utils.translation import gettext_lazy as _
2+
from drf_spectacular.utils import extend_schema
3+
from rest_framework.request import Request
4+
from rest_framework.views import APIView
5+
6+
from common.auth import TokenAuth
7+
from common.auth.authentication import has_permissions
8+
from common.constants.permission_constants import PermissionConstants
9+
from common.result import result
10+
from tools.api.tool import ToolCreateAPI
11+
from tools.serializers.tool import ToolSerializer
12+
13+
14+
class ToolCreateView(APIView):
15+
authentication_classes = [TokenAuth]
16+
17+
@extend_schema(methods=['POST'],
18+
description=_('Create tool'),
19+
operation_id=_('Create tool'),
20+
request=ToolCreateAPI.get_request(),
21+
responses=ToolCreateAPI.get_response(),
22+
tags=[_('Tool')])
23+
@has_permissions(PermissionConstants.TOOL_CREATE)
24+
# @log(menu='Tool', operate="Create tool",
25+
# get_operation_object=lambda r, k: r.data.get('name'))
26+
def post(self, request: Request, workspace_id: str):
27+
print(workspace_id)
28+
return result.success(ToolSerializer.Create(data={'user_id': request.user.id}).insert(request.data))

apps/util/__init__.py

Whitespace-only changes.

apps/util/field_message.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# coding=utf-8
2+
"""
3+
@project: maxkb
4+
@Author:虎
5+
@file: field_message.py
6+
@date:2024/3/1 14:30
7+
@desc:
8+
"""
9+
from django.utils.functional import lazy
10+
from rest_framework import serializers
11+
12+
13+
def value_(field, value):
14+
return f"【{field}{value}"
15+
16+
17+
def reset_messages(field, messages):
18+
return {key: lazy(value_, str)(field, messages.get(key)) for key in messages}
19+
20+
21+
def reset_message_by_field(field_text, field):
22+
return reset_messages(field_text, {**field.default_error_messages, **field.__bases__[0].default_error_messages})
23+
24+
25+
class ErrMessage:
26+
@staticmethod
27+
def char(field: str):
28+
return reset_message_by_field(field, serializers.CharField)
29+
30+
@staticmethod
31+
def uuid(field: str):
32+
return reset_messages(field, serializers.UUIDField.default_error_messages)
33+
34+
@staticmethod
35+
def integer(field: str):
36+
return reset_messages(field, serializers.IntegerField.default_error_messages)
37+
38+
@staticmethod
39+
def list(field: str):
40+
return reset_messages(field, serializers.ListField.default_error_messages)
41+
42+
@staticmethod
43+
def boolean(field: str):
44+
return reset_messages(field, serializers.BooleanField.default_error_messages)
45+
46+
@staticmethod
47+
def dict(field: str):
48+
return reset_messages(field, serializers.DictField.default_error_messages)
49+
50+
@staticmethod
51+
def float(field: str):
52+
return reset_messages(field, serializers.FloatField.default_error_messages)
53+
54+
@staticmethod
55+
def json(field: str):
56+
return reset_messages(field, serializers.JSONField.default_error_messages)
57+
58+
@staticmethod
59+
def base(field: str):
60+
return reset_messages(field, serializers.Field.default_error_messages)
61+
62+
@staticmethod
63+
def date(field: str):
64+
return reset_messages(field, serializers.DateField.default_error_messages)
65+
66+
@staticmethod
67+
def image(field: str):
68+
return reset_messages(field, serializers.ImageField.default_error_messages)
69+
70+
@staticmethod
71+
def file(field: str):
72+
return reset_messages(field, serializers.FileField.default_error_messages)

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ readme = "README.md"
99
python = "^3.11"
1010
django = "5.2"
1111
drf-spectacular = { extras = ["sidecar"], version = "0.28.0" }
12+
drf-yasg = "1.21.10"
1213
django-redis = "5.4.0"
1314
django-db-connection-pool = "1.2.5"
1415
psycopg = {extras = ["binary"], version = "3.2.6"}
1516
python-dotenv = "1.1.0"
1617
uuid-utils = "0.10.0"
18+
diskcache = "5.6.3"
1719

1820
[build-system]
1921
requires = ["poetry-core"]

0 commit comments

Comments
 (0)