feat: Import and Export function lib

This commit is contained in:
CaptainB 2025-02-14 17:40:51 +08:00 committed by 刘瑞斌
parent a1fca58864
commit f45855c34b
9 changed files with 230 additions and 5 deletions

View File

@ -7,15 +7,21 @@
@desc: @desc:
""" """
import json import json
import pickle
import re import re
import uuid import uuid
from typing import List
from django.core import validators from django.core import validators
from django.db import transaction
from django.db.models import QuerySet, Q from django.db.models import QuerySet, Q
from rest_framework import serializers from django.http import HttpResponse
from rest_framework import serializers, status
from common.db.search import page_search from common.db.search import page_search
from common.exception.app_exception import AppApiException from common.exception.app_exception import AppApiException
from common.field.common import UploadedFileField
from common.response import result
from common.util.field_message import ErrMessage from common.util.field_message import ErrMessage
from common.util.function_code import FunctionExecutor from common.util.function_code import FunctionExecutor
from function_lib.models.function import FunctionLib from function_lib.models.function import FunctionLib
@ -24,6 +30,11 @@ from django.utils.translation import gettext_lazy as _
function_executor = FunctionExecutor(CONFIG.get('SANDBOX')) function_executor = FunctionExecutor(CONFIG.get('SANDBOX'))
class FlibInstance:
def __init__(self, function_lib: dict, version: str):
self.function_lib = function_lib
self.version = version
class FunctionLibModelSerializer(serializers.ModelSerializer): class FunctionLibModelSerializer(serializers.ModelSerializer):
class Meta: class Meta:
@ -227,3 +238,43 @@ class FunctionLibSerializer(serializers.Serializer):
raise AppApiException(500, _('Function does not exist')) raise AppApiException(500, _('Function does not exist'))
function_lib = QuerySet(FunctionLib).filter(id=self.data.get('id')).first() function_lib = QuerySet(FunctionLib).filter(id=self.data.get('id')).first()
return FunctionLibModelSerializer(function_lib).data return FunctionLibModelSerializer(function_lib).data
def export(self, with_valid=True):
try:
if with_valid:
self.is_valid()
id = self.data.get('id')
function_lib = QuerySet(FunctionLib).filter(id=id).first()
application_dict = FunctionLibModelSerializer(function_lib).data
mk_instance = FlibInstance(application_dict, 'v1')
application_pickle = pickle.dumps(mk_instance)
response = HttpResponse(content_type='text/plain', content=application_pickle)
response['Content-Disposition'] = f'attachment; filename="{function_lib.name}.flib"'
return response
except Exception as e:
return result.error(str(e), response_status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class Import(serializers.Serializer):
file = UploadedFileField(required=True, error_messages=ErrMessage.image(_("file")))
user_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid(_("User ID")))
@transaction.atomic
def import_(self, with_valid=True):
if with_valid:
self.is_valid()
user_id = self.data.get('user_id')
flib_instance_bytes = self.data.get('file').read()
try:
flib_instance = pickle.loads(flib_instance_bytes)
except Exception as e:
raise AppApiException(1001, _("Unsupported file format"))
function_lib = flib_instance.function_lib
function_lib_model = FunctionLib(id=uuid.uuid1(), name=function_lib.get('name'),
desc=function_lib.get('desc'),
code=function_lib.get('code'),
user_id=user_id,
input_field_list=function_lib.get('input_field_list'),
permission_type=function_lib.get('permission_type'),
is_active=function_lib.get('is_active'))
function_lib_model.save()
return True

View File

@ -194,3 +194,24 @@ class FunctionLibApi(ApiMixin):
})) }))
} }
) )
class Export(ApiMixin):
@staticmethod
def get_request_params_api():
return [openapi.Parameter(name='id',
in_=openapi.IN_PATH,
type=openapi.TYPE_STRING,
required=True,
description=_('ID')),
]
class Import(ApiMixin):
@staticmethod
def get_request_params_api():
return [openapi.Parameter(name='file',
in_=openapi.IN_FORM,
type=openapi.TYPE_FILE,
required=True,
description=_('Upload image files'))
]

View File

@ -6,6 +6,8 @@ app_name = "function_lib"
urlpatterns = [ urlpatterns = [
path('function_lib', views.FunctionLibView.as_view()), path('function_lib', views.FunctionLibView.as_view()),
path('function_lib/debug', views.FunctionLibView.Debug.as_view()), path('function_lib/debug', views.FunctionLibView.Debug.as_view()),
path('function_lib/<str:id>/export', views.FunctionLibView.Export.as_view()),
path('function_lib/import', views.FunctionLibView.Import.as_view()),
path('function_lib/pylint', views.PyLintView.as_view()), path('function_lib/pylint', views.PyLintView.as_view()),
path('function_lib/<str:function_lib_id>', views.FunctionLibView.Operate.as_view()), path('function_lib/<str:function_lib_id>', views.FunctionLibView.Operate.as_view()),
path("function_lib/<int:current_page>/<int:page_size>", views.FunctionLibView.Page.as_view(), path("function_lib/<int:current_page>/<int:page_size>", views.FunctionLibView.Page.as_view(),

View File

@ -8,11 +8,12 @@
""" """
from drf_yasg.utils import swagger_auto_schema from drf_yasg.utils import swagger_auto_schema
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.parsers import MultiPartParser
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.views import APIView from rest_framework.views import APIView
from common.auth import TokenAuth, has_permissions from common.auth import TokenAuth, has_permissions
from common.constants.permission_constants import RoleConstants from common.constants.permission_constants import RoleConstants, Permission, Group, Operate
from common.response import result from common.response import result
from function_lib.serializers.function_lib_serializer import FunctionLibSerializer from function_lib.serializers.function_lib_serializer import FunctionLibSerializer
from function_lib.swagger_api.function_lib_api import FunctionLibApi from function_lib.swagger_api.function_lib_api import FunctionLibApi
@ -109,3 +110,30 @@ class FunctionLibView(APIView):
'user_id': request.user.id, 'user_id': request.user.id,
'select_user_id': request.query_params.get('select_user_id')}).page( 'select_user_id': request.query_params.get('select_user_id')}).page(
current_page, page_size)) current_page, page_size))
class Import(APIView):
authentication_classes = [TokenAuth]
parser_classes = [MultiPartParser]
@action(methods="POST", detail=False)
@swagger_auto_schema(operation_summary=_("Import function"), operation_id=_("Import function"),
manual_parameters=FunctionLibApi.Import.get_request_params_api(),
tags=[_("function")]
)
@has_permissions(RoleConstants.ADMIN, RoleConstants.USER)
def post(self, request: Request):
return result.success(FunctionLibSerializer.Import(
data={'user_id': request.user.id, 'file': request.FILES.get('file')}).import_())
class Export(APIView):
authentication_classes = [TokenAuth]
@action(methods="GET", detail=False)
@swagger_auto_schema(operation_summary=_("Export function"), operation_id=_("Export function"),
manual_parameters=FunctionLibApi.Export.get_request_params_api(),
tags=[_("function")]
)
@has_permissions(RoleConstants.ADMIN, RoleConstants.USER)
def get(self, request: Request, id: str):
return FunctionLibSerializer.Operate(
data={'id': id, 'user_id': request.user.id}).export()

View File

@ -1,5 +1,5 @@
import { Result } from '@/request/Result' import { Result } from '@/request/Result'
import { get, post, del, put } from '@/request/index' import { get, post, del, put, exportFile } from '@/request/index'
import type { pageRequest } from '@/api/type/common' import type { pageRequest } from '@/api/type/common'
import type { functionLibData } from '@/api/type/function-lib' import type { functionLibData } from '@/api/type/function-lib'
import { type Ref } from 'vue' import { type Ref } from 'vue'
@ -99,6 +99,25 @@ const pylint: (code: string, loading?: Ref<boolean>) => Promise<Result<any>> = (
return post(`${prefix}/pylint`, { code }, {}, loading) return post(`${prefix}/pylint`, { code }, {}, loading)
} }
const exportFunctionLib = (
id: string,
name: string,
loading?: Ref<boolean>
) => {
return exportFile(
name + '.flib',
`${prefix}/${id}/export`,
undefined,
loading
)
}
const importFunctionLib: (data: any, loading?: Ref<boolean>) => Promise<Result<any>> = (
data,
loading
) => {
return post(`${prefix}/import`, data, undefined, loading)
}
export default { export default {
getFunctionLib, getFunctionLib,
postFunctionLib, postFunctionLib,
@ -107,5 +126,7 @@ export default {
getAllFunctionLib, getAllFunctionLib,
delFunctionLib, delFunctionLib,
getFunctionLibById, getFunctionLibById,
exportFunctionLib,
importFunctionLib,
pylint pylint
} }

View File

@ -3,6 +3,7 @@ export default {
createFunction: 'Create Function', createFunction: 'Create Function',
editFunction: 'Edit Function', editFunction: 'Edit Function',
copyFunction: 'Copy Function', copyFunction: 'Copy Function',
importFunction: 'Import Function',
searchBar: { searchBar: {
placeholder: 'Search by function name' placeholder: 'Search by function name'
}, },

View File

@ -3,6 +3,7 @@ export default {
createFunction: '创建函数', createFunction: '创建函数',
editFunction: '编辑函数', editFunction: '编辑函数',
copyFunction: '复制函数', copyFunction: '复制函数',
importFunction: '导入函数',
searchBar: { searchBar: {
placeholder: '按函数名称搜索' placeholder: '按函数名称搜索'
}, },

View File

@ -3,6 +3,7 @@ export default {
createFunction: '建立函數', createFunction: '建立函數',
editFunction: '編輯函數', editFunction: '編輯函數',
copyFunction: '複製函數', copyFunction: '複製函數',
importFunction: '匯入函數',
searchBar: { searchBar: {
placeholder: '按函數名稱搜尋' placeholder: '按函數名稱搜尋'
}, },

View File

@ -42,7 +42,29 @@
> >
<el-row :gutter="15"> <el-row :gutter="15">
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6" class="mb-16"> <el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6" class="mb-16">
<CardAdd :title="$t('views.functionLib.createFunction')" @click="openCreateDialog()" /> <el-card shadow="hover" class="application-card-add" style="--el-card-padding: 8px">
<div class="card-add-button flex align-center cursor p-8" @click="openCreateDialog">
<AppIcon iconName="app-add-application" class="mr-8"></AppIcon>
{{ $t('views.functionLib.createFunction') }}
</div>
<el-divider style="margin: 8px 0" />
<el-upload
ref="elUploadRef"
:file-list="[]"
action="#"
multiple
:auto-upload="false"
:show-file-list="false"
:limit="1"
:on-change="(file: any, fileList: any) => importFunctionLib(file)"
class="card-add-button"
>
<div class="flex align-center cursor p-8">
<AppIcon iconName="app-import" class="mr-8"></AppIcon>
{{ $t('views.functionLib.importFunction') }}
</div>
</el-upload>
</el-card>
</el-col> </el-col>
<el-col <el-col
:xs="24" :xs="24"
@ -98,6 +120,12 @@
</el-button> </el-button>
</el-tooltip> </el-tooltip>
<el-divider direction="vertical" /> <el-divider direction="vertical" />
<el-tooltip effect="dark" :content="$t('common.export')" placement="top">
<el-button text @click.stop="exportFunctionLib(item)">
<AppIcon iconName="app-export"></AppIcon>
</el-button>
</el-tooltip>
<el-divider direction="vertical" />
<el-tooltip effect="dark" :content="$t('common.delete')" placement="top"> <el-tooltip effect="dark" :content="$t('common.delete')" placement="top">
<el-button <el-button
:disabled="item.permission_type === 'PUBLIC' && !canEdit(item)" :disabled="item.permission_type === 'PUBLIC' && !canEdit(item)"
@ -131,7 +159,7 @@ import { ref, onMounted, reactive } from 'vue'
import { cloneDeep } from 'lodash' import { cloneDeep } from 'lodash'
import functionLibApi from '@/api/function-lib' import functionLibApi from '@/api/function-lib'
import FunctionFormDrawer from './component/FunctionFormDrawer.vue' import FunctionFormDrawer from './component/FunctionFormDrawer.vue'
import { MsgSuccess, MsgConfirm } from '@/utils/message' import { MsgSuccess, MsgConfirm, MsgError } from '@/utils/message'
import useStore from '@/stores' import useStore from '@/stores'
import applicationApi from '@/api/application' import applicationApi from '@/api/application'
import { t } from '@/locales' import { t } from '@/locales'
@ -161,6 +189,7 @@ interface UserOption {
const userOptions = ref<UserOption[]>([]) const userOptions = ref<UserOption[]>([])
const selectUserId = ref('all') const selectUserId = ref('all')
const elUploadRef = ref<any>()
const canEdit = (row: any) => { const canEdit = (row: any) => {
return user.userInfo?.id === row?.user_id return user.userInfo?.id === row?.user_id
@ -242,6 +271,40 @@ function copyFunctionLib(row: any) {
FunctionFormDrawerRef.value.open(obj) FunctionFormDrawerRef.value.open(obj)
} }
function exportFunctionLib(row: any) {
functionLibApi.exportFunctionLib(row.id, row.name, loading)
.catch((e: any) => {
if (e.response.status !== 403) {
e.response.data.text().then((res: string) => {
MsgError(`${t('views.application.tip.ExportError')}:${JSON.parse(res).message}`)
})
}
})
}
function importFunctionLib(file: any) {
const formData = new FormData()
formData.append('file', file.raw, file.name)
elUploadRef.value.clearFiles()
functionLibApi
.importFunctionLib(formData, loading)
.then(async (res: any) => {
if (res?.data) {
searchHandle()
}
})
.catch((e: any) => {
if (e.code === 400) {
MsgConfirm(t('common.tip'), t('views.application.tip.professionalMessage'), {
cancelButtonText: t('common.confirm'),
confirmButtonText: t('common.professional')
}).then(() => {
window.open('https://maxkb.cn/pricing.html', '_blank')
})
}
})
}
function getList() { function getList() {
const params = { const params = {
...(searchValue.value && { name: searchValue.value }), ...(searchValue.value && { name: searchValue.value }),
@ -303,6 +366,42 @@ onMounted(() => {
}) })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.application-card-add {
width: 100%;
font-size: 14px;
min-height: var(--card-min-height);
border: 1px dashed var(--el-border-color);
background: var(--el-disabled-bg-color);
border-radius: 8px;
box-sizing: border-box;
&:hover {
border: 1px solid var(--el-card-bg-color);
background-color: var(--el-card-bg-color);
}
.card-add-button {
&:hover {
border-radius: 4px;
background: var(--app-text-color-light-1);
}
:deep(.el-upload) {
display: block;
width: 100%;
color: var(--el-text-color-regular);
}
}
}
.application-card {
.status-tag {
position: absolute;
right: 16px;
top: 15px;
}
}
.function-lib-list-container { .function-lib-list-container {
.status-button { .status-button {
position: absolute; position: absolute;