Skip to content

Commit f45855c

Browse files
committed
feat: Import and Export function lib
1 parent a1fca58 commit f45855c

File tree

9 files changed

+230
-5
lines changed

9 files changed

+230
-5
lines changed

apps/function_lib/serializers/function_lib_serializer.py

+52-1
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,21 @@
77
@desc:
88
"""
99
import json
10+
import pickle
1011
import re
1112
import uuid
13+
from typing import List
1214

1315
from django.core import validators
16+
from django.db import transaction
1417
from django.db.models import QuerySet, Q
15-
from rest_framework import serializers
18+
from django.http import HttpResponse
19+
from rest_framework import serializers, status
1620

1721
from common.db.search import page_search
1822
from common.exception.app_exception import AppApiException
23+
from common.field.common import UploadedFileField
24+
from common.response import result
1925
from common.util.field_message import ErrMessage
2026
from common.util.function_code import FunctionExecutor
2127
from function_lib.models.function import FunctionLib
@@ -24,6 +30,11 @@
2430

2531
function_executor = FunctionExecutor(CONFIG.get('SANDBOX'))
2632

33+
class FlibInstance:
34+
def __init__(self, function_lib: dict, version: str):
35+
self.function_lib = function_lib
36+
self.version = version
37+
2738

2839
class FunctionLibModelSerializer(serializers.ModelSerializer):
2940
class Meta:
@@ -227,3 +238,43 @@ def one(self, with_valid=True):
227238
raise AppApiException(500, _('Function does not exist'))
228239
function_lib = QuerySet(FunctionLib).filter(id=self.data.get('id')).first()
229240
return FunctionLibModelSerializer(function_lib).data
241+
242+
def export(self, with_valid=True):
243+
try:
244+
if with_valid:
245+
self.is_valid()
246+
id = self.data.get('id')
247+
function_lib = QuerySet(FunctionLib).filter(id=id).first()
248+
application_dict = FunctionLibModelSerializer(function_lib).data
249+
mk_instance = FlibInstance(application_dict, 'v1')
250+
application_pickle = pickle.dumps(mk_instance)
251+
response = HttpResponse(content_type='text/plain', content=application_pickle)
252+
response['Content-Disposition'] = f'attachment; filename="{function_lib.name}.flib"'
253+
return response
254+
except Exception as e:
255+
return result.error(str(e), response_status=status.HTTP_500_INTERNAL_SERVER_ERROR)
256+
257+
class Import(serializers.Serializer):
258+
file = UploadedFileField(required=True, error_messages=ErrMessage.image(_("file")))
259+
user_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid(_("User ID")))
260+
261+
@transaction.atomic
262+
def import_(self, with_valid=True):
263+
if with_valid:
264+
self.is_valid()
265+
user_id = self.data.get('user_id')
266+
flib_instance_bytes = self.data.get('file').read()
267+
try:
268+
flib_instance = pickle.loads(flib_instance_bytes)
269+
except Exception as e:
270+
raise AppApiException(1001, _("Unsupported file format"))
271+
function_lib = flib_instance.function_lib
272+
function_lib_model = FunctionLib(id=uuid.uuid1(), name=function_lib.get('name'),
273+
desc=function_lib.get('desc'),
274+
code=function_lib.get('code'),
275+
user_id=user_id,
276+
input_field_list=function_lib.get('input_field_list'),
277+
permission_type=function_lib.get('permission_type'),
278+
is_active=function_lib.get('is_active'))
279+
function_lib_model.save()
280+
return True

apps/function_lib/swagger_api/function_lib_api.py

+21
Original file line numberDiff line numberDiff line change
@@ -194,3 +194,24 @@ def get_request_body_api():
194194
}))
195195
}
196196
)
197+
198+
class Export(ApiMixin):
199+
@staticmethod
200+
def get_request_params_api():
201+
return [openapi.Parameter(name='id',
202+
in_=openapi.IN_PATH,
203+
type=openapi.TYPE_STRING,
204+
required=True,
205+
description=_('ID')),
206+
207+
]
208+
209+
class Import(ApiMixin):
210+
@staticmethod
211+
def get_request_params_api():
212+
return [openapi.Parameter(name='file',
213+
in_=openapi.IN_FORM,
214+
type=openapi.TYPE_FILE,
215+
required=True,
216+
description=_('Upload image files'))
217+
]

apps/function_lib/urls.py

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
urlpatterns = [
77
path('function_lib', views.FunctionLibView.as_view()),
88
path('function_lib/debug', views.FunctionLibView.Debug.as_view()),
9+
path('function_lib/<str:id>/export', views.FunctionLibView.Export.as_view()),
10+
path('function_lib/import', views.FunctionLibView.Import.as_view()),
911
path('function_lib/pylint', views.PyLintView.as_view()),
1012
path('function_lib/<str:function_lib_id>', views.FunctionLibView.Operate.as_view()),
1113
path("function_lib/<int:current_page>/<int:page_size>", views.FunctionLibView.Page.as_view(),

apps/function_lib/views/function_lib_views.py

+29-1
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@
88
"""
99
from drf_yasg.utils import swagger_auto_schema
1010
from rest_framework.decorators import action
11+
from rest_framework.parsers import MultiPartParser
1112
from rest_framework.request import Request
1213
from rest_framework.views import APIView
1314

1415
from common.auth import TokenAuth, has_permissions
15-
from common.constants.permission_constants import RoleConstants
16+
from common.constants.permission_constants import RoleConstants, Permission, Group, Operate
1617
from common.response import result
1718
from function_lib.serializers.function_lib_serializer import FunctionLibSerializer
1819
from function_lib.swagger_api.function_lib_api import FunctionLibApi
@@ -109,3 +110,30 @@ def get(self, request: Request, current_page: int, page_size: int):
109110
'user_id': request.user.id,
110111
'select_user_id': request.query_params.get('select_user_id')}).page(
111112
current_page, page_size))
113+
114+
class Import(APIView):
115+
authentication_classes = [TokenAuth]
116+
parser_classes = [MultiPartParser]
117+
118+
@action(methods="POST", detail=False)
119+
@swagger_auto_schema(operation_summary=_("Import function"), operation_id=_("Import function"),
120+
manual_parameters=FunctionLibApi.Import.get_request_params_api(),
121+
tags=[_("function")]
122+
)
123+
@has_permissions(RoleConstants.ADMIN, RoleConstants.USER)
124+
def post(self, request: Request):
125+
return result.success(FunctionLibSerializer.Import(
126+
data={'user_id': request.user.id, 'file': request.FILES.get('file')}).import_())
127+
128+
class Export(APIView):
129+
authentication_classes = [TokenAuth]
130+
131+
@action(methods="GET", detail=False)
132+
@swagger_auto_schema(operation_summary=_("Export function"), operation_id=_("Export function"),
133+
manual_parameters=FunctionLibApi.Export.get_request_params_api(),
134+
tags=[_("function")]
135+
)
136+
@has_permissions(RoleConstants.ADMIN, RoleConstants.USER)
137+
def get(self, request: Request, id: str):
138+
return FunctionLibSerializer.Operate(
139+
data={'id': id, 'user_id': request.user.id}).export()

ui/src/api/function-lib.ts

+22-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Result } from '@/request/Result'
2-
import { get, post, del, put } from '@/request/index'
2+
import { get, post, del, put, exportFile } from '@/request/index'
33
import type { pageRequest } from '@/api/type/common'
44
import type { functionLibData } from '@/api/type/function-lib'
55
import { type Ref } from 'vue'
@@ -99,6 +99,25 @@ const pylint: (code: string, loading?: Ref<boolean>) => Promise<Result<any>> = (
9999
return post(`${prefix}/pylint`, { code }, {}, loading)
100100
}
101101

102+
const exportFunctionLib = (
103+
id: string,
104+
name: string,
105+
loading?: Ref<boolean>
106+
) => {
107+
return exportFile(
108+
name + '.flib',
109+
`${prefix}/${id}/export`,
110+
undefined,
111+
loading
112+
)
113+
}
114+
115+
const importFunctionLib: (data: any, loading?: Ref<boolean>) => Promise<Result<any>> = (
116+
data,
117+
loading
118+
) => {
119+
return post(`${prefix}/import`, data, undefined, loading)
120+
}
102121
export default {
103122
getFunctionLib,
104123
postFunctionLib,
@@ -107,5 +126,7 @@ export default {
107126
getAllFunctionLib,
108127
delFunctionLib,
109128
getFunctionLibById,
129+
exportFunctionLib,
130+
importFunctionLib,
110131
pylint
111132
}

ui/src/locales/lang/en-US/views/function-lib.ts

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export default {
33
createFunction: 'Create Function',
44
editFunction: 'Edit Function',
55
copyFunction: 'Copy Function',
6+
importFunction: 'Import Function',
67
searchBar: {
78
placeholder: 'Search by function name'
89
},

ui/src/locales/lang/zh-CN/views/function-lib.ts

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export default {
33
createFunction: '创建函数',
44
editFunction: '编辑函数',
55
copyFunction: '复制函数',
6+
importFunction: '导入函数',
67
searchBar: {
78
placeholder: '按函数名称搜索'
89
},

ui/src/locales/lang/zh-Hant/views/function-lib.ts

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export default {
33
createFunction: '建立函數',
44
editFunction: '編輯函數',
55
copyFunction: '複製函數',
6+
importFunction: '匯入函數',
67
searchBar: {
78
placeholder: '按函數名稱搜尋'
89
},

ui/src/views/function-lib/index.vue

+101-2
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,29 @@
4242
>
4343
<el-row :gutter="15">
4444
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6" class="mb-16">
45-
<CardAdd :title="$t('views.functionLib.createFunction')" @click="openCreateDialog()" />
45+
<el-card shadow="hover" class="application-card-add" style="--el-card-padding: 8px">
46+
<div class="card-add-button flex align-center cursor p-8" @click="openCreateDialog">
47+
<AppIcon iconName="app-add-application" class="mr-8"></AppIcon>
48+
{{ $t('views.functionLib.createFunction') }}
49+
</div>
50+
<el-divider style="margin: 8px 0" />
51+
<el-upload
52+
ref="elUploadRef"
53+
:file-list="[]"
54+
action="#"
55+
multiple
56+
:auto-upload="false"
57+
:show-file-list="false"
58+
:limit="1"
59+
:on-change="(file: any, fileList: any) => importFunctionLib(file)"
60+
class="card-add-button"
61+
>
62+
<div class="flex align-center cursor p-8">
63+
<AppIcon iconName="app-import" class="mr-8"></AppIcon>
64+
{{ $t('views.functionLib.importFunction') }}
65+
</div>
66+
</el-upload>
67+
</el-card>
4668
</el-col>
4769
<el-col
4870
:xs="24"
@@ -98,6 +120,12 @@
98120
</el-button>
99121
</el-tooltip>
100122
<el-divider direction="vertical" />
123+
<el-tooltip effect="dark" :content="$t('common.export')" placement="top">
124+
<el-button text @click.stop="exportFunctionLib(item)">
125+
<AppIcon iconName="app-export"></AppIcon>
126+
</el-button>
127+
</el-tooltip>
128+
<el-divider direction="vertical" />
101129
<el-tooltip effect="dark" :content="$t('common.delete')" placement="top">
102130
<el-button
103131
:disabled="item.permission_type === 'PUBLIC' && !canEdit(item)"
@@ -131,7 +159,7 @@ import { ref, onMounted, reactive } from 'vue'
131159
import { cloneDeep } from 'lodash'
132160
import functionLibApi from '@/api/function-lib'
133161
import FunctionFormDrawer from './component/FunctionFormDrawer.vue'
134-
import { MsgSuccess, MsgConfirm } from '@/utils/message'
162+
import { MsgSuccess, MsgConfirm, MsgError } from '@/utils/message'
135163
import useStore from '@/stores'
136164
import applicationApi from '@/api/application'
137165
import { t } from '@/locales'
@@ -161,6 +189,7 @@ interface UserOption {
161189
const userOptions = ref<UserOption[]>([])
162190
163191
const selectUserId = ref('all')
192+
const elUploadRef = ref<any>()
164193
165194
const canEdit = (row: any) => {
166195
return user.userInfo?.id === row?.user_id
@@ -242,6 +271,40 @@ function copyFunctionLib(row: any) {
242271
FunctionFormDrawerRef.value.open(obj)
243272
}
244273
274+
function exportFunctionLib(row: any) {
275+
functionLibApi.exportFunctionLib(row.id, row.name, loading)
276+
.catch((e: any) => {
277+
if (e.response.status !== 403) {
278+
e.response.data.text().then((res: string) => {
279+
MsgError(`${t('views.application.tip.ExportError')}:${JSON.parse(res).message}`)
280+
})
281+
}
282+
})
283+
}
284+
285+
function importFunctionLib(file: any) {
286+
const formData = new FormData()
287+
formData.append('file', file.raw, file.name)
288+
elUploadRef.value.clearFiles()
289+
functionLibApi
290+
.importFunctionLib(formData, loading)
291+
.then(async (res: any) => {
292+
if (res?.data) {
293+
searchHandle()
294+
}
295+
})
296+
.catch((e: any) => {
297+
if (e.code === 400) {
298+
MsgConfirm(t('common.tip'), t('views.application.tip.professionalMessage'), {
299+
cancelButtonText: t('common.confirm'),
300+
confirmButtonText: t('common.professional')
301+
}).then(() => {
302+
window.open('https://maxkb.cn/#.html', '_blank')
303+
})
304+
}
305+
})
306+
}
307+
245308
function getList() {
246309
const params = {
247310
...(searchValue.value && { name: searchValue.value }),
@@ -303,6 +366,42 @@ onMounted(() => {
303366
})
304367
</script>
305368
<style lang="scss" scoped>
369+
.application-card-add {
370+
width: 100%;
371+
font-size: 14px;
372+
min-height: var(--card-min-height);
373+
border: 1px dashed var(--el-border-color);
374+
background: var(--el-disabled-bg-color);
375+
border-radius: 8px;
376+
box-sizing: border-box;
377+
378+
&:hover {
379+
border: 1px solid var(--el-card-bg-color);
380+
background-color: var(--el-card-bg-color);
381+
}
382+
383+
.card-add-button {
384+
&:hover {
385+
border-radius: 4px;
386+
background: var(--app-text-color-light-1);
387+
}
388+
389+
:deep(.el-upload) {
390+
display: block;
391+
width: 100%;
392+
color: var(--el-text-color-regular);
393+
}
394+
}
395+
}
396+
397+
.application-card {
398+
.status-tag {
399+
position: absolute;
400+
right: 16px;
401+
top: 15px;
402+
}
403+
}
404+
306405
.function-lib-list-container {
307406
.status-button {
308407
position: absolute;

0 commit comments

Comments
 (0)