From 4a5310b88e2357b1f80d7ed3c2fa3e48217c501c Mon Sep 17 00:00:00 2001 From: Saurabhkmr98 Date: Tue, 19 Aug 2025 20:40:29 +0530 Subject: [PATCH 1/5] add missing fields and methods in endpoints --- apps/api/plane/api/serializers/project.py | 4 ++++ apps/api/plane/api/urls/issue.py | 4 +++- apps/api/plane/api/urls/member.py | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/api/plane/api/serializers/project.py b/apps/api/plane/api/serializers/project.py index 6cb31ac82a9..e6a257f3eb4 100644 --- a/apps/api/plane/api/serializers/project.py +++ b/apps/api/plane/api/serializers/project.py @@ -45,6 +45,10 @@ class Meta: "archive_in", "close_in", "timezone", + "logo_props", + "external_source", + "external_id", + "is_issue_type_enabled", ] read_only_fields = [ diff --git a/apps/api/plane/api/urls/issue.py b/apps/api/plane/api/urls/issue.py index c8d1ea4afe1..2636c8618f9 100644 --- a/apps/api/plane/api/urls/issue.py +++ b/apps/api/plane/api/urls/issue.py @@ -89,7 +89,9 @@ ), path( "workspaces//projects//issues//issue-attachments//", - IssueAttachmentDetailAPIEndpoint.as_view(http_method_names=["get", "delete"]), + IssueAttachmentDetailAPIEndpoint.as_view( + http_method_names=["get", "delete", "patch"] + ), name="issue-attachment", ), ] diff --git a/apps/api/plane/api/urls/member.py b/apps/api/plane/api/urls/member.py index 14a09c832c3..ed87a950406 100644 --- a/apps/api/plane/api/urls/member.py +++ b/apps/api/plane/api/urls/member.py @@ -5,7 +5,7 @@ urlpatterns = [ path( "workspaces//projects//members/", - ProjectMemberAPIEndpoint.as_view(http_method_names=["get"]), + ProjectMemberAPIEndpoint.as_view(http_method_names=["get", "post"]), name="project-members", ), path( From 89153ecbaf3202b60af3b4a6c4cdc3cad6fba98a Mon Sep 17 00:00:00 2001 From: Saurabhkmr98 Date: Wed, 20 Aug 2025 14:00:21 +0530 Subject: [PATCH 2/5] add POST method for project members --- apps/api/plane/api/views/member.py | 129 ++++++++++++++++++++++++++++- 1 file changed, 128 insertions(+), 1 deletion(-) diff --git a/apps/api/plane/api/views/member.py b/apps/api/plane/api/views/member.py index 8ae66252010..a241cd784f4 100644 --- a/apps/api/plane/api/views/member.py +++ b/apps/api/plane/api/views/member.py @@ -1,15 +1,26 @@ +# Python imports +import uuid + +# Django imports +from django.core.validators import validate_email +from django.contrib.auth.hashers import make_password +from django.core.exceptions import ValidationError + + # Third Party imports from rest_framework.response import Response from rest_framework import status from drf_spectacular.utils import ( extend_schema, OpenApiResponse, + OpenApiRequest, + OpenApiExample, ) # Module imports from .base import BaseAPIView from plane.api.serializers import UserLiteSerializer -from plane.db.models import User, Workspace, WorkspaceMember, ProjectMember +from plane.db.models import User, Workspace, WorkspaceMember, ProjectMember, Project from plane.app.permissions import ProjectMemberPermission, WorkSpaceAdminPermission from plane.utils.openapi import ( WORKSPACE_SLUG_PARAMETER, @@ -135,3 +146,119 @@ def get(self, request, slug, project_id): ).data return Response(users, status=status.HTTP_200_OK) + + @extend_schema( + operation_id="add_project_members", + summary="Add a user to the specified project.", + description="Add a user to the specified project.", + tags=["Members"], + request=OpenApiRequest( + request=UserLiteSerializer, + examples=[ + OpenApiExample( + "Add Project Member", + value={ + "email": "test@test.com", + "display_name": "Test User", + }, + description="Example request for adding a project member", + ), + ], + ), + parameters=[WORKSPACE_SLUG_PARAMETER, PROJECT_ID_PARAMETER], + responses={ + 200: OpenApiResponse( + description="User added to the project", + response=UserLiteSerializer, + examples=[PROJECT_MEMBER_EXAMPLE], + ), + 401: UNAUTHORIZED_RESPONSE, + 403: FORBIDDEN_RESPONSE, + 404: PROJECT_NOT_FOUND_RESPONSE, + }, + ) + def post(self, request, slug, project_id): + # ------------------- Validation ------------------- + if ( + request.data.get("email") is None + or request.data.get("display_name") is None + ): + return Response( + { + "error": "Expected email, display_name, workspace_slug, project_id, one or more of the fields are missing." + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + email = request.data.get("email") + + try: + validate_email(email) + except ValidationError: + return Response( + {"error": "Invalid email provided"}, status=status.HTTP_400_BAD_REQUEST + ) + + workspace = Workspace.objects.filter(slug=slug).first() + project = Project.objects.filter(pk=project_id).first() + + if not all([workspace, project]): + return Response( + {"error": "Provided workspace or project does not exist"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + user = User.objects.filter(email=email).first() + + workspace_member = None + project_member = None + + if user: + # Check if user is part of the workspace + workspace_member = WorkspaceMember.objects.filter( + workspace=workspace, member=user + ).first() + if workspace_member: + # Check if user is part of the project + project_member = ProjectMember.objects.filter( + project=project, member=user + ).first() + if project_member: + return Response( + {"error": "User is already part of the workspace and project"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + # If user does not exist, create the user + if not user: + user = User.objects.create( + email=email, + display_name=request.data.get("display_name"), + first_name=request.data.get("first_name", ""), + last_name=request.data.get("last_name", ""), + username=uuid.uuid4().hex, + password=make_password(uuid.uuid4().hex), + is_password_autoset=True, + is_active=False, + avatar_asset_id=request.data.get("avatar_asset_id", None), + ) + user.save() + + # Create a workspace member for the user if not already a member + if not workspace_member: + workspace_member = WorkspaceMember.objects.create( + workspace=workspace, member=user, role=request.data.get("role", 5) + ) + workspace_member.save() + + # Create a project member for the user if not already a member + if not project_member: + project_member = ProjectMember.objects.create( + project=project, member=user, role=request.data.get("role", 5) + ) + project_member.save() + + # Serialize the user and return the response + user_data = UserLiteSerializer(user).data + + return Response(user_data, status=status.HTTP_201_CREATED) From 6f0fd89b2d2d2c4f4cd294f52ef1305cdb5a51cd Mon Sep 17 00:00:00 2001 From: Saurabhkmr98 Date: Wed, 20 Aug 2025 14:02:30 +0530 Subject: [PATCH 3/5] make project_id as uuid in url pattern --- apps/api/plane/api/urls/member.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/plane/api/urls/member.py b/apps/api/plane/api/urls/member.py index ed87a950406..dafd2eb07f0 100644 --- a/apps/api/plane/api/urls/member.py +++ b/apps/api/plane/api/urls/member.py @@ -4,7 +4,7 @@ urlpatterns = [ path( - "workspaces//projects//members/", + "workspaces//projects//members/", ProjectMemberAPIEndpoint.as_view(http_method_names=["get", "post"]), name="project-members", ), From 8de7e7b08ac13ac1026be1cbeed2a194187cb2ab Mon Sep 17 00:00:00 2001 From: Saurabhkmr98 Date: Wed, 20 Aug 2025 17:05:35 +0530 Subject: [PATCH 4/5] remove post method --- apps/api/plane/api/urls/member.py | 2 +- apps/api/plane/api/views/member.py | 129 +---------------------------- 2 files changed, 2 insertions(+), 129 deletions(-) diff --git a/apps/api/plane/api/urls/member.py b/apps/api/plane/api/urls/member.py index dafd2eb07f0..a2b331ea1c5 100644 --- a/apps/api/plane/api/urls/member.py +++ b/apps/api/plane/api/urls/member.py @@ -5,7 +5,7 @@ urlpatterns = [ path( "workspaces//projects//members/", - ProjectMemberAPIEndpoint.as_view(http_method_names=["get", "post"]), + ProjectMemberAPIEndpoint.as_view(http_method_names=["get"]), name="project-members", ), path( diff --git a/apps/api/plane/api/views/member.py b/apps/api/plane/api/views/member.py index a241cd784f4..8ae66252010 100644 --- a/apps/api/plane/api/views/member.py +++ b/apps/api/plane/api/views/member.py @@ -1,26 +1,15 @@ -# Python imports -import uuid - -# Django imports -from django.core.validators import validate_email -from django.contrib.auth.hashers import make_password -from django.core.exceptions import ValidationError - - # Third Party imports from rest_framework.response import Response from rest_framework import status from drf_spectacular.utils import ( extend_schema, OpenApiResponse, - OpenApiRequest, - OpenApiExample, ) # Module imports from .base import BaseAPIView from plane.api.serializers import UserLiteSerializer -from plane.db.models import User, Workspace, WorkspaceMember, ProjectMember, Project +from plane.db.models import User, Workspace, WorkspaceMember, ProjectMember from plane.app.permissions import ProjectMemberPermission, WorkSpaceAdminPermission from plane.utils.openapi import ( WORKSPACE_SLUG_PARAMETER, @@ -146,119 +135,3 @@ def get(self, request, slug, project_id): ).data return Response(users, status=status.HTTP_200_OK) - - @extend_schema( - operation_id="add_project_members", - summary="Add a user to the specified project.", - description="Add a user to the specified project.", - tags=["Members"], - request=OpenApiRequest( - request=UserLiteSerializer, - examples=[ - OpenApiExample( - "Add Project Member", - value={ - "email": "test@test.com", - "display_name": "Test User", - }, - description="Example request for adding a project member", - ), - ], - ), - parameters=[WORKSPACE_SLUG_PARAMETER, PROJECT_ID_PARAMETER], - responses={ - 200: OpenApiResponse( - description="User added to the project", - response=UserLiteSerializer, - examples=[PROJECT_MEMBER_EXAMPLE], - ), - 401: UNAUTHORIZED_RESPONSE, - 403: FORBIDDEN_RESPONSE, - 404: PROJECT_NOT_FOUND_RESPONSE, - }, - ) - def post(self, request, slug, project_id): - # ------------------- Validation ------------------- - if ( - request.data.get("email") is None - or request.data.get("display_name") is None - ): - return Response( - { - "error": "Expected email, display_name, workspace_slug, project_id, one or more of the fields are missing." - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - email = request.data.get("email") - - try: - validate_email(email) - except ValidationError: - return Response( - {"error": "Invalid email provided"}, status=status.HTTP_400_BAD_REQUEST - ) - - workspace = Workspace.objects.filter(slug=slug).first() - project = Project.objects.filter(pk=project_id).first() - - if not all([workspace, project]): - return Response( - {"error": "Provided workspace or project does not exist"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - user = User.objects.filter(email=email).first() - - workspace_member = None - project_member = None - - if user: - # Check if user is part of the workspace - workspace_member = WorkspaceMember.objects.filter( - workspace=workspace, member=user - ).first() - if workspace_member: - # Check if user is part of the project - project_member = ProjectMember.objects.filter( - project=project, member=user - ).first() - if project_member: - return Response( - {"error": "User is already part of the workspace and project"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - # If user does not exist, create the user - if not user: - user = User.objects.create( - email=email, - display_name=request.data.get("display_name"), - first_name=request.data.get("first_name", ""), - last_name=request.data.get("last_name", ""), - username=uuid.uuid4().hex, - password=make_password(uuid.uuid4().hex), - is_password_autoset=True, - is_active=False, - avatar_asset_id=request.data.get("avatar_asset_id", None), - ) - user.save() - - # Create a workspace member for the user if not already a member - if not workspace_member: - workspace_member = WorkspaceMember.objects.create( - workspace=workspace, member=user, role=request.data.get("role", 5) - ) - workspace_member.save() - - # Create a project member for the user if not already a member - if not project_member: - project_member = ProjectMember.objects.create( - project=project, member=user, role=request.data.get("role", 5) - ) - project_member.save() - - # Serialize the user and return the response - user_data = UserLiteSerializer(user).data - - return Response(user_data, status=status.HTTP_201_CREATED) From 8d1ee2d458e346a7ebce60e5818e4e7b620d4566 Mon Sep 17 00:00:00 2001 From: Saurabhkmr98 Date: Wed, 20 Aug 2025 20:36:09 +0530 Subject: [PATCH 5/5] fix method reordering --- apps/api/plane/api/urls/issue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/plane/api/urls/issue.py b/apps/api/plane/api/urls/issue.py index 2636c8618f9..df64684de23 100644 --- a/apps/api/plane/api/urls/issue.py +++ b/apps/api/plane/api/urls/issue.py @@ -90,7 +90,7 @@ path( "workspaces//projects//issues//issue-attachments//", IssueAttachmentDetailAPIEndpoint.as_view( - http_method_names=["get", "delete", "patch"] + http_method_names=["get", "patch", "delete"] ), name="issue-attachment", ),