From f45855c34bcb6972ca38d3ff9a2d0a2be883b91e Mon Sep 17 00:00:00 2001 From: CaptainB Date: Fri, 14 Feb 2025 17:40:51 +0800 Subject: [PATCH] feat: Import and Export function lib --- .../serializers/function_lib_serializer.py | 53 ++++++++- .../swagger_api/function_lib_api.py | 21 ++++ apps/function_lib/urls.py | 2 + apps/function_lib/views/function_lib_views.py | 30 ++++- ui/src/api/function-lib.ts | 23 +++- .../locales/lang/en-US/views/function-lib.ts | 1 + .../locales/lang/zh-CN/views/function-lib.ts | 1 + .../lang/zh-Hant/views/function-lib.ts | 1 + ui/src/views/function-lib/index.vue | 103 +++++++++++++++++- 9 files changed, 230 insertions(+), 5 deletions(-) diff --git a/apps/function_lib/serializers/function_lib_serializer.py b/apps/function_lib/serializers/function_lib_serializer.py index cfccb8929..7067e6994 100644 --- a/apps/function_lib/serializers/function_lib_serializer.py +++ b/apps/function_lib/serializers/function_lib_serializer.py @@ -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 \ No newline at end of file diff --git a/apps/function_lib/swagger_api/function_lib_api.py b/apps/function_lib/swagger_api/function_lib_api.py index 90e745a28..89b33b7ac 100644 --- a/apps/function_lib/swagger_api/function_lib_api.py +++ b/apps/function_lib/swagger_api/function_lib_api.py @@ -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')) + ] \ No newline at end of file diff --git a/apps/function_lib/urls.py b/apps/function_lib/urls.py index 784b48093..5bcf70bd9 100644 --- a/apps/function_lib/urls.py +++ b/apps/function_lib/urls.py @@ -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//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/', views.FunctionLibView.Operate.as_view()), path("function_lib//", views.FunctionLibView.Page.as_view(), diff --git a/apps/function_lib/views/function_lib_views.py b/apps/function_lib/views/function_lib_views.py index e24085083..40d1b1084 100644 --- a/apps/function_lib/views/function_lib_views.py +++ b/apps/function_lib/views/function_lib_views.py @@ -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() \ No newline at end of file diff --git a/ui/src/api/function-lib.ts b/ui/src/api/function-lib.ts index c6a8200ed..f9082a308 100644 --- a/ui/src/api/function-lib.ts +++ b/ui/src/api/function-lib.ts @@ -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) => Promise> = ( return post(`${prefix}/pylint`, { code }, {}, loading) } +const exportFunctionLib = ( + id: string, + name: string, + loading?: Ref +) => { + return exportFile( + name + '.flib', + `${prefix}/${id}/export`, + undefined, + loading + ) +} + +const importFunctionLib: (data: any, loading?: Ref) => Promise> = ( + 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 } diff --git a/ui/src/locales/lang/en-US/views/function-lib.ts b/ui/src/locales/lang/en-US/views/function-lib.ts index 99dba41c4..2f56c9456 100644 --- a/ui/src/locales/lang/en-US/views/function-lib.ts +++ b/ui/src/locales/lang/en-US/views/function-lib.ts @@ -3,6 +3,7 @@ export default { createFunction: 'Create Function', editFunction: 'Edit Function', copyFunction: 'Copy Function', + importFunction: 'Import Function', searchBar: { placeholder: 'Search by function name' }, diff --git a/ui/src/locales/lang/zh-CN/views/function-lib.ts b/ui/src/locales/lang/zh-CN/views/function-lib.ts index c5742e757..2ad28bad3 100644 --- a/ui/src/locales/lang/zh-CN/views/function-lib.ts +++ b/ui/src/locales/lang/zh-CN/views/function-lib.ts @@ -3,6 +3,7 @@ export default { createFunction: '创建函数', editFunction: '编辑函数', copyFunction: '复制函数', + importFunction: '导入函数', searchBar: { placeholder: '按函数名称搜索' }, diff --git a/ui/src/locales/lang/zh-Hant/views/function-lib.ts b/ui/src/locales/lang/zh-Hant/views/function-lib.ts index aed4397ea..f7c8eab49 100644 --- a/ui/src/locales/lang/zh-Hant/views/function-lib.ts +++ b/ui/src/locales/lang/zh-Hant/views/function-lib.ts @@ -3,6 +3,7 @@ export default { createFunction: '建立函數', editFunction: '編輯函數', copyFunction: '複製函數', + importFunction: '匯入函數', searchBar: { placeholder: '按函數名稱搜尋' }, diff --git a/ui/src/views/function-lib/index.vue b/ui/src/views/function-lib/index.vue index 8ba9e7fb6..59c1a4db4 100644 --- a/ui/src/views/function-lib/index.vue +++ b/ui/src/views/function-lib/index.vue @@ -42,7 +42,29 @@ > - + +
+ + {{ $t('views.functionLib.createFunction') }} +
+ + +
+ + {{ $t('views.functionLib.importFunction') }} +
+
+
+ + + + + + ([]) const selectUserId = ref('all') +const elUploadRef = ref() 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(() => { })