diff --git a/apps/application/api/application_api.py b/apps/application/api/application_api.py index f59ddf5b7f7..5b6b4171628 100644 --- a/apps/application/api/application_api.py +++ b/apps/application/api/application_api.py @@ -15,6 +15,7 @@ ApplicationImportRequest, ApplicationEditSerializer, TextToSpeechRequest, SpeechToTextRequest, PlayDemoTextRequest from common.mixins.api_mixin import APIMixin from common.result import ResultSerializer, ResultPageSerializer, DefaultResultSerializer +from knowledge.serializers.common import BatchSerializer, BatchMoveSerializer class ApplicationCreateRequest(ApplicationCreateSerializer.SimplateRequest): @@ -160,6 +161,27 @@ def get_parameters(): ] +class ApplicationBatchOperateAPI(APIMixin): + @staticmethod + def get_parameters(): + return [ + OpenApiParameter( + name="workspace_id", + description="工作空间id", + type=OpenApiTypes.STR, + location='path', + required=True, + ) + ] + @staticmethod + def get_request(): + return BatchSerializer + + @staticmethod + def get_move_request(): + return BatchMoveSerializer + + class ApplicationExportAPI(APIMixin): @staticmethod def get_parameters(): diff --git a/apps/application/serializers/application.py b/apps/application/serializers/application.py index bfc0658c3c7..abbcfd2b685 100644 --- a/apps/application/serializers/application.py +++ b/apps/application/serializers/application.py @@ -46,6 +46,7 @@ from common.utils.logger import maxkb_logger from common.utils.tool_code import ToolExecutor from knowledge.models import Knowledge, KnowledgeScope, File, FileSourceType +from knowledge.serializers.common import BatchSerializer, BatchMoveSerializer from knowledge.serializers.knowledge import KnowledgeSerializer, KnowledgeModelSerializer from maxkb.conf import PROJECT_DIR from models_provider.models import Model @@ -1314,3 +1315,52 @@ def play_demo_text(self, instance, with_valid=True): tts_model_id = instance.pop('tts_model_id') model = get_model_instance_by_model_workspace_id(tts_model_id, self.data.get('workspace_id'), **instance) return model.text_to_speech(text) + + +class ApplicationBatchOperateSerializer(serializers.Serializer): + workspace_id = serializers.CharField(required=True, label=_("Workspace ID")) + + def is_valid(self, *, raise_exception=False): + super().is_valid(raise_exception=True) + + @transaction.atomic + def batch_delete(self, instance: Dict, with_valid=True): + from trigger.handler.simple_tools import deploy + from trigger.serializers.trigger import TriggerModelSerializer + + if with_valid: + BatchSerializer(data=instance).is_valid(model=Application,raise_exception=True) + self.is_valid(raise_exception=True) + id_list = instance.get("id_list") + workspace_id = self.data.get('workspace_id') + + QuerySet(ApplicationVersion).filter(application_id__in=id_list).delete() + QuerySet(ResourceMapping).filter( + Q(target_id__in=id_list) | Q(source_id__in=id_list) + ).delete() + + QuerySet(Application).filter(id__in=id_list, workspace_id=workspace_id).delete() + + trigger_ids = list( + QuerySet(TriggerTask).filter( + source_type="APPLICATION", source_id__in=id_list + ).values('trigger_id').distinct() + ) + QuerySet(TriggerTask).filter(source_type="APPLICATION", source_id__in=id_list).delete() + + for trigger_id in trigger_ids: + trigger = Trigger.objects.filter(id=trigger_id['trigger_id']).first() + if trigger and trigger.is_active: + deploy(TriggerModelSerializer(trigger).data, **{}) + return True + + def batch_move(self, instance: Dict, with_valid=True): + if with_valid: + BatchMoveSerializer(data=instance).is_valid(model=Application, raise_exception=True) + self.is_valid(raise_exception=True) + id_list = instance.get("id_list") + folder_id = instance.get("folder_id") + workspace_id = self.data.get('workspace_id') + + QuerySet(Application).filter(id__in=id_list, workspace_id=workspace_id).update(folder_id=folder_id) + return True diff --git a/apps/application/urls.py b/apps/application/urls.py index dca864b86fd..5b8c7d32ca6 100644 --- a/apps/application/urls.py +++ b/apps/application/urls.py @@ -9,6 +9,8 @@ path('workspace//application', views.ApplicationAPI.as_view(), name='application'), path('workspace//application/folder//import', views.ApplicationAPI.Import.as_view()), path('workspace//application//', views.ApplicationAPI.Page.as_view(), name='application_page'), + path('workspace//application/batch_delete', views.ApplicationAPI.BatchDelete.as_view()), + path('workspace//application/batch_move', views.ApplicationAPI.BatchMove.as_view()), path('workspace//application/', views.ApplicationAPI.Operate.as_view()), path('workspace//application//publish', views.ApplicationAPI.Publish.as_view()), path('workspace//application//move/', views.ApplicationAPI.Move.as_view()), diff --git a/apps/application/views/application.py b/apps/application/views/application.py index ea2bf31dc26..ecfa1ae6b4e 100644 --- a/apps/application/views/application.py +++ b/apps/application/views/application.py @@ -15,12 +15,14 @@ from rest_framework.views import APIView from application.api.application_api import ApplicationCreateAPI, ApplicationQueryAPI, ApplicationImportAPI, \ - ApplicationExportAPI, ApplicationOperateAPI, ApplicationEditAPI, TextToSpeechAPI, SpeechToTextAPI, PlayDemoTextAPI + ApplicationExportAPI, ApplicationOperateAPI, ApplicationEditAPI, TextToSpeechAPI, SpeechToTextAPI, PlayDemoTextAPI, \ + ApplicationBatchOperateAPI from application.models import Application -from application.serializers.application import ApplicationSerializer, Query, ApplicationOperateSerializer +from application.serializers.application import ApplicationSerializer, Query, ApplicationOperateSerializer, \ + ApplicationBatchOperateSerializer from common import result from common.auth import TokenAuth -from common.auth.authentication import has_permissions, get_is_permissions +from common.auth.authentication import has_permissions, get_is_permissions, check_batch_permissions from common.constants.permission_constants import PermissionConstants, RoleConstants, ViewPermission, CompareConstants from common.log.log import log from tools.api.tool import GetInternalToolAPI @@ -35,6 +37,16 @@ def get_application_operation_object(application_id): return {} +def get_application_operation_object_batch(application_id_list): + application_model_list = QuerySet(model=Application).filter(id__in=application_id_list) + if application_model_list is not None: + return { + "name": f'[{",".join([app.name for app in application_model_list])}]', + 'application_list': [{'name': app.name} for app in application_model_list] + } + return {} + + class ApplicationAPI(APIView): authentication_classes = [TokenAuth] @@ -296,6 +308,81 @@ def get(self, request: Request): 'name': request.query_params.get('name', ''), }).get_appstore_templates()) + class BatchDelete(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=['PUT'], + description=_("Batch delete applications"), + summary=_("Batch delete applications"), + operation_id=_("Batch delete applications"), + parameters=ApplicationBatchOperateAPI.get_parameters(), + request=ApplicationBatchOperateAPI.get_request(), + responses=result.DefaultResultSerializer, + tags=[_('Application')] + ) + @has_permissions(PermissionConstants.APPLICATION_BATCH_DELETE.get_workspace_permission(), + RoleConstants.USER.get_workspace_role(), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role() + ) + def put(self, request: Request, workspace_id: str): + id_list = request.data.get('id_list', []) + permitted_ids = check_batch_permissions( + request, id_list, 'application_id', + (PermissionConstants.APPLICATION_DELETE.get_workspace_application_permission(), + PermissionConstants.APPLICATION_DELETE.get_workspace_permission_workspace_manage_role(), + ViewPermission([RoleConstants.USER.get_workspace_role()], + [PermissionConstants.APPLICATION.get_workspace_application_permission()], + CompareConstants.AND), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role()), workspace_id=workspace_id + ) + @log(menu='Application', operate='Batch delete applications', + get_operation_object=lambda r, k: get_application_operation_object_batch(permitted_ids)) + def inner(view,r, **kwargs): + return ApplicationBatchOperateSerializer( + data={'workspace_id': workspace_id, 'user_id': request.user.id} + ).batch_delete({'id_list': permitted_ids}) + + return result.success(inner(self,request, workspace_id=workspace_id)) + + class BatchMove(APIView): + authentication_classes = [TokenAuth] + + @extend_schema( + methods=['PUT'], + description=_("Batch move applications"), + summary=_("Batch move applications"), + operation_id=_("Batch move applications"), + parameters=ApplicationBatchOperateAPI.get_parameters(), + request=ApplicationBatchOperateAPI.get_move_request(), + responses=result.DefaultResultSerializer, + tags=[_('Application')] + ) + @has_permissions(PermissionConstants.APPLICATION_BATCH_MOVE.get_workspace_permission(), + RoleConstants.USER.get_workspace_role(), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role() + ) + def put(self, request: Request, workspace_id: str): + id_list = request.data.get('id_list', []) + permitted_ids = check_batch_permissions( + request, id_list, 'application_id', + (PermissionConstants.APPLICATION_EDIT.get_workspace_application_permission(), + PermissionConstants.APPLICATION_EDIT.get_workspace_permission_workspace_manage_role(), + ViewPermission([RoleConstants.USER.get_workspace_role()], + [PermissionConstants.APPLICATION.get_workspace_application_permission()], + CompareConstants.AND), + RoleConstants.WORKSPACE_MANAGE.get_workspace_role()), + workspace_id=workspace_id + ) + + @log(menu='Application', operate='Batch move applications', + get_operation_object=lambda r, k: get_application_operation_object_batch(permitted_ids)) + def inner(view,r, **kwargs): + return ApplicationBatchOperateSerializer( + data={'workspace_id': workspace_id, 'user_id': request.user.id} + ).batch_move({'id_list': permitted_ids, 'folder_id': request.data.get('folder_id')}) + + return result.success(inner(self,request, workspace_id=workspace_id)) class McpServers(APIView): authentication_classes = [TokenAuth] diff --git a/apps/common/auth/authentication.py b/apps/common/auth/authentication.py index b1c45092f02..7c619849cba 100644 --- a/apps/common/auth/authentication.py +++ b/apps/common/auth/authentication.py @@ -9,6 +9,7 @@ from typing import List from django.utils.translation import gettext_lazy as _ +from rest_framework.request import Request from common.constants.permission_constants import PermissionConstants, RoleConstants, ViewPermission, CompareConstants, \ Permission, Role @@ -92,6 +93,18 @@ def is_permissions(*permission, compare=CompareConstants.OR): return is_permissions +def check_batch_permissions(request: Request, id_list: List[str], id_key: str, permissions: tuple, + compare=CompareConstants.OR, **kwargs) -> List[str]: + result_list = [] + for resource_id in id_list: + kwargs[id_key] = resource_id + exit_list = list( + map(lambda p: exist(request.auth.role_list, request.auth.permission_list, p, request, **kwargs), + permissions) + ) + if any(exit_list) if compare == CompareConstants.OR else all(exit_list): + result_list.append(resource_id) + return result_list def has_permissions(*permission, compare=CompareConstants.OR): """ diff --git a/apps/common/constants/permission_constants.py b/apps/common/constants/permission_constants.py index ab41ff0e00d..d6a97dd3583 100644 --- a/apps/common/constants/permission_constants.py +++ b/apps/common/constants/permission_constants.py @@ -191,6 +191,8 @@ class Operate(Enum): TRIGGER_EDIT = "READ+TRIGGER_EDIT" TRIGGER_CREATE = "READ+TRIGGER_CREATE" TRIGGER_DELETE = "READ+TRIGGER_DELETE" + BATCH_DELETE = "READ+BATCH_DELETE" + BATCH_MOVE = "READ+BATCH_MOVE" class RoleGroup(Enum): @@ -571,7 +573,16 @@ class PermissionConstants(Enum): parent_group=[WorkspaceGroup.TOOL, UserGroup.TOOL], resource_permission_group_list=[ResourcePermissionConst.TOOL_MANGE] ) - + TOOL_BATCH_MOVE = Permission( + group=Group.TOOL, operate=Operate.BATCH_MOVE, role_list=[RoleConstants.ADMIN, RoleConstants.USER], + parent_group=[WorkspaceGroup.TOOL, UserGroup.TOOL], + resource_permission_group_list=[ResourcePermissionConst.TOOL_MANGE] + ) + TOOL_BATCH_DELETE = Permission( + group=Group.TOOL, operate=Operate.BATCH_DELETE, role_list=[RoleConstants.ADMIN, RoleConstants.USER], + parent_group=[WorkspaceGroup.TOOL, UserGroup.TOOL], + resource_permission_group_list=[ResourcePermissionConst.TOOL_MANGE] + ) TOOL_EDIT = Permission( group=Group.TOOL, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN, RoleConstants.USER], parent_group=[WorkspaceGroup.TOOL, UserGroup.TOOL], @@ -694,6 +705,16 @@ class PermissionConstants(Enum): resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE], parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE] ) + KNOWLEDGE_BATCH_DELETE = Permission(group=Group.KNOWLEDGE, operate=Operate.BATCH_DELETE, + role_list=[RoleConstants.ADMIN, RoleConstants.USER], + resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE], + parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE], + ) + KNOWLEDGE_BATCH_MOVE = Permission(group=Group.KNOWLEDGE, operate=Operate.BATCH_MOVE, + role_list=[RoleConstants.ADMIN, RoleConstants.USER], + resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE], + parent_group=[WorkspaceGroup.KNOWLEDGE, UserGroup.KNOWLEDGE], + ) KNOWLEDGE_RESOURCE_AUTHORIZATION = Permission( group=Group.KNOWLEDGE, operate=Operate.AUTH, role_list=[RoleConstants.ADMIN, RoleConstants.USER], resource_permission_group_list=[ResourcePermissionConst.KNOWLEDGE_MANGE], @@ -1031,6 +1052,16 @@ class PermissionConstants(Enum): resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANGE], parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION], ) + APPLICATION_BATCH_DELETE = Permission(group=Group.APPLICATION, operate=Operate.BATCH_DELETE, + role_list=[RoleConstants.ADMIN, RoleConstants.USER], + resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANGE], + parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION], + ) + APPLICATION_BATCH_MOVE = Permission(group=Group.APPLICATION, operate=Operate.BATCH_MOVE, + role_list=[RoleConstants.ADMIN, RoleConstants.USER], + resource_permission_group_list=[ResourcePermissionConst.APPLICATION_MANGE], + parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION], + ) APPLICATION_RESOURCE_AUTHORIZATION = Permission(group=Group.APPLICATION, operate=Operate.AUTH, role_list=[RoleConstants.ADMIN, RoleConstants.USER], parent_group=[WorkspaceGroup.APPLICATION, UserGroup.APPLICATION], diff --git a/apps/knowledge/serializers/common.py b/apps/knowledge/serializers/common.py index 50d8b16275d..e33fa545251 100644 --- a/apps/knowledge/serializers/common.py +++ b/apps/knowledge/serializers/common.py @@ -62,6 +62,8 @@ def is_valid(self, *, model=None, raise_exception=False): raise AppApiException(500, _('The following id does not exist: {error_id_list}').format( error_id_list=error_id_list)) +class BatchMoveSerializer(BatchSerializer): + folder_id = serializers.CharField(required=True, label=_('folder id')) class ProblemParagraphObject: def __init__(self, knowledge_id: str, document_id: str, paragraph_id: str, problem_content: str): diff --git a/apps/locales/en_US/LC_MESSAGES/django.po b/apps/locales/en_US/LC_MESSAGES/django.po index 6d7e7490f31..f4c31a5bb37 100644 --- a/apps/locales/en_US/LC_MESSAGES/django.po +++ b/apps/locales/en_US/LC_MESSAGES/django.po @@ -9168,4 +9168,10 @@ msgid "Document does not belong to current knowledge" msgstr "Document does not belong to current knowledge" msgid "Move an application" -msgstr "Move an application" \ No newline at end of file +msgstr "Move an application" + +msgid "Batch delete applications" +msgstr "Batch delete applications" + +msgid "Batch move applications" +msgstr "Batch move applications" diff --git a/apps/locales/zh_CN/LC_MESSAGES/django.po b/apps/locales/zh_CN/LC_MESSAGES/django.po index 9827d4b61d5..b6f48135315 100644 --- a/apps/locales/zh_CN/LC_MESSAGES/django.po +++ b/apps/locales/zh_CN/LC_MESSAGES/django.po @@ -9291,4 +9291,10 @@ msgid "Document does not belong to current knowledge" msgstr "文档不属于当前知识库" msgid "Move an application" -msgstr "移动应用程序" \ No newline at end of file +msgstr "移动应用程序" + +msgid "Batch delete applications" +msgstr "批量删除应用" + +msgid "Batch move applications" +msgstr "批量移动应用" diff --git a/apps/locales/zh_Hant/LC_MESSAGES/django.po b/apps/locales/zh_Hant/LC_MESSAGES/django.po index 9721ca9a7e4..303f9450601 100644 --- a/apps/locales/zh_Hant/LC_MESSAGES/django.po +++ b/apps/locales/zh_Hant/LC_MESSAGES/django.po @@ -9288,4 +9288,10 @@ msgid "Document does not belong to current knowledge" msgstr "文件不屬於當前知識庫" msgid "Move an application" -msgstr "移動應用程序" \ No newline at end of file +msgstr "移動應用程序" + +msgid "Batch delete applications" +msgstr "批量刪除應用" + +msgid "Batch move applications" +msgstr "批量移動應用"