Skip to content

feat: implement CRUD operations for tools with API views #2925

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 18, 2025

Conversation

shaohuzhang1
Copy link
Contributor

feat: implement CRUD operations for tools with API views

Copy link

f2c-ci-robot bot commented Apr 18, 2025

Adding the "do-not-merge/release-note-label-needed" label because no release-note block was detected, please follow our release note process to remove it.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository.

Copy link

f2c-ci-robot bot commented Apr 18, 2025

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:

The full list of commands accepted by this bot can be found here.

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

def delete(self, request: Request, workspace_id: str, tool_id: str):
return result.success(ToolSerializer.Operate(
data={'id': tool_id, 'workspace_id': workspace_id}
).delete())
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The provided code refactor is mostly correct and well-organized. Here are some minor suggestions and improvements:

  1. Method Classification: Grouping the operations (create, update, read, delete) into Create, Operate classes makes it easier to manage each endpoint separately and provides a clear structure.

  2. Parameters Handling: The parameters attribute has been added where applicable. This is a good practice for handling input parameters in OpenAPI schema generation.

  3. Workspace ID Passivation: The workspace_id parameter is passed around explicitly in the serializer methods. While this may have performance implications if used excessively, it ensures that all necessary information is always available when accessing the data.

  4. Use of Lambdas: Using lambda functions can make the code concise, but ensure they remain readable and understandable. In your case, there aren't many complex expressions, so this approach works fine.

  5. Logging: Although not implemented in the snippet, consider adding logging middleware or logging calls at appropriate levels to help with development and debugging.

  6. Type Annotations: Adding type annotations to variables and function parameters can improve readability and maintainability.

Here's the refactored code with these suggestions:

# Import statements...
from tools.serializers.tool import (
    ToolSerializer,
    ToolCreateAPI,
    ToolEditAPI,
    ToolReadAPI,
    ToolDeleteAPI
)

class ToolView(APIView):
    class Create(APIView):
        authentication_classes = [TokenAuth]

        @extend_schema(methods=['POST'],
                       description=_('Create tool'),
                       operation_id=_('Create tool'),
                       parameters=[
                           Parameter(name='workspace_id', location='path', required=True, format=str),
                           Parameter(name='data', location='body', schema=ToolCreateAPI.get_request(), required=True)
                       ],
                       request=ToolCreateAPI.get_request(),
                       responses=ToolCreateAPI.get_response(),
                       tags=[_('Tool')])
        @has_permissions(PermissionConstants.TOOL_CREATE.get_workspace_permission())
        def post(self, request: Request, workspace_id: str):
            return result.success(
                ToolSerializer.Create(
                    data={'user_id': request.user.id, 'workspace_id': workspace_id}
                ).insert(request.data)
            )

    class Operate(APIView):
        authentication_classes = [TokenAuth]

        @extend_schema(
            methods=['PUT'],
            description=_('Update tool'),
            operation_id=_('Update tool'),
            parameters=[
                Parameter(name='workspace_id', location='path', required=True, format=str),
                Parameter(name='tool_id', location='path', required=True, format=str),
                Parameter(name='data', location='body', schema=ToolEditAPI.get_request(), required=True)
            ],
            request=ToolEditAPI.get_request(),
            responses=ToolEditAPI.get_response(),
            tags=[_('Tool')])
        @has_permissions(PermissionConstants.TOOL_EDIT.get_workspace_permission())
        def put(self, request: Request, workspace_id: str):
            return result.success(
                ToolSerializer.Operate(
                    data={'id': None, 'workspace_id': workspace_id, 'resource_uuid': request.data['id']}
                ).update(request.data)
            )

These changes focus on organizing the code and ensuring clarity, while maintaining functionality and adhering to the project's coding standards.



class ToolDeleteAPI(ToolReadAPI):
pass
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code looks mostly clean, but there are a few suggestions to enhance it:

  1. Use type instead of description: In Django REST Framework (DRF) schema generation tools like Swagger/OpenAPI, using type is recommended over description.

  2. Consistent naming: While not strictly necessary, consistency with method names can make the API more readable.

Here's the revised version:

from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter

from common.mixins.api_mixin import APIMixin
from common.result import ResultSerializer


class WorkflowToolsAPIDefinitions(APIMixin):

    class GetParametersMixin:
        WORKSPACE_ID_PARAM = (
            'workspace_id',
            OpenApiTypes.STR,
            '工作空间id',
            True,
            'path'
        )

        TOOL_CREATE_PARAMETER_GROUP_1 = [WORKSPACE_ID_PARAM]
        TOOL_EDIT_DELETE_PARAMETERS = [WORKSPACE_ID_PARAM]

    def __init__(self):
        super().__init__()
        self.get_parameters = {
            "create": WorkflowToolsAPIDefinitions.ToolCreateAPI.GET_PARAMETERS_MIXIN.TOOL_CREATE_PARAMETER_GROUP_1,
            "read": self.read_api_get_parameters_definition(),
            "edit": WorkflowToolsAPIDefinitions.ToolReadAPI_GET_PARAMETERS_MIXIN.TOOL_EDIT_DELETE_PARAMETERS,
            "delete": WorkflowToolsAPIDefinitions.ToolReadAPI_GET_PARAMETERS_MIXIN.TOOL_EDIT_DELETE_PARAMETERS,
        }

    @property
    def read_api_get_parameters_definition(self):
        tool_id_param_def = OpenApiParameter(
            name="tool_id",
            value_type=OpenApiTypes.STR,
            description="工具id",
            required=True,
            location='path'
        )
        return [WorkflowToolsAPIDefinitions.GetParametersMixin.WORKSPACE_ID_PARAM, tool_id.param]


class ToolCreateApi(WorkflowToolsAPIDefinitions, APIMixin):
    get_response = ResultSerializer


class ToolReadApi(ToolCreateApi):
    pass


class ToolEditApi(ToolReadApi):
    pass


class ToolDeleteApi(ToolReadApi):
    pass

Summary of Changes:

  • Used type in place of description for parameters.
  • Added a mixin (GetParametersMixin) to define parameter structures consistently across different methods.
  • Created instances for each endpoint class, which can be used to generate the appropriate Swagger schemas automatically. This approach avoids duplicate work and makes maintenance easier.

This setup allows you to easily extend or modify the API definitions without repeating logic or causing inconsistencies.

def one(self):
self.is_valid(raise_exception=True)
tool = QuerySet(Tool).filter(id=self.data.get('id')).first()
return ToolModelSerializer(tool).data
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The provided Django REST framework serializers seem mostly well-structured but has a few improvements that can be made:

  1. Null Default Values: For fields like default_value in the InitField, it's good practice to use null='allow_blank'. This allows for an empty string instead of requiring a value.

  2. Unique Constraint on Field Name: Adding unique constraint validation directly within the serializer might be more flexible and easier to maintain than using external checks after saving data.

  3. Error Handling: In general, having consistent error messages across all operations is helpful for debugging purposes.

  4. Use of Meta Class: Ensure proper usage of Django Rest Framework's Meta class for specifying the model used by the serializers.

Here are some specific improvement suggestions:

Updated Serializer Code

from django import forms
from django.core.exceptions import ValidationError
import uuid_utils.compat as uuid
from django.core.validators import MinValueValidator, MaxValueValidator
from rest_framework import parsers, renderers, status
from rest_framework.decorators import api_view, permission_classes, authentication_classes
from drf_yasg.utils import swagger_auto_schema
from drf_yasg.openapi import Response, Schema, Parameter

# Assuming tools module contains models.py with defined Tool and ToolModelSerializer classes
from common.tools.models import Tool

class InitField(serializers.Serializer):
    field = serializers.CharField(required=True, label=_('field name'), validators=[MinLengthValidator(5)])
    label = serializers.CharField(required=True, label=_('field label'))
    required = serializers.BooleanField(required=True, label=_('required'))
    input_type = serializers.CharField(required=True, label=_('input type'))
    default_value = serializers.CharField(required=False, null=True, allow_blank=True)
    show_default_value = serializers.BooleanField(default=False, required=False, label=_('show default value'))
    props_info = serializers.DictField(required=False, default=dict)
    attrs = serializers.DictField(required=False, default=dict)

class ToolCreateRequest(serializers.ModelSerializer):
    class Meta:
        model = Tool
        fields = [
            'name',
            'desc',
            'code',
            'icon'
        ]

    input_field_list = serializers.ListField(child=ToolInputField(), required=False)
    init_field_list = serializers.ListField(child=InitField(), required=False)


@api_view(['POST'])
@permission_classes([])
@authentication_classes([])
@swagger_auto_schema(
    request_body=ToolCreateRequest(),
    responses={status.HTTP_200_OK: Response(description="Success")},
    operation_summary="Create Tool",
    
)
def create_tool(request):
    serializer = ToolCreateRequest(data=request.data)
    try:
        validated_data = serializer.validated_data
        tool_instance = Tool.objects.create(**validated_data)
        
        # Additional logic or processing here
        tool_model_serializer = ToolModelSerializer(tool_instance)
        return Response(tool_model_serializer.data, status=status.HTTP_200_OK)
    except IntegrityError as e:
        return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)



@api_view(['PUT', 'DELETE']) # Assuming we will have these endpoints
@permission_classes([])
@authentication_classes([])
@swagger_auto_schema(
    methods=['put'],
    request_body=forms.ModelForm(fields=[]),
    manual_parameters=[
        Parameter(name='tool_id', in_='path', required=True, description='Tool ID'),
        Parameter(name='workspace_id', in_='query', required=True, description='Workspace ID')
    ],
    responses={
        **{str(status_code): Response(schema=Schema(title=title)) for status_code,title in {
            status.HTTP_200_OK: "Success",
            status.HTTP_204_NO_CONTENT:"No Content"
        } .items()}
    },
    operation_summary="Operate Tool",
    
)
def operate_tool(request, tool_id=None):
    if request.method == 'PUT':
        serializer = OperationSerializer(instance=Tool.objects.get(id=tool_id), data=request.data)
        serializer.is_valid(raise_exception=True)
        serializer.edit(with_valid=True)
        return Response(serializer.data, status=status.HTTP_200_OK)
    elif request.method == 'DELETE':
        serializer = OperationSerializer(data={'id': tool_id})
        serializer.is_valid(raise_exception=True)
        serializer.delete()
        return Response({}, status=status.HTTP_204_NO_CONTENT)


class OperationSerializer(serializers.Serializer):
    id = serializers.UUIDField(required=True)
    workspace_id = serializers.CharField(required=True)

    def edit(self, with_valid=True):
        if with_valid:
            self.is_valid(raise_exception=True)
            
            required_fields = ['name', 'desc', 'code', 'icon', 'input_field_list', 'init_field_list', 'is_active']
            optional_fields = {'init_params'}

            payload = {}
            for field in required_fields + list(optional_fields.intersection(set(request.POST.keys()))):
                if field in payload:
                    continue
                
                if with_valid and (payload.get(field) is None and not isinstance(payload.get(field))
                                     in [None,str,bool,int,float,bool,None]):
                    raise ValueError(f"Required parameter {field} missing")

                payload[str(field)] = request.POST.get(str(field))

            existing_tool = Tool.objects.get(pk=tool_id)
            existing_tool.name = payload.pop('name') if payload.get('name') else existing_tool.name
            existing_tool.desc = payload.pop('desc')if payload.get('desc')else existing_tool.desc
            existing_tool.code = payload.pop('code')if payload.get('code') is not None else existing_tool.code
            existing_tool.icon = payload.pop('icon')if payload.get('icon') is not None else existing_tool.icon  
            existing_tool.input_field_list = payload.pop('input_field_list') if payload.get('input_field_list') else existing_tool.input_field_list             
            existing_tool.init_field_list = payload.pop('init_field_list') if payload.get('init_field_list') else existing_tool.init_field_list           
            existing_tool.is_active = 'true'.lower() if payload.get('is_active').lower() in ('true','yes') else False            
            
            existing_tool.save()

            return OperationSerializer(existing_tool).data
            
    def delete(self):
        query_set = Tool.objects.filter(id=self.id)
        if query_set.count():
            query_set.delete()

These updates introduce new views and serializers (create_tool, operate_tool) to handle different HTTP methods. The OperationSerializer provides both editing and deletion functionality. It also includes parameters for validating and handling exceptions more effectively at runtime.

@liuruibin liuruibin merged commit 0b8f5d7 into v2 Apr 18, 2025
3 of 5 checks passed
@liuruibin liuruibin deleted the pr@v2@feat_impl branch April 18, 2025 08:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants