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:
"""
import json
import pickle
import re
import uuid
from typing import List
from django.core import validators
from django.db import transaction
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.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.function_code import FunctionExecutor
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'))
class FlibInstance:
def __init__(self, function_lib: dict, version: str):
self.function_lib = function_lib
self.version = version
class FunctionLibModelSerializer(serializers.ModelSerializer):
class Meta:
@ -227,3 +238,43 @@ class FunctionLibSerializer(serializers.Serializer):
raise AppApiException(500, _('Function does not exist'))
function_lib = QuerySet(FunctionLib).filter(id=self.data.get('id')).first()
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 = [
path('function_lib', views.FunctionLibView.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/<str:function_lib_id>', views.FunctionLibView.Operate.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 rest_framework.decorators import action
from rest_framework.parsers import MultiPartParser
from rest_framework.request import Request
from rest_framework.views import APIView
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 function_lib.serializers.function_lib_serializer import FunctionLibSerializer
from function_lib.swagger_api.function_lib_api import FunctionLibApi
@ -109,3 +110,30 @@ class FunctionLibView(APIView):
'user_id': request.user.id,
'select_user_id': request.query_params.get('select_user_id')}).page(
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 { 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 { functionLibData } from '@/api/type/function-lib'
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)
}
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 {
getFunctionLib,
postFunctionLib,
@ -107,5 +126,7 @@ export default {
getAllFunctionLib,
delFunctionLib,
getFunctionLibById,
exportFunctionLib,
importFunctionLib,
pylint
}

View File

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

View File

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

View File

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

View File

@ -42,7 +42,29 @@
>
<el-row :gutter="15">
<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
:xs="24"
@ -98,6 +120,12 @@
</el-button>
</el-tooltip>
<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-button
:disabled="item.permission_type === 'PUBLIC' && !canEdit(item)"
@ -131,7 +159,7 @@ import { ref, onMounted, reactive } from 'vue'
import { cloneDeep } from 'lodash'
import functionLibApi from '@/api/function-lib'
import FunctionFormDrawer from './component/FunctionFormDrawer.vue'
import { MsgSuccess, MsgConfirm } from '@/utils/message'
import { MsgSuccess, MsgConfirm, MsgError } from '@/utils/message'
import useStore from '@/stores'
import applicationApi from '@/api/application'
import { t } from '@/locales'
@ -161,6 +189,7 @@ interface UserOption {
const userOptions = ref<UserOption[]>([])
const selectUserId = ref('all')
const elUploadRef = ref<any>()
const canEdit = (row: any) => {
return user.userInfo?.id === row?.user_id
@ -242,6 +271,40 @@ function copyFunctionLib(row: any) {
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() {
const params = {
...(searchValue.value && { name: searchValue.value }),
@ -303,6 +366,42 @@ onMounted(() => {
})
</script>
<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 {
.status-button {
position: absolute;